Files
bytetrack-counter-cpp/SPEC.md
T
proitlab 90038da298 Force file sources to decode from frame 0; add first-frame diagnostics
OpenCV FFMPEG build buffer (CAP_PROP_BUFFERSIZE=1) on file sources was
landing mid-file, so the first decoded frame could show an ~11s-later
camera clock than the source's true frame 0. Gate BUFFERSIZE to live
sources only and seek file sources to CAP_PROP_POS_FRAMES=0. Log
pos_frames/pos_msec/frame_count/fps once on first successful read.

SPEC: B1, V18, T20. AGENTS: capture setup notes.
2026-08-10 12:00:18 +07:00

6.1 KiB

SPEC — ByteTrack Counter C++

§G — Goal

Real-time object detection + tracking + line-cross counting pipeline on RK3588 NPU. Two-class (ayam|talenan). Ayam crossing → increment batch count. Talenan crossing → close batch. Persist to SQLite + JSON.

§C — Constraints

  • RK3588 ARM64, RKNN NPU (librknnrt)
  • Model zenai_apc_cicalengka_20260609.rknn — dynamic input shape
  • C++17, OpenCV 4, Eigen3, SQLite3
  • systemd service, root, ProtectSystem=full
  • 2 classes: ayam_class_id=0, talenan_class_id=1
  • Single counting line, crossing direction rtl|ltr|both
  • Shift-based counting: day boundary at DAILY_CUTOFF_TIME

§I — Interfaces

