195 lines
11 KiB
Markdown
195 lines
11 KiB
Markdown
# SPEC — bytetrack-counter
|
||
|
||
## §G — Goal
|
||
|
||
RTSP camera → YOLO RKNN (NPU) → ByteTrack → line-crossing counter → SQLite + Flask dashboard.
|
||
Count `ayam` (chicken) crossing counting line. Close batch on `talenan` (cutting-board) crossing.
|
||
Daily cutoff @ HH:MM resets batch numbering. RK3588 hardware.
|
||
**Reference implementation for C++ port in separate repo.**
|
||
|
||
## §C — Constraints
|
||
|
||
- Python 3.10, `rknn-toolkit-lite2` (NPU), `opencv-python` (RTSP/FFmpeg)
|
||
- `numpy<2` (rknn-toolkit-lite2 incompatible with numpy≥2)
|
||
- `PYTHONNOUSERSITE=1` ! set or venv breaks
|
||
- SQLite for persistence, JSON file for active-batch state
|
||
- Flask on port 5000 (dashboard) + 5002 (recounting) + 5003 (upload recounting), systemd supervision
|
||
- `counter_live.py` ⊥ run on RK3588 — Jetson TensorRT artifact, ! port target
|
||
- `counter_live_rknn_bytetrack.py` — reference for C++ port, this is what systemd runs
|
||
- Single deployment: `counter_live_rknn_bytetrack.py` + `counter_dashboard.py` + `recounting_dashboard.py` + `recounting_dashboard_upload.py`
|
||
- `go2rtc` ! running for recounting MP4 streaming (port 1984)
|
||
|
||
## §I — Interfaces
|
||
|
||
### Systemd
|
||
```
|
||
unit: bytetrack-counter.service → `venv/bin/python counter_live_rknn_bytetrack.py`
|
||
unit: bytetrack-counter-dashboard.service → `venv/bin/python counter_dashboard.py`
|
||
unit: bytetrack-recounting-dashboard.service → `venv/bin/python recounting_dashboard.py`
|
||
env: PYTHONNOUSERSITE=1 ! set in all units
|
||
env: EnvironmentFile=/opt/bytetrack-counter/.env
|
||
user: root (not jetson)
|
||
path: /opt/bytetrack-counter (fixed in service file)
|
||
```
|
||
|
||
### `.env` config (40+ vars, gitignored)
|
||
```
|
||
env: SOURCE ! set → RTSP URL | file path
|
||
env: MODEL_PATH ! set → .rknn file
|
||
env: IMGSZ ! set → model input size (e.g. 320)
|
||
env: CORE_MASK → NPU core mask (1=core0, 2=core1, 3=dual, 7=all)
|
||
env: NUM_CLASSES ! match model output count
|
||
env: CONF → detection confidence threshold (default 0.3)
|
||
env: DAILY_CUTOFF_TIME → HH:MM, default "20:00"
|
||
env: CROSS_DIRECTION → rtl | ltr | both (default rtl)
|
||
env: LINE_X | LINE_X_FRAC → counting line position
|
||
env: CLASS_AYAM → class name for counted object (index 0)
|
||
env: CLASS_TALENAN → class name for batch-close trigger (index 1)
|
||
env: RESET_COUNTERS_AT_CUTOFF → defined in example, ! consumed by code ? zombie
|
||
env: MOTION_DETECTION_ENABLED | MOTION_THRESHOLD → skip inference on still frames ?
|
||
env: TRACK_HIGH_THRESH_{0,1} | TRACK_LOW_THRESH_{0,1} | TRACK_MATCH_THRESH_{0,1} → ByteTrack params per class
|
||
env: BATCH_TIMEOUT_SECONDS → auto-close after inactivity (default 300)
|
||
env: IGNORE_BATCH_LABEL_TIMEOUT_SECONDS → suppress talenan close after batch start (default 30)
|
||
env: MIN_OBJECT_PER_BATCH → min count to persist batch (default 60)
|
||
env: MIN_DURATION_PER_BATCH → min seconds to persist batch (default 60)
|
||
env: LIVE_STREAM_ENABLED → write annotated JPEG snapshot each N frames
|
||
env: EXPORT_CSV → write per-crossing CSV (default true)
|
||
env: UPLOAD_DASHBOARD_URL → header link to upload recounting dashboard (default empty → hidden)
|
||
|
||
### Recounting dashboard config
|
||
```
|
||
env: RECOUNTING_DASHBOARD_PORT → port for recounting UI (default 5002)
|
||
env: LIVE_API_URL → base URL of live counter API (default http://localhost:5000)
|
||
env: RECOUNT_API_URL → base URL of recounting counter API (second node)
|
||
env: GO2RTC_API_URL → go2rtc REST API (default http://localhost:1984)
|
||
env: GO2RTC_STREAM_NAME → go2rtc stream name for recount preview (default "recount")
|
||
```
|
||
|
||
### Upload recounting dashboard config
|
||
```
|
||
env: RECOUNTING_UPLOAD_PORT → port for upload UI (default 5003)
|
||
env: UPLOAD_DIR → dir for uploaded MP4s (default <repo>/uploads)
|
||
env: RECOUNT_CMD → replay command template with {path} placeholder (default "bytetrack-counter config.env --source {path}")
|
||
env: RECOUNTING_UPLOAD_TEMPLATE → template (default recounting_upload_lamborghini.html)
|
||
env: LIVE_DASHBOARD_URL → header link to live counter dashboard (default empty → hidden)
|
||
```
|
||
```
|
||
|
||
### SQLite
|
||
```
|
||
table: batches (date, batch#, camera, label, count, start, end, created_at)
|
||
UNIQUE(counting_date, batch_number, camera_name, object_label)
|
||
table: daily_summaries (date, camera, label, total_count, total_batches, updated_at)
|
||
UNIQUE(counting_date, camera_name, object_label)
|
||
```
|
||
|
||
### JSON state file
|
||
```
|
||
path: /tmp/bytetrack_current_batch.json (default)
|
||
schema: {counting_date, batch_number, count, start_time, last_detection_time, counted_event_ids[]}
|
||
```
|
||
|
||
### Flask API
|
||
```
|
||
api: GET / → dashboard HTML
|
||
api: GET /api/current-batch → {count, batch_number, counting_date, start_time, last_detection_time}
|
||
api: GET /api/previous-batch → {date, batch#, count, start/end, duration_minutes}
|
||
api: GET /api/summary → {today, yesterday, all_time, average_per_day, best_day}
|
||
api: GET /api/daily-data?days=N → [ {date, total_count, total_batches, avg_per_batch} ]
|
||
api: GET /api/day-detail/<date> → {date, total_count, total_batches, total_duration, avg_duration, batches[]}
|
||
api: GET /api/recent-batches?limit=N → [ {date, batch#, count, start/end, duration} ]
|
||
api: GET /api/available-dates → [ {date, total_count, total_batches} ]
|
||
api: GET /api/export-daily-csv?days=N → .xlsx download (named csv, emits xlsx)
|
||
api: GET /api/export-day-csv/<date> → .xlsx download (named csv, emits xlsx)
|
||
api: POST /api/reset → delete STATE_FILE, touch {SHM_DIR}/.reset; returns 500 if SHM marker fails
|
||
api: GET /api/live-video → MJPEG stream from shared-memory JPEG
|
||
|
||
### Recounting dashboard
|
||
```
|
||
api: GET / → recounting HTML (template via DASHBOARD_TEMPLATE env var)
|
||
api: GET /api/live-progress → proxy to LIVE_API_URL:/api/current-batch
|
||
api: GET /api/recount-progress → proxy to RECOUNT_API_URL:/api/current-batch
|
||
api: GET /api/batch-result?file=<path> → parse batch num from filename, date from folder, query live API
|
||
api: GET /api/mp4-files → folders grouped by date, files sorted by batch #
|
||
api: GET /api/state → {streaming, file, file_name, stream_url, batch_number, batch_date}
|
||
api: GET /api/proxy-stream → proxy MJPEG from RECOUNT_API_URL:/api/live-video (same-origin, avoids ORB)
|
||
api: GET /api/download-mp4?file=<path> → download selected MP4
|
||
api: POST /api/start-recount {path} → reset recount counter + launch ffmpeg RTSP, return {stream_url}
|
||
api: POST /api/stop-recount → kill ffmpeg + clear current file
|
||
```
|
||
|
||
### Upload recounting dashboard
|
||
```
|
||
api: GET / → upload recounting HTML (template via RECOUNTING_UPLOAD_TEMPLATE env var)
|
||
api: POST /api/upload → multipart .mp4 (≤512MB), saved as original filename in UPLOAD_DIR; returns {name, path, size}
|
||
api: GET /api/uploads → list uploaded MP4s sorted by mtime desc → [ {name, path, size, mtime} ]
|
||
api: POST /api/delete-upload {path} → delete uploaded MP4 (path restricted to UPLOAD_DIR)
|
||
api: POST /api/start-recount {path} → reset recount API then launch RECOUNT_CMD (path shlex-quoted); START RECOUNT disabled until upload completes
|
||
api: POST /api/stop-recount → kill recount process + clear current file
|
||
api: GET /api/state → {streaming, file, file_name, stream_url, batch_number, batch_date}
|
||
api: GET /api/proxy-stream → proxy MJPEG from RECOUNT_API_URL:/api/live-video (same-origin)
|
||
api: GET /api/live-progress → proxy to LIVE_API_URL:/api/current-batch
|
||
api: GET /api/recount-progress → proxy to RECOUNT_API_URL:/api/current-batch
|
||
api: GET /api/batch-result?file=<path> → parse batch num + date (YYYY-MM-DD) from filename batch_XX_YYYYMMDD_HHmmSS.mp4, query live API /api/day-detail/<date>, fallback recent-batches
|
||
api: GET /api/download-mp4?file=<path> → download selected MP4
|
||
```
|
||
```
|
||
|
||
## §V — Invariants
|
||
|
||
```
|
||
V1: NUM_CLASSES must match model output → class 0=ayam, class 1=talenan
|
||
V2: line crossing → prev_cx > line_x ≥ cx (rtl) | prev_cx < line_x ≤ cx (ltr)
|
||
V3: ∀ track_id → counted at most once per batch (counted_event_ids set)
|
||
V4: batch persisted → count ≥ MIN_OBJECT_PER_BATCH & duration ≥ MIN_DURATION_PER_BATCH
|
||
V5: dt.time() < DAILY_CUTOFF_TIME → counting_date = today, else tomorrow
|
||
V6: talenan crossing → close batch, but ignored ∀ IGNORE_BATCH_LABEL_TIMEOUT_SEC after batch start
|
||
V7: batch inactivity ≥ BATCH_TIMEOUT_SECONDS → auto-close
|
||
V8: PYTHONNOUSERSITE=1 ! set for venv isolation
|
||
V9: .env ! exist before counter or dashboard starts
|
||
V10: DB tables ! exist on startup (created if absent, both store & dashboard)
|
||
V11: previous batch → last CARRY_IDS (default 50) track IDs carried forward to next batch
|
||
V12: ∃ ! batch per (date, batch#, camera, label) — UNIQUE constraint in DB
|
||
V13: counter & dashboard share DB path → no process-level coordination
|
||
V14: stream disconnect → reconnect with delay (RECONNECT_DELAY_SEC), ! block main loop
|
||
V15: model inference → letterbox-resize to IMGSZ×IMGSZ, BGR→RGB, run NPU
|
||
V16: NMS postprocessing → iou_thr=0.45, class-aware grouping, score > CONF
|
||
V17: tracks pruned after TRACKED_PRUNE_SEC (default 300s) without update
|
||
V18: .env missing → scripts fail at import (os.getenv falls back to defaults, may mismatch)
|
||
V19: stream reconnect → reset motion detection state (prev_gray = None)
|
||
V20: reset → delete STATE_FILE + touch {SHM_DIR}/.reset; batch_store._check_reset_signal() watches marker & clears in-memory state before next crossing
|
||
V21: upload filename batch_XX_YYYYMMDD_HHmmSS.mp4 → batch# + date parsed; batch-result queries /api/day-detail/<YYYY-MM-DD> on live API
|
||
V22: preview auto-on when recount running — enablePreview() on START RECOUNT + applyState streaming branch; manual toggle overrides; img.onerror retries cache-busted src while streaming
|
||
```
|
||
|
||
## §T — Tasks
|
||
|
||
```
|
||
id|status|task|cites
|
||
T1|.|unify install paths — README/DEPLOY/setup-venv.sh/uninstall say `/opt/jetson-counter`, service files & .env.example say `/opt/bytetrack-counter`|
|
||
T2|.|zombie var RESET_COUNTERS_AT_CUTOFF — defined in .env.example, unused in code (confirmed by review)|
|
||
T3|.|add version flag — no `--version` or git-derived version exists|
|
||
T4|.|keep reference Python clean — counter_live.py is artifact; focus edits on counter_live_rknn_bytetrack.py as port source|
|
||
T5|x|rename export routes — routes named `export-daily-csv` but emit `.xlsx`|I.flask
|
||
T6|x|fix live-stream snapshot path — some scripts default `/dev/shm/jetson-counter/`, example says `bytetrack-counter`|
|
||
T7|.|add dashboard health-check endpoint (no `/health` or `/api/status` exists)|
|
||
T8|.|add model checks on startup — model class names ! validated against CLASS_AYAM/CLASS_TALENAN (TensorRT variant validates; RKNN variants do not)|
|
||
T9|.|reset prev_gray on stream reconnect — stale gray ref causes crash or false motion|V19,B1
|
||
T10|.|decay inf_ms toward 0 when inference skipped — stale display misleads operator|
|
||
T11|x|reduce live-counter poll interval 2000→200ms for real-time feel|
|
||
T12|x|add recounting dashboard — dual-API counter panels, MP4 browser, go2rtc streaming|
|
||
T13|x|add template system — DASHBOARD_TEMPLATE env var selects dashboard.html|lamborghini|tesla|
|
||
T14|x|harden reset API — separate SHM marker creation from state file cleanup; return 500 on SHM failure|V20
|
||
T15|x|add upload recounting dashboard — MP4 upload, progress bar, RECOUNT_CMD replay|V21
|
||
T16|x|add cross-dashboard header links — UPLOAD_DASHBOARD_URL (live→upload) & LIVE_DASHBOARD_URL (upload→live)|I.config
|
||
T17|x|auto-on live preview when recount running — enablePreview() on START + applyState; onerror retry while streaming|V22
|
||
```
|
||
|
||
## §B — Bugs
|
||
|
||
```
|
||
id|date|cause|fix
|
||
B1|2026-07-29|prev_gray not reset on stream reconnect → cv2.absdiff crash or false motion|V19
|
||
```
|
||
|