From af2854d7713288d37b1f344efb661005d249f72b Mon Sep 17 00:00:00 2001 From: Alberto-Audrix Date: Fri, 10 Jul 2026 13:30:13 +0700 Subject: [PATCH 1/4] deployment update --- DEPLOY.md | 239 ++++++++++++++++++++++++++++++++++++ config.env.example | 148 +++++----------------- env.example | 107 ++++++++-------- zenai-kpc-counter.service | 32 +++++ zenai-kpc-dashboard.service | 32 +++++ 5 files changed, 381 insertions(+), 177 deletions(-) create mode 100644 DEPLOY.md create mode 100644 zenai-kpc-counter.service create mode 100644 zenai-kpc-dashboard.service diff --git a/DEPLOY.md b/DEPLOY.md new file mode 100644 index 0000000..044133d --- /dev/null +++ b/DEPLOY.md @@ -0,0 +1,239 @@ +# 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 /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://: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 `; 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 | +| 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; # needed for /api/live-video MJPEG stream + } +} +``` + +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. diff --git a/config.env.example b/config.env.example index d5423b2..9ede88b 100644 --- a/config.env.example +++ b/config.env.example @@ -1,195 +1,105 @@ # ============================================================================= -# Edge RK3588 production counter + dashboard -# Shared config for: counter_live_rknn_bytetrack.py + counter_dashboard.py -# Copy to .env on device: cp config.env.example .env && nano .env +# DEPRECATED — use env.example instead. +# +# cp env.example .env && nano .env +# +# This file is kept for backward compatibility only. All new deployments should +# use env.example as the single canonical template. See DEPLOY.md. # ============================================================================= # --- Core paths --- -# Root output directory (logs, DB, video, CSV) -OUTPUT_DIR=/opt/bytetrack-counter -# SQLite database path for daily counter records & crossing logs -DB_PATH=/opt/bytetrack-counter/bytetrack_counter.db -# JSON file persisting the current active counting day state -STATE_FILE=/tmp/bytetrack_current_counter.json +OUTPUT_DIR=/opt/zenai-kpc-counter +DB_PATH=/opt/zenai-kpc-counter/counter.db +STATE_FILE=/opt/zenai-kpc-counter/current_counter.json # --- Input source --- -# RTSP / HTTP live stream, or a local video file path SOURCE=rtsp://user:pass@192.168.0.100:554/stream1 -# FFmpeg capture options passed to cv2.VideoCapture (RTSP low-latency flags) OPENCV_FFMPEG_CAPTURE_OPTIONS=rtsp_transport;tcp|fflags;nobuffer|flags;low_delay # --- RKNN model --- -# Path to exported .rknn model (YOLO format, e.g. yolo11n.rknn) -MODEL_PATH=/opt/models/yolo9t.rknn -# Input image size for the model (square, e.g. 320 → 320×320) +MODEL_PATH=/opt/models/your_model.rknn IMGSZ=320 -# Use FP16 inference on NPU (true/false); currently unused in ByteTrack variant HALF=false -# NPU core mask: 1=core0, 2=core1, 3=core0+core1, 7=all three -CORE_MASK=7 -# Compute device index (reserved; not used at runtime) +CORE_MASK=1 DEVICE=0 # --- YOLO decoder --- -# Number of object classes the model outputs -NUM_CLASSES=2 -# Apply sigmoid to raw class scores (true/false); set true if model head uses BCE logits +NUM_CLASSES=4 SCORE_SIGMOID=false # --- Detection --- -# Confidence threshold – detections below this are discarded before NMS -CONF=0.3 +CONF=0.5 # --- ByteTrack tracking --- -# Detections with score >= this get priority matching in the first association stage TRACK_HIGH_THRESH=0.5 -# Detections with score between this and TRACK_HIGH_THRESH are matched in the second stage -TRACK_LOW_THRESH=0.1 -# IoU threshold for the first-stage association (0–1). Higher = stricter overlap required -TRACK_MATCH_THRESH=0.8 -# Frames a track survives without a match before being permanently removed -TRACK_BUFFER=30 -# Minimum consecutive (or total) hits needed before a track is considered confirmed +TRACK_LOW_THRESH=0.3 +TRACK_MATCH_THRESH=0.7 +TRACK_BUFFER=60 TRACK_MIN_HITS=3 # --- ID-switch counting guards --- -# When a track's ID changes right at the counting line, one physical object can be -# counted twice (two IDs cross) or missed (neither ID sees the full transition). -# These two guards correct for that. -# -# Dedup guard (prevents double counting): after a crossing, a second crossing in -# the SAME direction within DEDUP_FRAMES frames and DEDUP_PX horizontal pixels is -# ignored (treated as the same object under a new ID). DEDUP_FRAMES=15 DEDUP_PX=60 -# To DISABLE the dedup guard, set DEDUP_PX=-1 (distance check can never match). -# -# Inheritance guard (prevents missed counting): when a brand-new track appears, it -# inherits the last position of a recently-seen nearby track (within INHERIT_SEC -# seconds and INHERIT_PX horizontal pixels) so the crossing is still detected -# across the ID switch. INHERIT_SEC=1.0 INHERIT_PX=60 -# To DISABLE the inheritance guard, set INHERIT_PX=-1 (distance check can never match). # --- Display --- -# Site name shown on the dashboard header (top-right) SITE_NAME=ZenAi # --- Object class names --- -# Camera / location identifier shown in HUD and stored in DB CAMERA_NAME=ZenAi -# Label used for batch grouping in the database -OBJECT_LABEL=object -# Class name for the counted object (must match model class order) -CLASS_OBJECT=object -# Model class ID for the object being counted (default 0) +OBJECT_LABEL=karung +CLASS_OBJECT=karung OBJECT_CLASS_ID=0 # --- Line crossing --- -# 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) -LINE_Y1_FRAC=0.33 -# Fixed y-coordinate for line 2 (overrides LINE_Y2_FRAC if set) +LINE_Y1_FRAC=0.70 LINE_Y2= -# Fraction of frame height for line 2 (default 0.66) -LINE_Y2_FRAC=0.66 +LINE_Y2_FRAC=0.30 # --- Counting day management --- -# Daily cutoff time (HH:MM) – a new counting day starts after this time and the -# previous day's counter_in / counter_out totals are finalized in the database. -# CUTOFF_TIME is an alias used by the dashboard; DAILY_CUTOFF_TIME takes priority in counter_live_rknn.py. DAILY_CUTOFF_TIME=20:00 CUTOFF_TIME=20:00 # --- CSV export --- -# Write per-crossing events to a CSV file (true/false) -EXPORT_CSV=true -# Path where the crossing CSV is written -CROSS_CSV=/opt/batch-counter/crossings.csv +EXPORT_CSV=false +CROSS_CSV=/opt/zenai-kpc-counter/crossings.csv # --- Crossing snapshots --- -# Save an annotated frame image every time an object crosses a line and the -# counter increases (true/false, default: false). Written to /cross/ -# (filename: __id_f.jpg) -SAVE_CROSS_SNAPSHOT=false -# Also save one snapshot the first time each object is detected, before it crosses -# (true/false, default: false). Written to /detect/ with the same track id so -# it can be correlated with the crossing snapshot -# (filename: _detect_id_f.jpg) +SAVE_CROSS_SNAPSHOT=true SAVE_DETECT_SNAPSHOT=false -# Base directory for snapshots (detect/ and cross/ subfolders are created inside). -# The dashboard reads this same path to display the snapshot gallery, so keep it -# identical for both the counter and the dashboard. -CROSS_SNAPSHOT_DIR=/opt/batch-counter/snapshots -# JPEG quality for snapshots (1-100) +CROSS_SNAPSHOT_DIR=/opt/zenai-kpc-counter/snapshots CROSS_SNAPSHOT_QUALITY=85 -# Retention: keep at most this many snapshot files (detect + cross combined); -# oldest are deleted first (0 = unlimited) CROSS_SNAPSHOT_MAX_FILES=1000 -# Retention: delete snapshots older than this many days (0 = never by age) -CROSS_SNAPSHOT_MAX_AGE_DAYS=7 -# Run the cleanup sweep at most once every N seconds -CROSS_SNAPSHOT_CLEANUP_SEC=60 +CROSS_SNAPSHOT_MAX_AGE_DAYS=3 +CROSS_SNAPSHOT_CLEANUP_SEC=3600 # --- Rate / performance --- -# Enable motion detection pre-filter: skip inference on frames with no movement -# (true/false, default: false), saving NPU/CPU load. Motion is measured by the -# fraction of pixels that changed (localized-motion aware), NOT the whole-frame -# average, so an object entering the edge of the frame is detected immediately. -MOTION_DETECTION_ENABLED=false -# Per-pixel intensity change (0-255) for a pixel to count as "moved". Lower = more -# sensitive to subtle movement. Default 25. +MOTION_DETECTION_ENABLED=true MOTION_PIXEL_DELTA=25 -# Fraction of frame pixels (0-1) that must change to trigger inference. Lower = -# more sensitive / detects smaller or farther objects sooner. Default 0.002 (0.2%). MOTION_MIN_AREA_FRAC=0.002 -# Heartbeat: always run inference at least every N frames even with no detected -# motion, so a slow or barely-moving object is never missed for long. Default 15. MOTION_HEARTBEAT_FRAMES=15 -# (Deprecated) old whole-frame mean-difference threshold; no longer used. MOTION_THRESHOLD=5.0 -# Sliding window in seconds for computing the crossing rate (objects/minute) RATE_WINDOW_SEC=60 -# Number of frames to discard at startup to let the stream buffer stabilise WARMUP_FRAMES=30 -# Delay in seconds between stream reconnection attempts RECONNECT_DELAY_SEC=3 -# Maximum reconnection attempts (0 = infinite) MAX_RECONNECT_ATTEMPTS=0 -# Seconds after which a tracked but unseen object is pruned from the active set TRACKED_PRUNE_SEC=300 # --- Video recording --- -# Save annotated frames to segmented MP4 files (true/false) RECORD_VIDEO=false -# Duration in seconds of each video segment file VIDEO_SEGMENT_SEC=3600 -# Output video FPS (fallback if source FPS is unknown or ≤ 1) OUTPUT_FPS=15 # --- Live stream snapshot --- -# Periodically write the latest annotated frame as JPEG for an external web server -LIVE_STREAM_ENABLED=false -# Path to the shared-memory snapshot file (served by nginx / lighttpd) -LIVE_STREAM_FRAME_PATH=/dev/shm/byetrack-counter/live_frame.jpg -# JPEG quality (1–100) +LIVE_STREAM_ENABLED=true +LIVE_STREAM_FRAME_PATH=/dev/shm/zenai-kpc-counter/live_frame.jpg LIVE_STREAM_QUALITY=75 -# Write the snapshot every N frames (lower = more frequent updates) LIVE_STREAM_EVERY_N=2 # --- Dashboard (counter_dashboard.py) --- -# Flask secret key for session/cookie signing — change in production! SECRET_KEY=change-me-in-production -# Bind address for the Flask web server DASHBOARD_HOST=0.0.0.0 -# Listen port for the dashboard web UI 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 diff --git a/env.example b/env.example index f2164da..bf4ef20 100644 --- a/env.example +++ b/env.example @@ -1,27 +1,32 @@ # ============================================================================= -# Edge RK3588 production counter + dashboard -# Shared config for: counter_live_rknn_bytetrack.py + counter_dashboard.py -# Copy to .env on device: cp config.env.example .env && nano .env +# ZenAI KPC edge counter + dashboard +# Shared config for: counter_live_rknn.py + counter_dashboard.py +# +# On device: +# cp env.example .env && nano .env +# +# Install path (systemd): /opt/zenai-kpc-python +# Data path: /opt/zenai-kpc-counter +# See DEPLOY.md for full setup instructions. # ============================================================================= # --- Core paths --- -# Root output directory (logs, DB, video, CSV) -OUTPUT_DIR=/opt/zenai-kpc-bt-counter -# SQLite database path for daily counter records & crossing logs -DB_PATH=/tmp/counter.db -# JSON file persisting the current active counting day state -STATE_FILE=/tmp/current_counter.json +# Root output directory (video segments, default snapshot/CSV paths) +OUTPUT_DIR=/opt/zenai-kpc-counter +# SQLite database path for daily counter records +DB_PATH=/opt/zenai-kpc-counter/counter.db +# JSON file for the active counting-day state (counter writes, dashboard reads) +STATE_FILE=/opt/zenai-kpc-counter/current_counter.json # --- Input source --- # RTSP / HTTP live stream, or a local video file path -#SOURCE=rtsp://user:pass@192.168.0.100:554/stream1 -SOURCE=rtsp://10.38.30.64:8554/my_stream +SOURCE=rtsp://user:pass@192.168.0.100:554/stream1 # FFmpeg capture options passed to cv2.VideoCapture (RTSP low-latency flags) OPENCV_FFMPEG_CAPTURE_OPTIONS=rtsp_transport;tcp|fflags;nobuffer|flags;low_delay # --- RKNN model --- -# Path to exported .rknn model (YOLO format, e.g. yolo11n.rknn) -MODEL_PATH=/opt/models/zenai_kac_sukawarna_20260702.rknn +# Path to exported .rknn model (YOLO format) +MODEL_PATH=/opt/models/your_model.rknn # Input image size for the model (square, e.g. 320 → 320×320) IMGSZ=320 # Use FP16 inference on NPU (true/false); currently unused in ByteTrack variant @@ -53,14 +58,24 @@ TRACK_BUFFER=60 # Minimum consecutive (or total) hits needed before a track is considered confirmed TRACK_MIN_HITS=3 +# --- ID-switch counting guards --- +# Dedup guard: ignore a second crossing in the same direction within DEDUP_FRAMES +# frames and DEDUP_PX horizontal pixels (set DEDUP_PX=-1 to disable). +DEDUP_FRAMES=15 +DEDUP_PX=60 +# Inheritance guard: new tracks inherit position from a recently-seen nearby track +# (set INHERIT_PX=-1 to disable). +INHERIT_SEC=1.0 +INHERIT_PX=60 + # --- Display --- -# Site name shown on the dashboard header (top-right) +# Site name shown on the dashboard header SITE_NAME=ZenAi # --- Object class names --- # Camera / location identifier shown in HUD and stored in DB CAMERA_NAME=ZenAi -# Label used for batch grouping in the database +# Label used for grouping in the database OBJECT_LABEL=karung # Class name for the counted object (must match model class order) CLASS_OBJECT=karung @@ -68,83 +83,59 @@ CLASS_OBJECT=karung OBJECT_CLASS_ID=0 # --- Line crossing --- -# 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 1 (~upper): counts top-to-down (IN). Line 2 (~lower): bottom-to-up (OUT). +# Fixed y-coordinate overrides the fraction if set. LINE_Y1= -# Fraction of frame height for line 1 (default 0.33) LINE_Y1_FRAC=0.70 -# Fixed y-coordinate for line 2 (overrides LINE_Y2_FRAC if set) LINE_Y2= -# Fraction of frame height for line 2 (default 0.66) LINE_Y2_FRAC=0.30 # --- Counting day management --- -# Daily cutoff time (HH:MM) – a new counting day starts after this time and the -# previous day's counter_in / counter_out totals are finalized in the database. -# CUTOFF_TIME is an alias used by the dashboard; DAILY_CUTOFF_TIME takes priority in counter_live_rknn.py. +# New counting day starts after this time (HH:MM). DAILY_CUTOFF_TIME=20:00 CUTOFF_TIME=20:00 # --- CSV export --- -# Write per-crossing events to a CSV file (true/false) EXPORT_CSV=false -# Path where the crossing CSV is written -CROSS_CSV=/tmp/crossings.csv +CROSS_CSV=/opt/zenai-kpc-counter/crossings.csv + +# --- Crossing snapshots --- +SAVE_CROSS_SNAPSHOT=true +SAVE_DETECT_SNAPSHOT=false +CROSS_SNAPSHOT_DIR=/opt/zenai-kpc-counter/snapshots +CROSS_SNAPSHOT_QUALITY=85 +CROSS_SNAPSHOT_MAX_FILES=1000 +CROSS_SNAPSHOT_MAX_AGE_DAYS=3 +CROSS_SNAPSHOT_CLEANUP_SEC=3600 # --- Rate / performance --- -# Enable motion detection pre-filter: skip inference on frames with no movement -# (true/false, default: false). When enabled, frames below MOTION_THRESHOLD are -# skipped, saving NPU/CPU load. MOTION_DETECTION_ENABLED=true -# Mean absolute pixel difference threshold (0–255) to consider a frame as having -# motion. Lower = more sensitive. Default 5.0. +MOTION_PIXEL_DELTA=25 +MOTION_MIN_AREA_FRAC=0.002 +MOTION_HEARTBEAT_FRAMES=15 MOTION_THRESHOLD=5.0 -# Sliding window in seconds for computing the crossing rate (objects/minute) RATE_WINDOW_SEC=60 -# Number of frames to discard at startup to let the stream buffer stabilise WARMUP_FRAMES=30 -# Delay in seconds between stream reconnection attempts RECONNECT_DELAY_SEC=3 -# Maximum reconnection attempts (0 = infinite) MAX_RECONNECT_ATTEMPTS=0 -# Seconds after which a tracked but unseen object is pruned from the active set TRACKED_PRUNE_SEC=300 # --- Video recording --- -# Save annotated frames to segmented MP4 files (true/false) RECORD_VIDEO=false -# Duration in seconds of each video segment file VIDEO_SEGMENT_SEC=3600 -# Output video FPS (fallback if source FPS is unknown or ≤ 1) OUTPUT_FPS=15 # --- Live stream snapshot --- -# Periodically write the latest annotated frame as JPEG for an external web server LIVE_STREAM_ENABLED=true -# Path to the shared-memory snapshot file (served by nginx / lighttpd) -LIVE_STREAM_FRAME_PATH=/dev/shm/byetrack-counter/live_frame.jpg -# JPEG quality (1–100) +LIVE_STREAM_FRAME_PATH=/dev/shm/zenai-kpc-counter/live_frame.jpg LIVE_STREAM_QUALITY=75 -# Write the snapshot every N frames (lower = more frequent updates) LIVE_STREAM_EVERY_N=2 # --- Dashboard (counter_dashboard.py) --- -# Flask secret key for session/cookie signing — change in production! SECRET_KEY=change-me-in-production -# Bind address for the Flask web server DASHBOARD_HOST=0.0.0.0 -# Listen port for the dashboard web UI 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 -SAVE_CROSS_SNAPSHOT=true -CROSS_SNAPSHOT_DIR=/opt/zenai-kpc-snaps/snapshots -CROSS_SNAPSHOT_CLEANUP_SEC=3600 -CROSS_SNAPSHOT_MAX_AGE_DAYS=3 - -DEDUP_PX=-1 +# Debug: set DEBUG_TRACKING=true to log per-frame tracking details to stdout +# DEBUG_TRACKING=false diff --git a/zenai-kpc-counter.service b/zenai-kpc-counter.service new file mode 100644 index 0000000..c4e92a2 --- /dev/null +++ b/zenai-kpc-counter.service @@ -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 diff --git a/zenai-kpc-dashboard.service b/zenai-kpc-dashboard.service new file mode 100644 index 0000000..1b5cc13 --- /dev/null +++ b/zenai-kpc-dashboard.service @@ -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 -- 2.54.0 From 314b598c668778041ae362c487f7edff54f707a1 Mon Sep 17 00:00:00 2001 From: Alberto-Audrix Date: Fri, 10 Jul 2026 13:54:36 +0700 Subject: [PATCH 2/4] fix : live view buffering --- DEPLOY.md | 6 ++++-- counter_dashboard.py | 36 ++++++++++++++++++++++-------------- counter_live_rknn.py | 16 +++++++++++++--- templates/dashboard.html | 39 ++++++++++++++++++++++++++++++++++----- 4 files changed, 73 insertions(+), 24 deletions(-) diff --git a/DEPLOY.md b/DEPLOY.md index 044133d..289bb12 100644 --- a/DEPLOY.md +++ b/DEPLOY.md @@ -193,7 +193,7 @@ sudo systemctl start zenai-kpc-counter zenai-kpc-dashboard | Counter won't start | `journalctl -u zenai-kpc-counter`; verify `MODEL_PATH` exists; RKNN drivers installed | | No RTSP frames | Ping camera; test with `ffplay `; 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 | +| 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 | @@ -222,7 +222,9 @@ server { proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; - proxy_buffering off; # needed for /api/live-video MJPEG stream + proxy_buffering off; # required for /api/live-video MJPEG stream + proxy_read_timeout 3600s; # keep long-lived MJPEG connections open + proxy_send_timeout 3600s; } } ``` diff --git a/counter_dashboard.py b/counter_dashboard.py index 0ceef5b..c5e3820 100644 --- a/counter_dashboard.py +++ b/counter_dashboard.py @@ -44,32 +44,40 @@ DASHBOARD_PORT = int(os.getenv("DASHBOARD_PORT", "5000")) DASHBOARD_HOST = os.getenv("DASHBOARD_HOST", "0.0.0.0") FLASK_DEBUG = os.getenv("FLASK_DEBUG", "false").lower() == "true" +_JPEG_SOI = b"\xff\xd8" +_JPEG_EOI = b"\xff\xd9" +_MIN_JPEG_BYTES = 128 + + +def _is_valid_jpeg(data): + return ( + data + and len(data) >= _MIN_JPEG_BYTES + and data.startswith(_JPEG_SOI) + and data.endswith(_JPEG_EOI) + ) + + @app.route("/api/live-video") def api_live_video(): - if not os.path.isfile(LIVE_STREAM_FRAME_PATH): - return jsonify({"success": False, "error": "Live stream frame not available yet"}), 503 - def generate(): - consecutive_fails = 0 - MAX_FAILS = 30 while True: try: + if not os.path.isfile(LIVE_STREAM_FRAME_PATH): + time.sleep(0.5) + continue with open(LIVE_STREAM_FRAME_PATH, "rb") as f: jpeg = f.read() - consecutive_fails = 0 + if not _is_valid_jpeg(jpeg): + time.sleep(0.05) + continue yield (b"--frame\r\n" b"Content-Type: image/jpeg\r\n\r\n" + jpeg + b"\r\n") except FileNotFoundError: - consecutive_fails += 1 - if consecutive_fails >= MAX_FAILS: - return - time.sleep(1.0) + time.sleep(0.5) continue except Exception: - consecutive_fails += 1 - if consecutive_fails >= MAX_FAILS: - return - time.sleep(0.5) + time.sleep(0.25) continue time.sleep(0.05) return Response(generate(), mimetype="multipart/x-mixed-replace; boundary=frame") diff --git a/counter_live_rknn.py b/counter_live_rknn.py index 4847a97..aaeee1d 100644 --- a/counter_live_rknn.py +++ b/counter_live_rknn.py @@ -1002,6 +1002,18 @@ def draw_popups(img, popups, frame_idx): return alive +def _write_live_frame_atomic(path, jpeg_bytes): + """Write JPEG atomically so dashboard readers never see a partial file.""" + dest = Path(path) + dest.parent.mkdir(parents=True, exist_ok=True) + tmp = dest.with_suffix(dest.suffix + ".tmp") + with open(tmp, "wb") as f: + f.write(jpeg_bytes) + f.flush() + os.fsync(f.fileno()) + os.replace(tmp, dest) + + def connect_stream(source, warmup=WARMUP_FRAMES): attempts = 0 while not shutdown_requested: @@ -1381,12 +1393,10 @@ def run(): if LIVE_STREAM_ENABLED and frame_idx % LIVE_STREAM_EVERY_N == 0: try: - Path(LIVE_STREAM_FRAME_PATH).parent.mkdir(parents=True, exist_ok=True) _, jpeg = cv2.imencode( ".jpg", frame, [cv2.IMWRITE_JPEG_QUALITY, LIVE_STREAM_QUALITY] ) - with open(LIVE_STREAM_FRAME_PATH, "wb") as f: - f.write(jpeg.tobytes()) + _write_live_frame_atomic(LIVE_STREAM_FRAME_PATH, jpeg.tobytes()) except Exception: pass diff --git a/templates/dashboard.html b/templates/dashboard.html index 40648a4..9ae0755 100644 --- a/templates/dashboard.html +++ b/templates/dashboard.html @@ -811,6 +811,9 @@ function updateToggleIcon() { let mainChart = null; let videoActive = false; let videoErrorTimer = null; +let videoWatchdogTimer = null; +let lastVideoFrameAt = 0; +const VIDEO_STALL_MS = 8000; document.addEventListener('DOMContentLoaded', () => { loadSummary(); @@ -1083,6 +1086,32 @@ document.addEventListener('keydown', (e) => { // --- Live Video Feed --- +function reconnectVideo() { + if (!videoActive) return; + const img = document.getElementById('live-video-img'); + if (!img) return; + lastVideoFrameAt = Date.now(); + img.src = '/api/live-video?' + Date.now(); +} + +function startVideoWatchdog() { + stopVideoWatchdog(); + lastVideoFrameAt = Date.now(); + videoWatchdogTimer = setInterval(() => { + if (!videoActive) return; + if (Date.now() - lastVideoFrameAt > VIDEO_STALL_MS) { + reconnectVideo(); + } + }, 2000); +} + +function stopVideoWatchdog() { + if (videoWatchdogTimer) { + clearInterval(videoWatchdogTimer); + videoWatchdogTimer = null; + } +} + function toggleVideo() { const btn = document.getElementById('videoToggle'); const img = document.getElementById('live-video-img'); @@ -1093,14 +1122,16 @@ function toggleVideo() { if (videoActive) { resetFilter(); - img.src = '/api/live-video?' + Date.now(); img.style.display = 'block'; if (placeholder) placeholder.style.display = 'none'; btn.textContent = 'STREAM ON'; btn.classList.add('active'); status.innerHTML = ' connecting...'; status.className = 'video-status waiting'; + reconnectVideo(); + startVideoWatchdog(); } else { + stopVideoWatchdog(); img.src = ''; img.style.display = 'none'; if (placeholder) placeholder.style.display = ''; @@ -1113,6 +1144,7 @@ function toggleVideo() { } function onVideoLoad() { + lastVideoFrameAt = Date.now(); const status = document.getElementById('video-status'); status.innerHTML = ' live'; status.className = 'video-status connected'; @@ -1124,10 +1156,7 @@ function onVideoError() { if (overlay) overlay.classList.add('visible'); if (videoErrorTimer) clearTimeout(videoErrorTimer); videoErrorTimer = setTimeout(() => { - if (videoActive) { - const img = document.getElementById('live-video-img'); - img.src = '/api/live-video?' + Date.now(); - } + reconnectVideo(); }, 2000); } -- 2.54.0 From 6071d56dc3aef8b6a1a4eb4693fd2a2c797e8cbe Mon Sep 17 00:00:00 2001 From: Alberto-Audrix Date: Tue, 14 Jul 2026 08:34:44 +0700 Subject: [PATCH 3/4] merging to main --- config.env.example | 144 +++++++++++++++++++++++++++++++-------- counter_dashboard.py | 34 ++++----- counter_live_rknn.py | 14 +--- env.example | 109 +++++++++++++++-------------- templates/dashboard.html | 41 ++--------- 5 files changed, 196 insertions(+), 146 deletions(-) diff --git a/config.env.example b/config.env.example index ca08cf3..6b4bb92 100644 --- a/config.env.example +++ b/config.env.example @@ -1,30 +1,39 @@ # ============================================================================= -# DEPRECATED — use env.example instead. -# -# cp env.example .env && nano .env -# -# This file is kept for backward compatibility only. All new deployments should -# use env.example as the single canonical template. See DEPLOY.md. +# Edge RK3588 production counter + dashboard +# Shared config for: counter_live_rknn_bytetrack.py + counter_dashboard.py +# Copy to .env on device: cp config.env.example .env && nano .env # ============================================================================= # --- Core paths --- -OUTPUT_DIR=/opt/zenai-kpc-counter -DB_PATH=/opt/zenai-kpc-counter/counter.db -STATE_FILE=/opt/zenai-kpc-counter/current_counter.json +# Root output directory (logs, DB, video, CSV) +OUTPUT_DIR=/opt/bytetrack-counter +# SQLite database path for daily counter records & crossing logs +DB_PATH=/opt/bytetrack-counter/bytetrack_counter.db +# JSON file persisting the current active counting day state +STATE_FILE=/tmp/bytetrack_current_counter.json # --- Input source --- +# RTSP / HTTP live stream, or a local video file path SOURCE=rtsp://user:pass@192.168.0.100:554/stream1 +# FFmpeg capture options passed to cv2.VideoCapture (RTSP low-latency flags) OPENCV_FFMPEG_CAPTURE_OPTIONS=rtsp_transport;tcp|fflags;nobuffer|flags;low_delay # --- RKNN model --- -MODEL_PATH=/opt/models/your_model.rknn +# Path to exported .rknn model (YOLO format, e.g. yolo11n.rknn) +MODEL_PATH=/opt/models/yolo9t.rknn +# Input image size for the model (square, e.g. 320 → 320×320) IMGSZ=320 +# Use FP16 inference on NPU (true/false); currently unused in ByteTrack variant HALF=false -CORE_MASK=1 +# NPU core mask: 1=core0, 2=core1, 3=core0+core1, 7=all three +CORE_MASK=7 +# Compute device index (reserved; not used at runtime) DEVICE=0 # --- YOLO decoder --- -NUM_CLASSES=4 +# Number of object classes the model outputs +NUM_CLASSES=2 +# Apply sigmoid to raw class scores (true/false); set true if model head uses BCE logits SCORE_SIGMOID=false # --- Detection --- @@ -37,55 +46,117 @@ CONF=0.3 NMS_IOU=0.45 # --- ByteTrack tracking --- +# Detections with score >= this get priority matching in the first association stage TRACK_HIGH_THRESH=0.5 -TRACK_LOW_THRESH=0.3 -TRACK_MATCH_THRESH=0.7 -TRACK_BUFFER=60 +# Detections with score between this and TRACK_HIGH_THRESH are matched in the second stage +TRACK_LOW_THRESH=0.1 +# IoU threshold for the first-stage association (0–1). Higher = stricter overlap required +TRACK_MATCH_THRESH=0.8 +# Frames a track survives without a match before being permanently removed +TRACK_BUFFER=30 +# Minimum consecutive (or total) hits needed before a track is considered confirmed TRACK_MIN_HITS=3 # --- ID-switch counting guards --- +# When a track's ID changes right at the counting line, one physical object can be +# counted twice (two IDs cross) or missed (neither ID sees the full transition). +# These two guards correct for that. +# +# Dedup guard (prevents double counting): after a crossing, a second crossing in +# the SAME direction within DEDUP_FRAMES frames and DEDUP_PX horizontal pixels is +# ignored (treated as the same object under a new ID). DEDUP_FRAMES=15 DEDUP_PX=60 +# To DISABLE the dedup guard, set DEDUP_PX=-1 (distance check can never match). +# +# Inheritance guard (prevents missed counting): when a brand-new track appears, it +# inherits the last position of a recently-seen nearby track (within INHERIT_SEC +# seconds and INHERIT_PX horizontal pixels) so the crossing is still detected +# across the ID switch. INHERIT_SEC=1.0 INHERIT_PX=60 +# To DISABLE the inheritance guard, set INHERIT_PX=-1 (distance check can never match). # --- Display --- +# Site name shown on the dashboard header (top-right) SITE_NAME=ZenAi # --- Object class names --- +# Camera / location identifier shown in HUD and stored in DB CAMERA_NAME=ZenAi -OBJECT_LABEL=karung -CLASS_OBJECT=karung +# Label used for batch grouping in the database +OBJECT_LABEL=object +# Class name for the counted object (must match model class order) +CLASS_OBJECT=object +# Model class ID for the object being counted (default 0) OBJECT_CLASS_ID=0 # --- Line crossing --- +# 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= -LINE_Y1_FRAC=0.70 +# Fraction of frame height for line 1 (default 0.33) +LINE_Y1_FRAC=0.33 +# Fixed y-coordinate for line 2 (overrides LINE_Y2_FRAC if set) LINE_Y2= -LINE_Y2_FRAC=0.30 +# Fraction of frame height for line 2 (default 0.66) +LINE_Y2_FRAC=0.66 # --- Counting day management --- +# Daily cutoff time (HH:MM) – a new counting day starts after this time and the +# previous day's counter_in / counter_out totals are finalized in the database. +# CUTOFF_TIME is an alias used by the dashboard; DAILY_CUTOFF_TIME takes priority in counter_live_rknn.py. DAILY_CUTOFF_TIME=20:00 CUTOFF_TIME=20:00 # --- CSV export --- -EXPORT_CSV=false -CROSS_CSV=/opt/zenai-kpc-counter/crossings.csv +# Write per-crossing events to a CSV file (true/false) +EXPORT_CSV=true +# Path where the crossing CSV is written +CROSS_CSV=/opt/batch-counter/crossings.csv # --- Crossing snapshots --- -SAVE_CROSS_SNAPSHOT=true +# Save an annotated frame image every time an object crosses a line and the +# counter increases (true/false, default: false). Written to /cross/ +# (filename: __id_f.jpg) +SAVE_CROSS_SNAPSHOT=false +# Also save one snapshot the first time each object is detected, before it crosses +# (true/false, default: false). Written to /detect/ with the same track id so +# it can be correlated with the crossing snapshot +# (filename: _detect_id_f.jpg) SAVE_DETECT_SNAPSHOT=false -CROSS_SNAPSHOT_DIR=/opt/zenai-kpc-counter/snapshots +# Base directory for snapshots (detect/ and cross/ subfolders are created inside). +# The dashboard reads this same path to display the snapshot gallery, so keep it +# identical for both the counter and the dashboard. +CROSS_SNAPSHOT_DIR=/opt/batch-counter/snapshots +# JPEG quality for snapshots (1-100) CROSS_SNAPSHOT_QUALITY=85 +# Retention: keep at most this many snapshot files (detect + cross combined); +# oldest are deleted first (0 = unlimited) CROSS_SNAPSHOT_MAX_FILES=1000 -CROSS_SNAPSHOT_MAX_AGE_DAYS=3 -CROSS_SNAPSHOT_CLEANUP_SEC=3600 +# Retention: delete snapshots older than this many days (0 = never by age) +CROSS_SNAPSHOT_MAX_AGE_DAYS=7 +# Run the cleanup sweep at most once every N seconds +CROSS_SNAPSHOT_CLEANUP_SEC=60 # --- Rate / performance --- -MOTION_DETECTION_ENABLED=true +# Enable motion detection pre-filter: skip inference on frames with no movement +# (true/false, default: false), saving NPU/CPU load. Motion is measured by the +# fraction of pixels that changed (localized-motion aware), NOT the whole-frame +# average, so an object entering the edge of the frame is detected immediately. +MOTION_DETECTION_ENABLED=false +# Per-pixel intensity change (0-255) for a pixel to count as "moved". Lower = more +# sensitive to subtle movement. Default 25. MOTION_PIXEL_DELTA=25 +# Fraction of frame pixels (0-1) that must change to trigger inference. Lower = +# more sensitive / detects smaller or farther objects sooner. Default 0.002 (0.2%). MOTION_MIN_AREA_FRAC=0.002 +# Heartbeat: always run inference at least every N frames even with no detected +# motion, so a slow or barely-moving object is never missed for long. Default 15. MOTION_HEARTBEAT_FRAMES=15 +# (Deprecated) old whole-frame mean-difference threshold; no longer used. MOTION_THRESHOLD=5.0 # --- Runtime control (start/stop counting on the fly) --- @@ -115,24 +186,41 @@ CONTROL_SOCKET_PORT=5090 # Sliding window in seconds for computing the crossing rate (objects/minute) RATE_WINDOW_SEC=60 +# Number of frames to discard at startup to let the stream buffer stabilise WARMUP_FRAMES=30 +# Delay in seconds between stream reconnection attempts RECONNECT_DELAY_SEC=3 +# Maximum reconnection attempts (0 = infinite) MAX_RECONNECT_ATTEMPTS=0 +# Seconds after which a tracked but unseen object is pruned from the active set TRACKED_PRUNE_SEC=300 # --- Video recording --- +# Save annotated frames to segmented MP4 files (true/false) RECORD_VIDEO=false +# Duration in seconds of each video segment file VIDEO_SEGMENT_SEC=3600 +# Output video FPS (fallback if source FPS is unknown or ≤ 1) OUTPUT_FPS=15 # --- Live stream snapshot --- -LIVE_STREAM_ENABLED=true -LIVE_STREAM_FRAME_PATH=/dev/shm/zenai-kpc-counter/live_frame.jpg +# Periodically write the latest annotated frame as JPEG for an external web server +LIVE_STREAM_ENABLED=false +# Path to the shared-memory snapshot file (served by nginx / lighttpd) +LIVE_STREAM_FRAME_PATH=/dev/shm/byetrack-counter/live_frame.jpg +# JPEG quality (1–100) LIVE_STREAM_QUALITY=75 +# Write the snapshot every N frames (lower = more frequent updates) LIVE_STREAM_EVERY_N=2 # --- Dashboard (counter_dashboard.py) --- +# Flask secret key for session/cookie signing — change in production! SECRET_KEY=change-me-in-production +# Bind address for the Flask web server DASHBOARD_HOST=0.0.0.0 +# Listen port for the dashboard web UI 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 \ No newline at end of file diff --git a/counter_dashboard.py b/counter_dashboard.py index 7fa6525..c645346 100644 --- a/counter_dashboard.py +++ b/counter_dashboard.py @@ -48,28 +48,16 @@ DASHBOARD_PORT = int(os.getenv("DASHBOARD_PORT", "5000")) DASHBOARD_HOST = os.getenv("DASHBOARD_HOST", "0.0.0.0") FLASK_DEBUG = os.getenv("FLASK_DEBUG", "false").lower() == "true" -_JPEG_SOI = b"\xff\xd8" -_JPEG_EOI = b"\xff\xd9" -_MIN_JPEG_BYTES = 128 - - -def _is_valid_jpeg(data): - return ( - data - and len(data) >= _MIN_JPEG_BYTES - and data.startswith(_JPEG_SOI) - and data.endswith(_JPEG_EOI) - ) - - @app.route("/api/live-video") def api_live_video(): + if not os.path.isfile(LIVE_STREAM_FRAME_PATH): + return jsonify({"success": False, "error": "Live stream frame not available yet"}), 503 + def generate(): + consecutive_fails = 0 + MAX_FAILS = 30 while True: try: - if not os.path.isfile(LIVE_STREAM_FRAME_PATH): - time.sleep(0.5) - continue with open(LIVE_STREAM_FRAME_PATH, "rb") as f: jpeg = f.read() if not jpeg or len(jpeg) < 2 or jpeg[:2] != b"\xff\xd8": @@ -82,10 +70,16 @@ def api_live_video(): yield (b"--frame\r\n" b"Content-Type: image/jpeg\r\n\r\n" + jpeg + b"\r\n") except FileNotFoundError: - time.sleep(0.5) + consecutive_fails += 1 + if consecutive_fails >= MAX_FAILS: + return + time.sleep(1.0) continue except Exception: - time.sleep(0.25) + consecutive_fails += 1 + if consecutive_fails >= MAX_FAILS: + return + time.sleep(0.5) continue time.sleep(0.05) return Response(generate(), mimetype="multipart/x-mixed-replace; boundary=frame") @@ -544,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) \ No newline at end of file diff --git a/counter_live_rknn.py b/counter_live_rknn.py index b9082b2..4f8f0e0 100644 --- a/counter_live_rknn.py +++ b/counter_live_rknn.py @@ -1108,18 +1108,6 @@ def draw_popups(img, popups, frame_idx): return alive -def _write_live_frame_atomic(path, jpeg_bytes): - """Write JPEG atomically so dashboard readers never see a partial file.""" - dest = Path(path) - dest.parent.mkdir(parents=True, exist_ok=True) - tmp = dest.with_suffix(dest.suffix + ".tmp") - with open(tmp, "wb") as f: - f.write(jpeg_bytes) - f.flush() - os.fsync(f.fileno()) - os.replace(tmp, dest) - - def connect_stream(source, warmup=WARMUP_FRAMES): attempts = 0 while not shutdown_requested: @@ -1605,4 +1593,4 @@ object_tracked = {} if __name__ == "__main__": - run() + run() \ No newline at end of file diff --git a/env.example b/env.example index bf4ef20..d0db80e 100644 --- a/env.example +++ b/env.example @@ -1,32 +1,27 @@ -# ============================================================================= -# ZenAI KPC edge counter + dashboard -# Shared config for: counter_live_rknn.py + counter_dashboard.py -# -# On device: -# cp env.example .env && nano .env -# -# Install path (systemd): /opt/zenai-kpc-python -# Data path: /opt/zenai-kpc-counter -# See DEPLOY.md for full setup instructions. + # ============================================================================= +# Edge RK3588 production counter + dashboard +# Shared config for: counter_live_rknn_bytetrack.py + counter_dashboard.py +# Copy to .env on device: cp config.env.example .env && nano .env # ============================================================================= # --- Core paths --- -# Root output directory (video segments, default snapshot/CSV paths) -OUTPUT_DIR=/opt/zenai-kpc-counter -# SQLite database path for daily counter records -DB_PATH=/opt/zenai-kpc-counter/counter.db -# JSON file for the active counting-day state (counter writes, dashboard reads) -STATE_FILE=/opt/zenai-kpc-counter/current_counter.json +# Root output directory (logs, DB, video, CSV) +OUTPUT_DIR=/opt/zenai-kpc-bt-counter +# SQLite database path for daily counter records & crossing logs +DB_PATH=/tmp/counter.db +# JSON file persisting the current active counting day state +STATE_FILE=/tmp/current_counter.json # --- Input source --- # RTSP / HTTP live stream, or a local video file path -SOURCE=rtsp://user:pass@192.168.0.100:554/stream1 +#SOURCE=rtsp://user:pass@192.168.0.100:554/stream1 +SOURCE=rtsp://10.38.30.64:8554/my_stream # FFmpeg capture options passed to cv2.VideoCapture (RTSP low-latency flags) OPENCV_FFMPEG_CAPTURE_OPTIONS=rtsp_transport;tcp|fflags;nobuffer|flags;low_delay # --- RKNN model --- -# Path to exported .rknn model (YOLO format) -MODEL_PATH=/opt/models/your_model.rknn +# Path to exported .rknn model (YOLO format, e.g. yolo11n.rknn) +MODEL_PATH=/opt/models/zenai_kac_sukawarna_20260702.rknn # Input image size for the model (square, e.g. 320 → 320×320) IMGSZ=320 # Use FP16 inference on NPU (true/false); currently unused in ByteTrack variant @@ -58,24 +53,14 @@ TRACK_BUFFER=60 # Minimum consecutive (or total) hits needed before a track is considered confirmed TRACK_MIN_HITS=3 -# --- ID-switch counting guards --- -# Dedup guard: ignore a second crossing in the same direction within DEDUP_FRAMES -# frames and DEDUP_PX horizontal pixels (set DEDUP_PX=-1 to disable). -DEDUP_FRAMES=15 -DEDUP_PX=60 -# Inheritance guard: new tracks inherit position from a recently-seen nearby track -# (set INHERIT_PX=-1 to disable). -INHERIT_SEC=1.0 -INHERIT_PX=60 - # --- Display --- -# Site name shown on the dashboard header +# Site name shown on the dashboard header (top-right) SITE_NAME=ZenAi # --- Object class names --- # Camera / location identifier shown in HUD and stored in DB CAMERA_NAME=ZenAi -# Label used for grouping in the database +# Label used for batch grouping in the database OBJECT_LABEL=karung # Class name for the counted object (must match model class order) CLASS_OBJECT=karung @@ -83,59 +68,83 @@ CLASS_OBJECT=karung OBJECT_CLASS_ID=0 # --- Line crossing --- -# Line 1 (~upper): counts top-to-down (IN). Line 2 (~lower): bottom-to-up (OUT). -# Fixed y-coordinate overrides the fraction if set. +# 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) LINE_Y1_FRAC=0.70 +# Fixed y-coordinate for line 2 (overrides LINE_Y2_FRAC if set) LINE_Y2= +# Fraction of frame height for line 2 (default 0.66) LINE_Y2_FRAC=0.30 # --- Counting day management --- -# New counting day starts after this time (HH:MM). +# Daily cutoff time (HH:MM) – a new counting day starts after this time and the +# previous day's counter_in / counter_out totals are finalized in the database. +# CUTOFF_TIME is an alias used by the dashboard; DAILY_CUTOFF_TIME takes priority in counter_live_rknn.py. DAILY_CUTOFF_TIME=20:00 CUTOFF_TIME=20:00 # --- CSV export --- +# Write per-crossing events to a CSV file (true/false) EXPORT_CSV=false -CROSS_CSV=/opt/zenai-kpc-counter/crossings.csv - -# --- Crossing snapshots --- -SAVE_CROSS_SNAPSHOT=true -SAVE_DETECT_SNAPSHOT=false -CROSS_SNAPSHOT_DIR=/opt/zenai-kpc-counter/snapshots -CROSS_SNAPSHOT_QUALITY=85 -CROSS_SNAPSHOT_MAX_FILES=1000 -CROSS_SNAPSHOT_MAX_AGE_DAYS=3 -CROSS_SNAPSHOT_CLEANUP_SEC=3600 +# Path where the crossing CSV is written +CROSS_CSV=/tmp/crossings.csv # --- Rate / performance --- +# Enable motion detection pre-filter: skip inference on frames with no movement +# (true/false, default: false). When enabled, frames below MOTION_THRESHOLD are +# skipped, saving NPU/CPU load. MOTION_DETECTION_ENABLED=true -MOTION_PIXEL_DELTA=25 -MOTION_MIN_AREA_FRAC=0.002 -MOTION_HEARTBEAT_FRAMES=15 +# Mean absolute pixel difference threshold (0–255) to consider a frame as having +# motion. Lower = more sensitive. Default 5.0. MOTION_THRESHOLD=5.0 +# Sliding window in seconds for computing the crossing rate (objects/minute) RATE_WINDOW_SEC=60 +# Number of frames to discard at startup to let the stream buffer stabilise WARMUP_FRAMES=30 +# Delay in seconds between stream reconnection attempts RECONNECT_DELAY_SEC=3 +# Maximum reconnection attempts (0 = infinite) MAX_RECONNECT_ATTEMPTS=0 +# Seconds after which a tracked but unseen object is pruned from the active set TRACKED_PRUNE_SEC=300 # --- Video recording --- +# Save annotated frames to segmented MP4 files (true/false) RECORD_VIDEO=false +# Duration in seconds of each video segment file VIDEO_SEGMENT_SEC=3600 +# Output video FPS (fallback if source FPS is unknown or ≤ 1) OUTPUT_FPS=15 # --- Live stream snapshot --- +# Periodically write the latest annotated frame as JPEG for an external web server LIVE_STREAM_ENABLED=true -LIVE_STREAM_FRAME_PATH=/dev/shm/zenai-kpc-counter/live_frame.jpg +# Path to the shared-memory snapshot file (served by nginx / lighttpd) +LIVE_STREAM_FRAME_PATH=/dev/shm/byetrack-counter/live_frame.jpg +# JPEG quality (1–100) LIVE_STREAM_QUALITY=75 +# Write the snapshot every N frames (lower = more frequent updates) LIVE_STREAM_EVERY_N=2 # --- Dashboard (counter_dashboard.py) --- +# Flask secret key for session/cookie signing — change in production! SECRET_KEY=change-me-in-production +# Bind address for the Flask web server DASHBOARD_HOST=0.0.0.0 +# Listen port for the dashboard web UI 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 -# Debug: set DEBUG_TRACKING=true to log per-frame tracking details to stdout -# DEBUG_TRACKING=false +SAVE_CROSS_SNAPSHOT=true +CROSS_SNAPSHOT_DIR=/opt/zenai-kpc-snaps/snapshots +CROSS_SNAPSHOT_CLEANUP_SEC=3600 +CROSS_SNAPSHOT_MAX_AGE_DAYS=3 + +DEDUP_PX=-1 \ No newline at end of file diff --git a/templates/dashboard.html b/templates/dashboard.html index 46e8261..23640bd 100644 --- a/templates/dashboard.html +++ b/templates/dashboard.html @@ -814,9 +814,6 @@ function updateToggleIcon() { let mainChart = null; let videoActive = false; let videoErrorTimer = null; -let videoWatchdogTimer = null; -let lastVideoFrameAt = 0; -const VIDEO_STALL_MS = 8000; document.addEventListener('DOMContentLoaded', () => { loadSummary(); @@ -1140,32 +1137,6 @@ document.addEventListener('keydown', (e) => { // --- Live Video Feed --- -function reconnectVideo() { - if (!videoActive) return; - const img = document.getElementById('live-video-img'); - if (!img) return; - lastVideoFrameAt = Date.now(); - img.src = '/api/live-video?' + Date.now(); -} - -function startVideoWatchdog() { - stopVideoWatchdog(); - lastVideoFrameAt = Date.now(); - videoWatchdogTimer = setInterval(() => { - if (!videoActive) return; - if (Date.now() - lastVideoFrameAt > VIDEO_STALL_MS) { - reconnectVideo(); - } - }, 2000); -} - -function stopVideoWatchdog() { - if (videoWatchdogTimer) { - clearInterval(videoWatchdogTimer); - videoWatchdogTimer = null; - } -} - function toggleVideo() { const btn = document.getElementById('videoToggle'); const img = document.getElementById('live-video-img'); @@ -1176,16 +1147,14 @@ function toggleVideo() { if (videoActive) { resetFilter(); + img.src = '/api/live-video?' + Date.now(); img.style.display = 'block'; if (placeholder) placeholder.style.display = 'none'; btn.textContent = 'STREAM ON'; btn.classList.add('active'); status.innerHTML = ' connecting...'; status.className = 'video-status waiting'; - reconnectVideo(); - startVideoWatchdog(); } else { - stopVideoWatchdog(); img.src = ''; img.style.display = 'none'; if (placeholder) placeholder.style.display = ''; @@ -1198,7 +1167,6 @@ function toggleVideo() { } function onVideoLoad() { - lastVideoFrameAt = Date.now(); const status = document.getElementById('video-status'); status.innerHTML = ' live'; status.className = 'video-status connected'; @@ -1210,7 +1178,10 @@ function onVideoError() { if (overlay) overlay.classList.add('visible'); if (videoErrorTimer) clearTimeout(videoErrorTimer); videoErrorTimer = setTimeout(() => { - reconnectVideo(); + if (videoActive) { + const img = document.getElementById('live-video-img'); + img.src = '/api/live-video?' + Date.now(); + } }, 2000); } @@ -1246,4 +1217,4 @@ function hideVideoError() { - + \ No newline at end of file -- 2.54.0 From 3af0b8188f4931b2d4dea8a6bdc2fa57c4970e07 Mon Sep 17 00:00:00 2001 From: Audrix Date: Tue, 14 Jul 2026 08:38:29 +0700 Subject: [PATCH 4/4] fix small typo --- env.example | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/env.example b/env.example index d0db80e..bf99158 100644 --- a/env.example +++ b/env.example @@ -1,4 +1,4 @@ - # ============================================================================= +# ============================================================================= # Edge RK3588 production counter + dashboard # Shared config for: counter_live_rknn_bytetrack.py + counter_dashboard.py # Copy to .env on device: cp config.env.example .env && nano .env -- 2.54.0