Compare commits
8
Commits
fix-merged-bb
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
25f3baddc9 | ||
|
|
1920ff183b | ||
|
|
a72d35776e | ||
|
|
3af0b8188f | ||
|
|
6071d56dc3 | ||
|
|
d9c04d36c0 | ||
|
|
314b598c66 | ||
|
|
af2854d771 |
No files matched your search
@@ -0,0 +1,241 @@
|
|||||||
|
# ZenAI KPC Counter — Edge Deployment Guide
|
||||||
|
|
||||||
|
Production deployment for **RK3588** (or compatible RKNN NPU) edge devices running:
|
||||||
|
|
||||||
|
| Component | Script | systemd unit |
|
||||||
|
|-----------|--------|--------------|
|
||||||
|
| RTSP counter (RKNN + ByteTrack) | `counter_live_rknn.py` | `zenai-kpc-counter.service` |
|
||||||
|
| Web dashboard (Flask) | `counter_dashboard.py` | `zenai-kpc-dashboard.service` |
|
||||||
|
|
||||||
|
Both processes share a single `.env` file and read/write the same SQLite database and state JSON.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Prerequisites
|
||||||
|
|
||||||
|
### Hardware & OS
|
||||||
|
|
||||||
|
- RK3588 board (or Jetson/RK device with RKNN Lite runtime)
|
||||||
|
- Linux with systemd
|
||||||
|
- Network access to the RTSP camera stream
|
||||||
|
|
||||||
|
### System packages
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install -y python3 python3-venv python3-pip ffmpeg libgl1
|
||||||
|
```
|
||||||
|
|
||||||
|
`ffmpeg` is required for low-latency RTSP capture via OpenCV. `libgl1` is often needed for `opencv-python` on headless systems.
|
||||||
|
|
||||||
|
### RKNN model
|
||||||
|
|
||||||
|
Export or copy your `.rknn` model to the device, e.g.:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/opt/models/your_model.rknn
|
||||||
|
```
|
||||||
|
|
||||||
|
Set `MODEL_PATH` in `.env` to match. The model class count must match `NUM_CLASSES`, and `OBJECT_CLASS_ID` must point at the class you count.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Directory layout
|
||||||
|
|
||||||
|
Default paths used by the service files and `env.example`:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/opt/zenai-kpc-python/ # application code (this repo)
|
||||||
|
├── counter_live_rknn.py
|
||||||
|
├── counter_dashboard.py
|
||||||
|
├── counter_store.py
|
||||||
|
├── templates/
|
||||||
|
├── venv/ # Python virtual environment (created during install)
|
||||||
|
├── .env # runtime config (not in git)
|
||||||
|
├── env.example # template — copy to .env
|
||||||
|
└── DEPLOY.md
|
||||||
|
|
||||||
|
/opt/zenai-kpc-counter/ # persistent runtime data (created automatically)
|
||||||
|
├── counter.db # SQLite daily records
|
||||||
|
├── current_counter.json # live counting-day state
|
||||||
|
├── snapshots/ # crossing/detect JPEGs (if enabled)
|
||||||
|
└── crossings.csv # optional per-event CSV
|
||||||
|
|
||||||
|
/opt/models/ # RKNN models (deploy separately)
|
||||||
|
/dev/shm/zenai-kpc-counter/ # live JPEG frame for dashboard video (tmpfs)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Install application
|
||||||
|
|
||||||
|
### 3.1 Copy code to the device
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo mkdir -p /opt/zenai-kpc-python
|
||||||
|
sudo rsync -av --exclude venv --exclude .env --exclude __pycache__ \
|
||||||
|
./ /opt/zenai-kpc-python/
|
||||||
|
# Or: sudo git clone <repo-url> /opt/zenai-kpc-python
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 Create virtual environment and install dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /opt/zenai-kpc-python
|
||||||
|
sudo python3 -m venv venv
|
||||||
|
sudo ./venv/bin/pip install --upgrade pip
|
||||||
|
sudo ./venv/bin/pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
> `rknn-toolkit-lite2` is platform-specific. Install on the target ARM device, not on a Windows dev machine.
|
||||||
|
|
||||||
|
### 3.3 Create runtime config
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /opt/zenai-kpc-python
|
||||||
|
sudo cp env.example .env
|
||||||
|
sudo nano .env
|
||||||
|
```
|
||||||
|
|
||||||
|
**Minimum values to edit before starting:**
|
||||||
|
|
||||||
|
| Variable | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| `SOURCE` | RTSP URL or local video file path |
|
||||||
|
| `MODEL_PATH` | Path to your `.rknn` model on device |
|
||||||
|
| `NUM_CLASSES` | Must match the exported model |
|
||||||
|
| `OBJECT_CLASS_ID` | Class index of the object being counted |
|
||||||
|
| `CLASS_OBJECT` / `OBJECT_LABEL` | Labels stored in DB (e.g. `karung`) |
|
||||||
|
| `LINE_Y1_FRAC` / `LINE_Y2_FRAC` | Counting line positions (tune per camera) |
|
||||||
|
| `SECRET_KEY` | Random string for Flask sessions |
|
||||||
|
|
||||||
|
Ensure `STATE_FILE` and `DB_PATH` both live under `/opt/zenai-kpc-counter/` so data survives reboots (avoid `/tmp` in production).
|
||||||
|
|
||||||
|
### 3.4 Create data directories (optional — app creates most paths automatically)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo mkdir -p /opt/zenai-kpc-counter /opt/models /dev/shm/zenai-kpc-counter
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Install systemd services
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /opt/zenai-kpc-python
|
||||||
|
sudo cp zenai-kpc-counter.service zenai-kpc-dashboard.service /etc/systemd/system/
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
sudo systemctl enable zenai-kpc-counter zenai-kpc-dashboard
|
||||||
|
sudo systemctl start zenai-kpc-counter
|
||||||
|
sudo systemctl start zenai-kpc-dashboard
|
||||||
|
```
|
||||||
|
|
||||||
|
The dashboard unit starts **after** the counter unit (`After=zenai-kpc-counter.service`).
|
||||||
|
|
||||||
|
### Verify
|
||||||
|
|
||||||
|
```bash
|
||||||
|
systemctl status zenai-kpc-counter
|
||||||
|
systemctl status zenai-kpc-dashboard
|
||||||
|
journalctl -u zenai-kpc-counter -f
|
||||||
|
```
|
||||||
|
|
||||||
|
Open the dashboard in a browser:
|
||||||
|
|
||||||
|
```text
|
||||||
|
http://<device-ip>:5000
|
||||||
|
```
|
||||||
|
|
||||||
|
(Port is set by `DASHBOARD_PORT` in `.env`, default `5000`.)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Operations
|
||||||
|
|
||||||
|
### Restart after config change
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo systemctl restart zenai-kpc-counter
|
||||||
|
sudo systemctl restart zenai-kpc-dashboard
|
||||||
|
```
|
||||||
|
|
||||||
|
### View logs
|
||||||
|
|
||||||
|
```bash
|
||||||
|
journalctl -u zenai-kpc-counter -n 100 --no-pager
|
||||||
|
journalctl -u zenai-kpc-dashboard -n 100 --no-pager
|
||||||
|
```
|
||||||
|
|
||||||
|
### Stop services
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo systemctl stop zenai-kpc-dashboard zenai-kpc-counter
|
||||||
|
```
|
||||||
|
|
||||||
|
The counter handles `SIGTERM` gracefully — it finishes the current frame, persists state to SQLite, then exits.
|
||||||
|
|
||||||
|
### Update application code
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /opt/zenai-kpc-python
|
||||||
|
sudo systemctl stop zenai-kpc-dashboard zenai-kpc-counter
|
||||||
|
# rsync or git pull new code
|
||||||
|
sudo ./venv/bin/pip install -r requirements.txt # if dependencies changed
|
||||||
|
sudo systemctl start zenai-kpc-counter zenai-kpc-dashboard
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Troubleshooting
|
||||||
|
|
||||||
|
| Symptom | Things to check |
|
||||||
|
|---------|-----------------|
|
||||||
|
| Counter won't start | `journalctl -u zenai-kpc-counter`; verify `MODEL_PATH` exists; RKNN drivers installed |
|
||||||
|
| No RTSP frames | Ping camera; test with `ffplay <SOURCE>`; check `OPENCV_FFMPEG_CAPTURE_OPTIONS` |
|
||||||
|
| Dashboard shows 0 count | `STATE_FILE` in `.env` must match between counter and dashboard; check file exists |
|
||||||
|
| Live video blank | `LIVE_STREAM_ENABLED=true`; path matches `LIVE_STREAM_FRAME_PATH` in both processes; if using nginx, set `proxy_read_timeout` (see §7) |
|
||||||
|
| Wrong counts | Tune `LINE_Y1_FRAC`/`LINE_Y2_FRAC`, `CONF`, ByteTrack thresholds; enable `DEBUG_TRACKING=true` temporarily |
|
||||||
|
| Service keeps restarting | `journalctl -u zenai-kpc-counter -e`; often missing model, bad RTSP URL, or venv not created |
|
||||||
|
|
||||||
|
### Manual test (without systemd)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /opt/zenai-kpc-python
|
||||||
|
source venv/bin/activate
|
||||||
|
python counter_live_rknn.py # terminal 1
|
||||||
|
python counter_dashboard.py # terminal 2
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Optional: reverse proxy
|
||||||
|
|
||||||
|
For HTTPS or port 80 access, put nginx in front of the dashboard:
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name counter.example.com;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://127.0.0.1:5000;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_buffering off; # required for /api/live-video MJPEG stream
|
||||||
|
proxy_read_timeout 3600s; # keep long-lived MJPEG connections open
|
||||||
|
proxy_send_timeout 3600s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The live JPEG at `LIVE_STREAM_FRAME_PATH` can also be served statically by nginx if you prefer not to use the Flask MJPEG endpoint.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Security notes
|
||||||
|
|
||||||
|
- Change `SECRET_KEY` from the default before exposing the dashboard on a network.
|
||||||
|
- Services currently run as `root` for simplicity on edge devices. For hardened deployments, create a dedicated user, chown `/opt/zenai-kpc-counter`, and update the `User=` / `Group=` lines in the service files.
|
||||||
|
- Do not commit `.env` — it may contain RTSP credentials.
|
||||||
|
- Set `FLASK_DEBUG=false` in production.
|
||||||
+6
-4
@@ -92,9 +92,11 @@ CLASS_OBJECT=object
|
|||||||
OBJECT_CLASS_ID=0
|
OBJECT_CLASS_ID=0
|
||||||
|
|
||||||
# --- Line crossing ---
|
# --- Line crossing ---
|
||||||
# Two horizontal counting lines:
|
# Two horizontal counting lines with sequence logic:
|
||||||
# Line 1 (default ~33%): counts top-to-down (IN)
|
# Line 1 (IN): top→down counts IN (OUT→IN sequence, or IN-only)
|
||||||
# Line 2 (default ~66%): counts bottom-to-up (OUT)
|
# Line 2 (OUT): bottom→up counts OUT (IN→OUT sequence, or OUT-only)
|
||||||
|
# Passing the opposite line first only arms; the destination line still counts
|
||||||
|
# even if the opposite line was never crossed.
|
||||||
# Fixed y-coordinate for line 1/IN (overrides LINE_Y1_FRAC if set)
|
# Fixed y-coordinate for line 1/IN (overrides LINE_Y1_FRAC if set)
|
||||||
LINE_Y1=
|
LINE_Y1=
|
||||||
# Fraction of frame height for line 1 (default 0.33)
|
# Fraction of frame height for line 1 (default 0.33)
|
||||||
@@ -223,4 +225,4 @@ DASHBOARD_PORT=5000
|
|||||||
# Enable Flask debug mode (true/false) — auto-reloads on code changes; disable in production
|
# Enable Flask debug mode (true/false) — auto-reloads on code changes; disable in production
|
||||||
FLASK_DEBUG=false
|
FLASK_DEBUG=false
|
||||||
# Fallback name for the active counting-day JSON state file used by the dashboard
|
# Fallback name for the active counting-day JSON state file used by the dashboard
|
||||||
CURRENT_COUNTER_PATH=/tmp/bytetrack_current_counter.json
|
CURRENT_COUNTER_PATH=/tmp/bytetrack_current_counter.json
|
||||||
@@ -538,4 +538,4 @@ if __name__ == "__main__":
|
|||||||
print(f"Jetson counter dashboard at http://{DASHBOARD_HOST}:{DASHBOARD_PORT}")
|
print(f"Jetson counter dashboard at http://{DASHBOARD_HOST}:{DASHBOARD_PORT}")
|
||||||
print(f"DB: {DB_PATH}")
|
print(f"DB: {DB_PATH}")
|
||||||
print(f"State: {CURRENT_COUNTER_PATH}")
|
print(f"State: {CURRENT_COUNTER_PATH}")
|
||||||
app.run(host=DASHBOARD_HOST, port=DASHBOARD_PORT, debug=FLASK_DEBUG)
|
app.run(host=DASHBOARD_HOST, port=DASHBOARD_PORT, debug=FLASK_DEBUG)
|
||||||
+139
-40
@@ -694,8 +694,9 @@ def is_duplicate_cross(recent, cx, frame_idx):
|
|||||||
|
|
||||||
|
|
||||||
def _inherit_prev(tracked, new_tid, cx, cy, mono, max_age, max_px):
|
def _inherit_prev(tracked, new_tid, cx, cy, mono, max_age, max_px):
|
||||||
"""Find a recently-seen nearby track and return its (cx, cy, mono) so a new
|
"""Find a recently-seen nearby track for ID-switch continuation.
|
||||||
track ID can inherit the trajectory across an ID switch. Returns None if none.
|
|
||||||
|
Returns (cx, cy, mono, source_tid) or None.
|
||||||
|
|
||||||
The predecessor must be close in BOTH x and y (same physical object at the same
|
The predecessor must be close in BOTH x and y (same physical object at the same
|
||||||
spot). Matching on x only would let a new track inherit a far-away y, seeding a
|
spot). Matching on x only would let a new track inherit a far-away y, seeding a
|
||||||
@@ -712,10 +713,27 @@ def _inherit_prev(tracked, new_tid, cx, cy, mono, max_age, max_px):
|
|||||||
dist = abs(tcx - cx)
|
dist = abs(tcx - cx)
|
||||||
if dist <= best_dist:
|
if dist <= best_dist:
|
||||||
best_dist = dist
|
best_dist = dist
|
||||||
best = (tcx, tcy, ts)
|
best = (tcx, tcy, ts, tid)
|
||||||
return best
|
return best
|
||||||
|
|
||||||
|
|
||||||
|
def _default_cross_state():
|
||||||
|
"""Per-track line-crossing state for sequence counting.
|
||||||
|
|
||||||
|
seen_out / seen_in: ever registered a pass of that line (arm or count).
|
||||||
|
counted: already contributed one IN or OUT (at most one per physical object).
|
||||||
|
"""
|
||||||
|
return {"seen_out": False, "seen_in": False, "counted": False}
|
||||||
|
|
||||||
|
|
||||||
|
def _copy_cross_state(src):
|
||||||
|
return {
|
||||||
|
"seen_out": bool(src.get("seen_out", False)),
|
||||||
|
"seen_in": bool(src.get("seen_in", False)),
|
||||||
|
"counted": bool(src.get("counted", False)),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def now_str():
|
def now_str():
|
||||||
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
|
||||||
@@ -1176,8 +1194,9 @@ def run():
|
|||||||
min_hits=TRACK_MIN_HITS,
|
min_hits=TRACK_MIN_HITS,
|
||||||
)
|
)
|
||||||
|
|
||||||
object_line1_crossed = set()
|
# Per-track sequence state: OUT→IN / IN-only → IN; IN→OUT / OUT-only → OUT.
|
||||||
object_line2_crossed = set()
|
# Keys survive ID switches via inheritance (see _inherit_prev).
|
||||||
|
object_cross_state = {}
|
||||||
|
|
||||||
recent_cross_in = deque()
|
recent_cross_in = deque()
|
||||||
recent_cross_out = deque()
|
recent_cross_out = deque()
|
||||||
@@ -1226,7 +1245,9 @@ def run():
|
|||||||
line_y1 = resolve_line_y1(h)
|
line_y1 = resolve_line_y1(h)
|
||||||
line_y2 = resolve_line_y2(h)
|
line_y2 = resolve_line_y2(h)
|
||||||
print(
|
print(
|
||||||
f"RKNN+ByteTrack counter | {w}x{h} @ {fps}fps | line1 y={line_y1} (in v) line2 y={line_y2} (out ^)"
|
f"RKNN+ByteTrack counter | {w}x{h} @ {fps}fps | "
|
||||||
|
f"line1 y={line_y1} (IN ↓) line2 y={line_y2} (OUT ↑) | "
|
||||||
|
f"seq: OUT→IN/IN-only→in, IN→OUT/OUT-only→out"
|
||||||
)
|
)
|
||||||
print(f"Model: {MODEL_PATH} | imgsz={IMGSZ} | core_mask={CORE_MASK}")
|
print(f"Model: {MODEL_PATH} | imgsz={IMGSZ} | core_mask={CORE_MASK}")
|
||||||
print(
|
print(
|
||||||
@@ -1336,16 +1357,24 @@ def run():
|
|||||||
if object_det_to_track
|
if object_det_to_track
|
||||||
else ""
|
else ""
|
||||||
)
|
)
|
||||||
crossing_str1 = (
|
crossing_str1 = ""
|
||||||
f" line1_crossed: {sorted(object_line1_crossed)}"
|
crossing_str2 = ""
|
||||||
if object_line1_crossed
|
if object_cross_state:
|
||||||
else ""
|
seen_in_ids = sorted(
|
||||||
)
|
tid for tid, st in object_cross_state.items() if st.get("seen_in")
|
||||||
crossing_str2 = (
|
)
|
||||||
f" line2_crossed: {sorted(object_line2_crossed)}"
|
seen_out_ids = sorted(
|
||||||
if object_line2_crossed
|
tid for tid, st in object_cross_state.items() if st.get("seen_out")
|
||||||
else ""
|
)
|
||||||
)
|
counted_ids = sorted(
|
||||||
|
tid for tid, st in object_cross_state.items() if st.get("counted")
|
||||||
|
)
|
||||||
|
if seen_in_ids:
|
||||||
|
crossing_str1 = f" seen_in: {seen_in_ids}"
|
||||||
|
if seen_out_ids:
|
||||||
|
crossing_str2 = f" seen_out: {seen_out_ids}"
|
||||||
|
if counted_ids:
|
||||||
|
crossing_str2 += f" counted: {counted_ids}"
|
||||||
print(
|
print(
|
||||||
f"[DEBUG F{frame_idx}] dets={len(object_boxes_xyxy)} "
|
f"[DEBUG F{frame_idx}] dets={len(object_boxes_xyxy)} "
|
||||||
f"tracks={len(object_track_map)} "
|
f"tracks={len(object_track_map)} "
|
||||||
@@ -1367,36 +1396,92 @@ def run():
|
|||||||
detect_snapshot_ids.add(tid)
|
detect_snapshot_ids.add(tid)
|
||||||
detect_events_frame.append(tid)
|
detect_events_frame.append(tid)
|
||||||
|
|
||||||
|
inherited_this_frame = False
|
||||||
if tid not in object_tracked:
|
if tid not in object_tracked:
|
||||||
inherited = _inherit_prev(
|
inherited = _inherit_prev(
|
||||||
object_tracked, tid, cx, cy, mono, INHERIT_SEC, INHERIT_PX
|
object_tracked, tid, cx, cy, mono, INHERIT_SEC, INHERIT_PX
|
||||||
)
|
)
|
||||||
if inherited is not None:
|
if inherited is not None:
|
||||||
object_tracked[tid] = inherited
|
object_tracked[tid] = inherited[:3]
|
||||||
|
src_tid = inherited[3]
|
||||||
|
if src_tid in object_cross_state:
|
||||||
|
object_cross_state[tid] = _copy_cross_state(
|
||||||
|
object_cross_state[src_tid]
|
||||||
|
)
|
||||||
|
inherited_this_frame = True
|
||||||
if os.getenv("DEBUG_TRACKING", "").lower() == "true":
|
if os.getenv("DEBUG_TRACKING", "").lower() == "true":
|
||||||
|
src_st = object_cross_state.get(tid, {})
|
||||||
print(
|
print(
|
||||||
f"[DEBUG F{frame_idx}] INHERIT prev for new tid={tid} "
|
f"[DEBUG F{frame_idx}] INHERIT prev for new tid={tid} "
|
||||||
f"from ({inherited[0]:.1f},{inherited[1]:.1f})"
|
f"from tid={src_tid} ({inherited[0]:.1f},{inherited[1]:.1f})"
|
||||||
|
f" seen_in={src_st.get('seen_in', False)}"
|
||||||
|
f" seen_out={src_st.get('seen_out', False)}"
|
||||||
|
f" counted={src_st.get('counted', False)}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if tid not in object_cross_state:
|
||||||
|
object_cross_state[tid] = _default_cross_state()
|
||||||
|
st = object_cross_state[tid]
|
||||||
|
|
||||||
if tid in object_tracked:
|
if tid in object_tracked:
|
||||||
prev_cy = object_tracked[tid][1]
|
prev_cy = object_tracked[tid][1]
|
||||||
crossed1 = (
|
# Line 1 = IN, line 2 = OUT.
|
||||||
crossed_top_down(prev_cy, cy, line_y1)
|
# Arm (no count): OUT top→down, IN bottom→up.
|
||||||
and tid not in object_line1_crossed
|
# Count: IN top→down → IN (OUT→IN or IN-only)
|
||||||
|
# OUT bottom→up → OUT (IN→OUT or OUT-only)
|
||||||
|
out_down = (
|
||||||
|
crossed_top_down(prev_cy, cy, line_y2) and not st["seen_out"]
|
||||||
)
|
)
|
||||||
crossed2 = (
|
in_up = (
|
||||||
crossed_bottom_up(prev_cy, cy, line_y2)
|
crossed_bottom_up(prev_cy, cy, line_y1) and not st["seen_in"]
|
||||||
and tid not in object_line2_crossed
|
|
||||||
)
|
)
|
||||||
if crossed1 or crossed2:
|
in_down = (
|
||||||
direction = "in" if crossed1 else "out"
|
crossed_top_down(prev_cy, cy, line_y1) and not st["seen_in"]
|
||||||
recent = recent_cross_in if crossed1 else recent_cross_out
|
)
|
||||||
|
out_up = (
|
||||||
|
crossed_bottom_up(prev_cy, cy, line_y2) and not st["seen_out"]
|
||||||
|
)
|
||||||
|
|
||||||
|
if out_down or in_up or in_down or out_up:
|
||||||
|
if out_down:
|
||||||
|
st["seen_out"] = True
|
||||||
|
if in_up:
|
||||||
|
st["seen_in"] = True
|
||||||
|
if in_down:
|
||||||
|
st["seen_in"] = True
|
||||||
|
if out_up:
|
||||||
|
st["seen_out"] = True
|
||||||
|
|
||||||
|
# Prefer decisive motion if both count triggers fire in one jump.
|
||||||
|
count_in = in_down and not st["counted"]
|
||||||
|
count_out = out_up and not st["counted"]
|
||||||
|
if count_in and count_out:
|
||||||
|
if cy >= prev_cy:
|
||||||
|
count_out = False
|
||||||
|
else:
|
||||||
|
count_in = False
|
||||||
|
direction = "in" if count_in else ("out" if count_out else None)
|
||||||
|
|
||||||
|
if direction is None:
|
||||||
|
# Arm-only pass (OUT↓ or IN↑). No count yet.
|
||||||
|
if os.getenv("DEBUG_TRACKING", "").lower() == "true":
|
||||||
|
arm = []
|
||||||
|
if out_down:
|
||||||
|
arm.append("out_down")
|
||||||
|
if in_up:
|
||||||
|
arm.append("in_up")
|
||||||
|
print(
|
||||||
|
f"[DEBUG F{frame_idx}] CROSS ARM: tid={tid} "
|
||||||
|
f"prev_cy={prev_cy:.1f} -> cy={cy:.1f} "
|
||||||
|
f"arm={'+'.join(arm)} "
|
||||||
|
f"seen_in={st['seen_in']} seen_out={st['seen_out']}"
|
||||||
|
)
|
||||||
|
object_tracked[tid] = (cx, cy, mono)
|
||||||
|
continue
|
||||||
|
|
||||||
|
recent = recent_cross_in if direction == "in" else recent_cross_out
|
||||||
if is_duplicate_cross(recent, cx, frame_idx):
|
if is_duplicate_cross(recent, cx, frame_idx):
|
||||||
if crossed1:
|
st["counted"] = True
|
||||||
object_line1_crossed.add(tid)
|
|
||||||
if crossed2:
|
|
||||||
object_line2_crossed.add(tid)
|
|
||||||
if os.getenv("DEBUG_TRACKING", "").lower() == "true":
|
if os.getenv("DEBUG_TRACKING", "").lower() == "true":
|
||||||
print(
|
print(
|
||||||
f"[DEBUG F{frame_idx}] DUP CROSS IGNORED: tid={tid} "
|
f"[DEBUG F{frame_idx}] DUP CROSS IGNORED: tid={tid} "
|
||||||
@@ -1404,20 +1489,34 @@ def run():
|
|||||||
)
|
)
|
||||||
object_tracked[tid] = (cx, cy, mono)
|
object_tracked[tid] = (cx, cy, mono)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
seq = (
|
||||||
|
"out→in"
|
||||||
|
if direction == "in" and st["seen_out"]
|
||||||
|
else (
|
||||||
|
"in→out"
|
||||||
|
if direction == "out" and st["seen_in"]
|
||||||
|
else ("in-only" if direction == "in" else "out-only")
|
||||||
|
)
|
||||||
|
)
|
||||||
if os.getenv("DEBUG_TRACKING", "").lower() == "true":
|
if os.getenv("DEBUG_TRACKING", "").lower() == "true":
|
||||||
line_label = "1" if crossed1 else "2"
|
|
||||||
line_y_val = line_y1 if crossed1 else line_y2
|
|
||||||
print(
|
print(
|
||||||
f"[DEBUG F{frame_idx}] CROSS DETECTED: tid={tid} "
|
f"[DEBUG F{frame_idx}] CROSS DETECTED: tid={tid} "
|
||||||
f"prev_cy={prev_cy:.1f} -> cy={cy:.1f} line={line_label} y={line_y_val} dir={direction}"
|
f"prev_cy={prev_cy:.1f} -> cy={cy:.1f} dir={direction} "
|
||||||
|
f"seq={seq}"
|
||||||
|
+ (
|
||||||
|
" (inherit)"
|
||||||
|
if inherited_this_frame
|
||||||
|
else ""
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
recent.append((frame_idx, cx))
|
recent.append((frame_idx, cx))
|
||||||
if crossed1:
|
st["counted"] = True
|
||||||
object_line1_crossed.add(tid)
|
if direction == "in":
|
||||||
counter_in += 1
|
counter_in += 1
|
||||||
count_in_pulse = COUNT_PULSE_FRAMES
|
count_in_pulse = COUNT_PULSE_FRAMES
|
||||||
if crossed2:
|
else:
|
||||||
object_line2_crossed.add(tid)
|
|
||||||
counter_out += 1
|
counter_out += 1
|
||||||
count_out_pulse = COUNT_PULSE_FRAMES
|
count_out_pulse = COUNT_PULSE_FRAMES
|
||||||
store.record_object_crossing(tid, direction)
|
store.record_object_crossing(tid, direction)
|
||||||
@@ -1440,7 +1539,7 @@ def run():
|
|||||||
"x": int(cx) - 12,
|
"x": int(cx) - 12,
|
||||||
"y": int(cy),
|
"y": int(cy),
|
||||||
"born": frame_idx,
|
"born": frame_idx,
|
||||||
"text": f"+1" if not (crossed1 and crossed2) else "x2",
|
"text": "+1",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
object_tracked[tid] = (cx, cy, mono)
|
object_tracked[tid] = (cx, cy, mono)
|
||||||
@@ -1593,4 +1692,4 @@ object_tracked = {}
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
run()
|
run()
|
||||||
+6
-4
@@ -68,9 +68,11 @@ CLASS_OBJECT=karung
|
|||||||
OBJECT_CLASS_ID=0
|
OBJECT_CLASS_ID=0
|
||||||
|
|
||||||
# --- Line crossing ---
|
# --- Line crossing ---
|
||||||
# Two horizontal counting lines:
|
# Two horizontal counting lines with sequence logic:
|
||||||
# Line 1 (default ~33%): counts top-to-down (IN)
|
# Line 1 (IN): top→down counts IN (OUT→IN sequence, or IN-only)
|
||||||
# Line 2 (default ~66%): counts bottom-to-up (OUT)
|
# Line 2 (OUT): bottom→up counts OUT (IN→OUT sequence, or OUT-only)
|
||||||
|
# Passing the opposite line first only arms; the destination line still counts
|
||||||
|
# even if the opposite line was never crossed.
|
||||||
# Fixed y-coordinate for line 1/IN (overrides LINE_Y1_FRAC if set)
|
# Fixed y-coordinate for line 1/IN (overrides LINE_Y1_FRAC if set)
|
||||||
LINE_Y1=
|
LINE_Y1=
|
||||||
# Fraction of frame height for line 1 (default 0.33)
|
# Fraction of frame height for line 1 (default 0.33)
|
||||||
@@ -147,4 +149,4 @@ CROSS_SNAPSHOT_DIR=/opt/zenai-kpc-snaps/snapshots
|
|||||||
CROSS_SNAPSHOT_CLEANUP_SEC=3600
|
CROSS_SNAPSHOT_CLEANUP_SEC=3600
|
||||||
CROSS_SNAPSHOT_MAX_AGE_DAYS=3
|
CROSS_SNAPSHOT_MAX_AGE_DAYS=3
|
||||||
|
|
||||||
DEDUP_PX=-1
|
DEDUP_PX=-1
|
||||||
@@ -1217,4 +1217,4 @@ function hideVideoError() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=ZenAI KPC Edge Counter (RTSP + RKNN)
|
||||||
|
Documentation=file:///opt/zenai-kpc-python/DEPLOY.md
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=root
|
||||||
|
Group=root
|
||||||
|
|
||||||
|
WorkingDirectory=/opt/zenai-kpc-python
|
||||||
|
EnvironmentFile=/opt/zenai-kpc-python/.env
|
||||||
|
Environment=PYTHONNOUSERSITE=1
|
||||||
|
Environment=PATH=/opt/zenai-kpc-python/venv/bin:/usr/local/bin:/usr/bin:/bin
|
||||||
|
|
||||||
|
ExecStart=/opt/zenai-kpc-python/venv/bin/python counter_live_rknn.py
|
||||||
|
|
||||||
|
TimeoutStopSec=30
|
||||||
|
KillSignal=SIGTERM
|
||||||
|
|
||||||
|
Restart=always
|
||||||
|
RestartSec=10
|
||||||
|
StartLimitInterval=120s
|
||||||
|
StartLimitBurst=5
|
||||||
|
|
||||||
|
NoNewPrivileges=true
|
||||||
|
ProtectHome=true
|
||||||
|
PrivateTmp=false
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=ZenAI KPC Dashboard (Flask)
|
||||||
|
Documentation=file:///opt/zenai-kpc-python/DEPLOY.md
|
||||||
|
After=network-online.target zenai-kpc-counter.service
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=root
|
||||||
|
Group=root
|
||||||
|
|
||||||
|
WorkingDirectory=/opt/zenai-kpc-python
|
||||||
|
EnvironmentFile=/opt/zenai-kpc-python/.env
|
||||||
|
Environment=PATH=/opt/zenai-kpc-python/venv/bin:/usr/local/bin:/usr/bin:/bin
|
||||||
|
Environment=FLASK_DEBUG=false
|
||||||
|
|
||||||
|
ExecStart=/opt/zenai-kpc-python/venv/bin/python counter_dashboard.py
|
||||||
|
|
||||||
|
TimeoutStopSec=15
|
||||||
|
KillSignal=SIGTERM
|
||||||
|
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
StartLimitInterval=60s
|
||||||
|
StartLimitBurst=3
|
||||||
|
|
||||||
|
NoNewPrivileges=true
|
||||||
|
ProtectHome=true
|
||||||
|
PrivateTmp=false
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
Reference in new issue
Block a user