env:  .env KEY=VALUE file — all config per config.env.example
env:  SHM_DIR → shared memory directory (/dev/shm/bytetrack-counter)
env:  LIVE_STREAM_FRAME_PATH → JPEG snapshot path
env:  RECORD_VIDEO, RECORD_DELAY_SECONDS, RECORD_END_DELAY, RECORD_RETENTION_DAYS, RECORD_CRF, RECORD_PRESET
cmd:  bytetrack-counter <config.env> [--source <rtsp://|http://|/path/to/file.mp4>] [--pause-on-first-frame]
db:   SQLite batches(id,counting_date,batch_number,camera_name,object_label,count,start_time,end_time)
db:   SQLite daily_summaries(id,counting_date,camera_name,object_label,total_count,total_batches)
file: JSON state — batch_number,count,start_time,last_detection_time,counted_event_ids[]
file: CSV crossing log — batch,frame,timestamp,chicken_id
file: SHM_DIR/.reset → trigger full state reset
file: SHM_DIR/.continue → resume processing after pause-on-first-frame
file: SHM_DIR/batch_pending_*.mp4 → confirmed when ayam detected, moved to OUTPUT_DIR/<batch counting_date>/batch_<num>_<YYYYMMDD>_<HHMMSS>.mp4 (num=batch, YYYYMMDD=counting_date, HHMMSS=move time)
sig:  SIGTERM|SIGINT → shutdown_requested → graceful exit

§V — Invariants

V1:  ∀ detection → score ≥ conf threshold before tracking
V2:  rknn_set_input_shapes called before rknn_inputs_set (dynamic shape model !)
V3:  ∀ track_id → counted ≤ once per batch (dedup via counted_event_ids)
V4:  talenan crossing → close batch only after ignore_batch_label_timeout (30s) from batch start
V5:  batch (count < min_object | duration < min_duration) → discard ⊥ persist
V6:  batch inactivity ≥ batch_timeout (300s) → auto-close
V7:  counting_date rolls at DAILY_CUTOFF_TIME → cutoff_watcher thread
V8:  ayam & talenan → separate ByteTrack instances (match_thresh_0=0.8, match_thresh_1=0.6)
V9:  live_stream_enabled → parent dir of LIVE_STREAM_FRAME_PATH created @ startup
V10: stale track (> TRACKED_PRUNE_SEC unseen) → prune from external tracking maps
V11: all dirs (output_dir, shm_dir, live_stream parent) created @ startup
V12: SHM_DIR/.reset → BatchStore::reset() — end active batch, clear memory, delete state_file, write blank live JPEG
V13: video recording: motion → SPECULATING (≤RECORD_DELAY_SECONDS, live only; file sources never discard) → CONFIRMED → talenan/batch-end → STOPPING (+RECORD_END_DELAY) → move to OUTPUT_DIR/<batch counting_date>/ via fork. feed_frame called every frame (no detection gaps)
V14: video retention: day-dirs > RECORD_RETENTION_DAYS old → removed @ startup & @ each CUTOFF_TIME (localtime cutoff matches folder names)
V15: RECORD_CRF (lower = better, -1 unset) & RECORD_PRESET → OPENCV_FFMPEG_WRITE_OPTIONS before writer_.open() (crf;<n>|preset;<p>)
V16: pause_on_first_frame (env/--pause-on-first-frame, file source only; live ignored w/ warn): process frame 0 → idle-loop, re-write annotated frame to LIVE_STREAM_FRAME_PATH, wait for SHM_DIR/.continue → delete → resume
V17: file sources (non-live) record from frame 0: SPECULATING never discarded, any active recording saved on batch close and shutdown → output first frame = source frame 0 (no RECORD_DELAY offset)
V18: file sources: cap opened w/ CAP_PROP_POS_FRAMES=0 (frame-0 seek); CAP_PROP_BUFFERSIZE=1 live-only; ∀ live|file → first decoded frame == source frame 0 (no build-buffer offset into file)

§T — Tasks

id|status|task|cites
T1|x|YOLO inference via RKNN NPU (letterbox, BGR2RGB, coordinate rescale)|V1,V2
T2|x|ByteTrack Kalman filter + two-stage IoU matching|V8
T3|x|Line-cross detection (rtl|ltr|both) with prev→current cx|V3
T4|x|Batch persistence: SQLite + JSON state file|V5,V6
T5|x|Batch dedup via counted_event_ids[]|V3
T6|x|Talenan-closes-batch logic with ignore-on-start timeout|V4
T7|x|Batch timeout thread (generational counter, detached)|V6
T8|x|Daily cutoff watcher thread|V7
T9|x|Optional motion detection skip-inference|C.motion
T10|x|Config from .env file (get_env with defaults)|I.env
T11|x|Drawing: HUD, hero count, line, boxes, popups, skeleton|-
T12|x|CsvLogger for crossing events|I.file
T13|x|Live JPEG snapshot to shared memory|V9
T14|x|systemd service with ProtectSystem + ReadWritePaths|I.cmd
T15|x|Graceful shutdown: SIGTERM → close batch → save state|I.sig
T16|x|Video per-batch recording: motion-triggered → speculating in shm (live only, discarded after RECORD_DELAY_SECONDS unconfirmed), confirmed when ayam detected, stopped RECORD_END_DELAY after batch close, moved to OUTPUT_DIR/<batch counting_date>/ via fork, retention cleanup at startup & cutoff, H.264 quality via RECORD_CRF/RECORD_PRESET (OPENCV_FFMPEG_WRITE_OPTIONS). Every frame fed (no detection gaps). File sources: record from frame 0, never discard, save on close & shutdown.|V13,V14,V15,V17
T17|~?|Python DEBUG_TRACKING equivalent (per-frame score/track dump)|-
T18|x|CLI --source/-s/--source= override of SOURCE (rtsp|http|local mp4; file = single pass)|I.cmd
T19|x|Pause-on-first-frame for file source: process frame 0, hold annotated frame as live view, wait for SHM_DIR/.continue, resume. Env PAUSE_ON_FIRST_FRAME + --pause-on-first-frame; stale .continue cleared @ startup; live source ignored w/ warning.|V16,V18
T20|x|File-source capture: POS_FRAMES=0 seek @ open (frame 0 !), BUFFERSIZE live-only; log pos_frames/pos_msec/frame_count/fps on first decoded frame|V18

§B — Bugs

id|date|cause|fix
B1|2026-08-10|file source CAP_PROP_BUFFERSIZE=1 @ open → OpenCV FFMPEG build buffer offset → first decoded frame ≠ source frame 0 (paused view showed ~11s-later clock 21:07:39 vs mp4 frame0 21:07:28)|V18