Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
657f4f80d9 |
No files matched your search
@@ -1,241 +0,0 @@
|
||||
# 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.
|
||||
+4
-6
@@ -92,11 +92,9 @@ CLASS_OBJECT=object
|
||||
OBJECT_CLASS_ID=0
|
||||
|
||||
# --- Line crossing ---
|
||||
# Two horizontal counting lines with sequence logic:
|
||||
# Line 1 (IN): top→down counts IN (OUT→IN sequence, or IN-only)
|
||||
# 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.
|
||||
# Two horizontal counting lines:
|
||||
# Line 1 (default ~33%): counts top-to-down (IN)
|
||||
# Line 2 (default ~66%): counts bottom-to-up (OUT)
|
||||
# Fixed y-coordinate for line 1/IN (overrides LINE_Y1_FRAC if set)
|
||||
LINE_Y1=
|
||||
# Fraction of frame height for line 1 (default 0.33)
|
||||
@@ -225,4 +223,4 @@ DASHBOARD_PORT=5000
|
||||
# Enable Flask debug mode (true/false) — auto-reloads on code changes; disable in production
|
||||
FLASK_DEBUG=false
|
||||
# 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"DB: {DB_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)
|
||||
+65
-139
@@ -441,6 +441,7 @@ class ByteTracker:
|
||||
cost_mat = 1.0 - iou_mat
|
||||
matches = _greedy_match(cost_mat, threshold=1.0 - self.match_thresh)
|
||||
|
||||
matched_det_idx = set()
|
||||
for di, ti in matches:
|
||||
det_global = int(high_idx[di])
|
||||
orig_idx = int(remain_orig_idx[det_global])
|
||||
@@ -450,6 +451,30 @@ class ByteTracker:
|
||||
det_to_track[orig_idx] = track_pool[ti].track_id
|
||||
tracked_map[track_pool[ti].track_id] = (track_pool[ti].get_cx(), track_pool[ti].get_cy())
|
||||
match_pairs_high.append((det_global, ti))
|
||||
matched_det_idx.add(di)
|
||||
|
||||
# --- second pass: any unmatched high-score detection tries to
|
||||
# pair with ANY track that overlaps above threshold, even one
|
||||
# that already has a match. This prevents a close/overlapping
|
||||
# object from spawning a spurious new ID when the greedy 1:1
|
||||
# match paired its detection elsewhere. ---
|
||||
for di in range(len(high_dets)):
|
||||
if di in matched_det_idx:
|
||||
continue
|
||||
best_ti = None
|
||||
best_iou = 0.0
|
||||
for ti in range(num_tracks):
|
||||
if iou_mat[di, ti] > best_iou and iou_mat[di, ti] >= self.match_thresh:
|
||||
best_iou = iou_mat[di, ti]
|
||||
best_ti = ti
|
||||
if best_ti is not None:
|
||||
det_global = int(high_idx[di])
|
||||
orig_idx = int(remain_orig_idx[det_global])
|
||||
track_pool[best_ti].update(dets[det_global])
|
||||
track_pool[best_ti].hit_streak = max(1, track_pool[best_ti].hit_streak)
|
||||
matched_track_idx.add(best_ti)
|
||||
det_to_track[orig_idx] = track_pool[best_ti].track_id
|
||||
tracked_map[track_pool[best_ti].track_id] = (track_pool[best_ti].get_cx(), track_pool[best_ti].get_cy())
|
||||
|
||||
unmatched_tracks = [
|
||||
t for t in range(num_tracks) if t not in matched_track_idx
|
||||
@@ -694,9 +719,8 @@ def is_duplicate_cross(recent, cx, frame_idx):
|
||||
|
||||
|
||||
def _inherit_prev(tracked, new_tid, cx, cy, mono, max_age, max_px):
|
||||
"""Find a recently-seen nearby track for ID-switch continuation.
|
||||
|
||||
Returns (cx, cy, mono, source_tid) or None.
|
||||
"""Find a recently-seen nearby track and return its (cx, cy, mono) so a new
|
||||
track ID can inherit the trajectory across an ID switch. Returns None if none.
|
||||
|
||||
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
|
||||
@@ -713,27 +737,10 @@ def _inherit_prev(tracked, new_tid, cx, cy, mono, max_age, max_px):
|
||||
dist = abs(tcx - cx)
|
||||
if dist <= best_dist:
|
||||
best_dist = dist
|
||||
best = (tcx, tcy, ts, tid)
|
||||
best = (tcx, tcy, ts)
|
||||
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():
|
||||
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
@@ -1194,9 +1201,8 @@ def run():
|
||||
min_hits=TRACK_MIN_HITS,
|
||||
)
|
||||
|
||||
# Per-track sequence state: OUT→IN / IN-only → IN; IN→OUT / OUT-only → OUT.
|
||||
# Keys survive ID switches via inheritance (see _inherit_prev).
|
||||
object_cross_state = {}
|
||||
object_line1_crossed = set()
|
||||
object_line2_crossed = set()
|
||||
|
||||
recent_cross_in = deque()
|
||||
recent_cross_out = deque()
|
||||
@@ -1245,9 +1251,7 @@ def run():
|
||||
line_y1 = resolve_line_y1(h)
|
||||
line_y2 = resolve_line_y2(h)
|
||||
print(
|
||||
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"
|
||||
f"RKNN+ByteTrack counter | {w}x{h} @ {fps}fps | line1 y={line_y1} (in v) line2 y={line_y2} (out ^)"
|
||||
)
|
||||
print(f"Model: {MODEL_PATH} | imgsz={IMGSZ} | core_mask={CORE_MASK}")
|
||||
print(
|
||||
@@ -1357,24 +1361,16 @@ def run():
|
||||
if object_det_to_track
|
||||
else ""
|
||||
)
|
||||
crossing_str1 = ""
|
||||
crossing_str2 = ""
|
||||
if object_cross_state:
|
||||
seen_in_ids = sorted(
|
||||
tid for tid, st in object_cross_state.items() if st.get("seen_in")
|
||||
)
|
||||
seen_out_ids = sorted(
|
||||
tid for tid, st in object_cross_state.items() if st.get("seen_out")
|
||||
)
|
||||
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}"
|
||||
crossing_str1 = (
|
||||
f" line1_crossed: {sorted(object_line1_crossed)}"
|
||||
if object_line1_crossed
|
||||
else ""
|
||||
)
|
||||
crossing_str2 = (
|
||||
f" line2_crossed: {sorted(object_line2_crossed)}"
|
||||
if object_line2_crossed
|
||||
else ""
|
||||
)
|
||||
print(
|
||||
f"[DEBUG F{frame_idx}] dets={len(object_boxes_xyxy)} "
|
||||
f"tracks={len(object_track_map)} "
|
||||
@@ -1396,92 +1392,36 @@ def run():
|
||||
detect_snapshot_ids.add(tid)
|
||||
detect_events_frame.append(tid)
|
||||
|
||||
inherited_this_frame = False
|
||||
if tid not in object_tracked:
|
||||
inherited = _inherit_prev(
|
||||
object_tracked, tid, cx, cy, mono, INHERIT_SEC, INHERIT_PX
|
||||
)
|
||||
if inherited is not None:
|
||||
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
|
||||
object_tracked[tid] = inherited
|
||||
if os.getenv("DEBUG_TRACKING", "").lower() == "true":
|
||||
src_st = object_cross_state.get(tid, {})
|
||||
print(
|
||||
f"[DEBUG F{frame_idx}] INHERIT prev for new tid={tid} "
|
||||
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)}"
|
||||
f"from ({inherited[0]:.1f},{inherited[1]:.1f})"
|
||||
)
|
||||
|
||||
if tid not in object_cross_state:
|
||||
object_cross_state[tid] = _default_cross_state()
|
||||
st = object_cross_state[tid]
|
||||
|
||||
if tid in object_tracked:
|
||||
prev_cy = object_tracked[tid][1]
|
||||
# Line 1 = IN, line 2 = OUT.
|
||||
# Arm (no count): OUT top→down, IN bottom→up.
|
||||
# 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"]
|
||||
crossed1 = (
|
||||
crossed_top_down(prev_cy, cy, line_y1)
|
||||
and tid not in object_line1_crossed
|
||||
)
|
||||
in_up = (
|
||||
crossed_bottom_up(prev_cy, cy, line_y1) and not st["seen_in"]
|
||||
crossed2 = (
|
||||
crossed_bottom_up(prev_cy, cy, line_y2)
|
||||
and tid not in object_line2_crossed
|
||||
)
|
||||
in_down = (
|
||||
crossed_top_down(prev_cy, cy, line_y1) and not st["seen_in"]
|
||||
)
|
||||
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 crossed1 or crossed2:
|
||||
direction = "in" if crossed1 else "out"
|
||||
recent = recent_cross_in if crossed1 else recent_cross_out
|
||||
if is_duplicate_cross(recent, cx, frame_idx):
|
||||
st["counted"] = True
|
||||
if crossed1:
|
||||
object_line1_crossed.add(tid)
|
||||
if crossed2:
|
||||
object_line2_crossed.add(tid)
|
||||
if os.getenv("DEBUG_TRACKING", "").lower() == "true":
|
||||
print(
|
||||
f"[DEBUG F{frame_idx}] DUP CROSS IGNORED: tid={tid} "
|
||||
@@ -1489,34 +1429,20 @@ def run():
|
||||
)
|
||||
object_tracked[tid] = (cx, cy, mono)
|
||||
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":
|
||||
line_label = "1" if crossed1 else "2"
|
||||
line_y_val = line_y1 if crossed1 else line_y2
|
||||
print(
|
||||
f"[DEBUG F{frame_idx}] CROSS DETECTED: tid={tid} "
|
||||
f"prev_cy={prev_cy:.1f} -> cy={cy:.1f} dir={direction} "
|
||||
f"seq={seq}"
|
||||
+ (
|
||||
" (inherit)"
|
||||
if inherited_this_frame
|
||||
else ""
|
||||
)
|
||||
f"prev_cy={prev_cy:.1f} -> cy={cy:.1f} line={line_label} y={line_y_val} dir={direction}"
|
||||
)
|
||||
|
||||
recent.append((frame_idx, cx))
|
||||
st["counted"] = True
|
||||
if direction == "in":
|
||||
if crossed1:
|
||||
object_line1_crossed.add(tid)
|
||||
counter_in += 1
|
||||
count_in_pulse = COUNT_PULSE_FRAMES
|
||||
else:
|
||||
if crossed2:
|
||||
object_line2_crossed.add(tid)
|
||||
counter_out += 1
|
||||
count_out_pulse = COUNT_PULSE_FRAMES
|
||||
store.record_object_crossing(tid, direction)
|
||||
@@ -1539,7 +1465,7 @@ def run():
|
||||
"x": int(cx) - 12,
|
||||
"y": int(cy),
|
||||
"born": frame_idx,
|
||||
"text": "+1",
|
||||
"text": f"+1" if not (crossed1 and crossed2) else "x2",
|
||||
}
|
||||
)
|
||||
object_tracked[tid] = (cx, cy, mono)
|
||||
@@ -1692,4 +1618,4 @@ object_tracked = {}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run()
|
||||
run()
|
||||
@@ -0,0 +1 @@
|
||||
Jul 14 22:15:51 frigate-karung-masuk-tuang-dev python[2390681]: , 5806, 5808, 5809, 5810, 5811, 5812, 5813, 5814, 5815, 5816, 5817, 5818, 5819, 5820, 5821, 5822, 5824, 5825, 5826, 5827, 5828, 5829, 5830, 5831, 5832, 5833, 5834, 5835, 5837, 5838, 5839, 5840, 5843, 5844, 5845, 5846, 5847, 5848, 5849, 5854, 5855, 5856, 5857, 5858, 5860, 5861, 5862, 5863, 5864, 5865, 5866, 5867, 5868, 5870, 5871, 5872, 5873, 5874, 5876, 5877, 5878, 5879, 5880, 5881, 5882, 5883, 5884, 5885, 5886, 5887, 5888, 5889, 5892, 5893, 5894, 5895, 5896, 5897, 5899, 5900, 5901, 5902, 5903, 5904, 5907, 5908, 5909, 5910, 5911, 5912, 5913, 5914, 5915, 5916, 5917, 5918, 5919, 5920, 5921, 5922, 5923, 5925, 5926, 5927, 5928, 5929, 5930, 5931, 5932, 5935, 5936, 5937, 5938, 5939, 5942, 5943, 5944, 5945, 5946, 5947, 5948, 5949, 5950, 5951, 5953, 5954, 5955, 5956, 5957, 5958, 5959, 5961, 5962, 5963, 5964, 5965, 5966, 5967, 5968, 5969, 5970, 5971, 5972, 5974, 5975, 5976, 5977, 5978, 5979, 5980, 5982, 5983, 5985, 5986, 5987, 5988, 5989, 5990, 5991, 5992, 5993, 5995, 5996, 5997, 5998, 5999, 6000, 6001, 6002, 6003, 6004, 6005, 6007, 6008, 6009, 6010, 6011, 6012, 6013, 6014, 6015, 6020, 6021, 6022, 6023, 6024, 6025, 6026, 6030, 6031, 6032, 6033, 6034, 6035, 6036, 6037, 6038, 6042, 6043, 6044, 6045, 6046, 6047, 6048, 6050, 6051, 6052, 6056, 6057, 6058, 6059, 6060, 6061, 6062, 6063, 6064, 6065, 6068, 6069, 6070, 6071, 6072, 6073, 6074, 6075, 6076, 6080, 6081, 6082, 6083, 6084, 6085, 6087, 6088, 6089, 6090, 6091, 6092, 6093, 6094, 6095, 6096, 6097, 6098, 6099, 6100, 6102, 6103, 6104, 6105, 6106, 6107, 6108, 6115, 6116, 6117, 6118, 6119, 6120, 6121, 6122, 6123, 6124, 6131, 6132, 6133, 6134, 6135, 6136, 6137, 6138, 6139, 6140, 6141, 6142, 6143, 6144, 6145, 6146, 6147, 6148, 6149, 6150, 6151, 6152, 6154, 6155, 6156, 6157, 6158, 6159, 6160, 6161, 6162, 6163, 6164, 6165, 6166, 6168, 6169, 6170, 6171, 6172, 6173, 6175, 6176, 6177, 6178, 6179, 6180, 6181, 6183, 6184, 6185, 6186, 6190, 6191, 6192, 6193, 6194, 6195, 6196, 6197, 6198, 6199, 6200, 6201, 6202, 6203, 6204, 6205, 6206, 6207, 6208, 6209, 6210, 6212, 6213, 6214, 6215, 6216, 6217, 6218, 6219, 6220, 6227, 6228, 6229, 6230, 6231, 6232, 6233, 6234, 6235, 6236, 6237, 6239, 6240, 6241, 6242, 6243, 6244, 6245, 6246, 6247, 6250, 6251, 6252, 6253, 6254, 6255, 6256, 6257, 6258, 6259, 6260, 6261, 6267, 6268, 6269, 6270, 6271, 6272, 6273, 6274, 6275, 6276, 6277, 6278, 6280, 6281, 6282, 6283, 6284, 6285, 6286, 6287, 6288, 6290, 6291, 6292, 6293, 6294, 6295, 6296, 6297, 6298, 6303, 6304, 6305, 6306, 6307, 6308, 6309, 6310, 6311, 6312, 6313, 6314, 6315, 6316, 6317, 6318, 6319, 6320, 6321, 6322, 6323, 6324, 6325, 6327, 6328, 6329, 6330, 6331, 6332, 6333, 6334, 6335, 6336, 6338, 6339, 6340, 6341, 6342, 6344, 6345, 6346, 6347, 6348, 6349, 6350, 6351, 6352, 6353, 6355, 6356, 6357, 6358, 6359, 6360, 6361, 6366, 6367, 6368, 6369, 6370, 6371, 6372, 6373, 6374, 6375, 6376, 6377, 6378, 6379, 6380, 6381, 6382, 6383, 6384, 6385, 6386, 6387, 6388, 6389, 6390, 6391, 6392, 6393, 6394, 6395, 6396, 6397, 6398, 6401, 6402, 6403, 6404, 6405, 6406, 6407, 6408, 6409, 6410, 6411, 6412, 6415, 6416, 6417, 6418, 6419, 6420, 6421, 6422, 6428, 6429, 6430, 6431, 6440, 6441, 6442, 6443, 6444, 6445, 6446, 6447, 6450, 6451, 6452, 6453, 6454, 6455, 6456, 6457, 6458, 6459, 6460, 6461, 6464, 6465, 6466, 6467, 6468, 6469, 6470, 6471, 6472, 6475, 6476, 6477, 6478, 6479, 6480, 6481, 6482, 6483, 6484, 6485, 6486, 6487, 6488, 6489, 6490, 6491, 6492, 6493, 6494, 6495, 6496, 6498, 6499, 6500, 6502, 6503, 6504, 6505, 6506, 6507, 6508, 6509, 6510, 6511, 6512, 6513, 6514, 6515, 6516, 6517, 6518, 6519, 6520, 6521, 6522, 6523, 6528, 6529, 6530, 6531, 6532, 6533, 6534, 6535, 6538, 6539, 6540, 6541, 6542, 6546, 6547, 6548, 6549, 6550, 6551, 6552, 6553, 6554, 6555, 6556, 6557, 6558, 6559, 6560, 6561, 6562, 6563, 6564, 6565, 6566, 6567, 6568, 6569, 6570, 6571, 6572, 6573, 6574, 6575, 6576, 6577, 6580, 6581, 6582, 6583, 6584, 6585, 6586, 6587, 6588, 6589, 6590, 6591, 6592, 6593, 6594, 6595, 6596, 6597, 6598, 6599, 6600, 6601, 6602, 6603, 6605, 6606, 6607, 6608, 6609, 6610, 6611, 6614, 6615, 6616, 6617, 6619, 6620, 6621, 6622, 6623, 6624, 6625, 6626, 6627, 6628, 6630, 6631, 6632, 6633, 6634, 6636, 6642, 6643, 6644, 6645, 6646, 6647, 6648, 6649, 6650, 6652, 6653, 6654, 6655, 6656, 6657, 6658, 6659, 6660, 6661, 6662, 6663, 6664, 6668, 6669, 6670, 6671, 6672, 6673, 6674, 6676, 6677, 6678, 6679, 6680, 6681, 6682, 6683, 6684, 6685, 6686, 6687, 6688, 6694, 6695, 6696, 6697, 6698, 6699, 6700, 6701, 6702, 6703, 6704, 6705, 6706, 6707, 6708, 6709, 6710, 6711, 6712, 6713, 6714, 6722, 6723, 6724, 6725, 6726, 6727, 6728, 6729, 6730, 6731, 6732, 6733, 6738, 6739, 6740, 6741, 6742, 6743, 6744, 6745, 6746, 6747, 6748, 6749, 6750, 6754, 6755, 6756, 6757, 6758, 6759, 6760, 6761, 6762, 6763, 6764, 6765, 6766, 6767, 6768, 6769, 6772, 6773, 6774, 6777, 6778, 6779, 6780, 6781, 6782, 6788, 6789, 6790, 6791, 6792, 6793, 6794, 6795, 6797, 6798, 6799, 6800, 6801, 6802, 6803, 6804, 6805, 6806, 6Line truncated
|
||||
+4
-6
@@ -68,11 +68,9 @@ CLASS_OBJECT=karung
|
||||
OBJECT_CLASS_ID=0
|
||||
|
||||
# --- Line crossing ---
|
||||
# Two horizontal counting lines with sequence logic:
|
||||
# Line 1 (IN): top→down counts IN (OUT→IN sequence, or IN-only)
|
||||
# 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.
|
||||
# Two horizontal counting lines:
|
||||
# Line 1 (default ~33%): counts top-to-down (IN)
|
||||
# Line 2 (default ~66%): counts bottom-to-up (OUT)
|
||||
# Fixed y-coordinate for line 1/IN (overrides LINE_Y1_FRAC if set)
|
||||
LINE_Y1=
|
||||
# Fraction of frame height for line 1 (default 0.33)
|
||||
@@ -149,4 +147,4 @@ CROSS_SNAPSHOT_DIR=/opt/zenai-kpc-snaps/snapshots
|
||||
CROSS_SNAPSHOT_CLEANUP_SEC=3600
|
||||
CROSS_SNAPSHOT_MAX_AGE_DAYS=3
|
||||
|
||||
DEDUP_PX=-1
|
||||
DEDUP_PX=-1
|
||||
@@ -1217,4 +1217,4 @@ function hideVideoError() {
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
@@ -1,32 +0,0 @@
|
||||
[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
|
||||
@@ -1,32 +0,0 @@
|
||||
[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