Latest Leveling with python Fix!

This commit is contained in:
proitlab committed 2026-07-06 03:50:58 +07:00
1 parent 5483837769
commit 9ca1f2914e
11 files changed
+561 -172

No files matched your search

+32 -14
View File
@@ -1,17 +1,20 @@
# Edge RK3588 production counter — copy to .env on device # Edge RK3588 production counter — copy to .env on device
# cp .env.example .env && nano .env # cp config.env.example .env && nano .env
OUTPUT_DIR=/opt/bytetrack-counter-cpp #DEBUG_TRACKING=true
DB_PATH=/opt/bytetrack-counter-cpp/bytetrack_counter.db SITE_NAME=Salatiga
OUTPUT_DIR=/opt/bytetrack-counter
DB_PATH=/opt/bytetrack-counter/bytetrack_counter.db
STATE_FILE=/tmp/bytetrack_current_batch.json STATE_FILE=/tmp/bytetrack_current_batch.json
# Direct LAN camera RTSP (low latency) # Direct LAN camera RTSP (low latency)
#SOURCE=rtsp://frigate:zenai@192.168.192.16:8554/camera_stream_640 #SOURCE=rtsp://frigate:zenai@127.0.0.1:8554/camera_stream_640
SOURCE=rtsp://192.168.192.53:8554/my_stream SOURCE=rtsp://10.38.30.64:8554/my_stream
OPENCV_FFMPEG_CAPTURE_OPTIONS=rtsp_transport;tcp|fflags;nobuffer|flags;low_delay OPENCV_FFMPEG_CAPTURE_OPTIONS=rtsp_transport;tcp|fflags;nobuffer|flags;low_delay
# RKNN model — export'd from YOLO with imgsz=320 # RKNN model — export'd from YOLO9t with imgsz=320
MODEL_PATH=/opt/models/zenai_apc_cicalengka_20260609.rknn MODEL_PATH=/opt/models/zenai_apc_salatiga_20260703.rknn
IMGSZ=320 IMGSZ=320
HALF=false HALF=false
CONF=0.3 CONF=0.3
@@ -21,24 +24,31 @@ CORE_MASK=7
# YOLO decoder params (must match model export) # YOLO decoder params (must match model export)
NUM_CLASSES=2 NUM_CLASSES=2
SCORE_SIGMOID=false NUM_KEYPOINTS=9
REG_MAX=16
STRIDES=8,16,32
CAMERA_NAME=CC1 CAMERA_NAME=ZenAi
OBJECT_LABEL=ayam-potong OBJECT_LABEL=ayam-potong
CLASS_AYAM=ayam CLASS_AYAM=ayam
CLASS_TALENAN=telenan CLASS_TALENAN=talenan
# Line crossing: rtl (default) | ltr | both # Line crossing: rtl (default) | ltr | both
CROSS_DIRECTION=ltr CROSS_DIRECTION=rtl
LINE_X= LINE_X=
LINE_X_FRAC=0.5 LINE_X_FRAC=0.5
DAILY_CUTOFF_TIME=17:00 DAILY_CUTOFF_TIME=20:00
CUTOFF_TIME=20:00
BATCH_TIMEOUT_SECONDS=300 BATCH_TIMEOUT_SECONDS=300
IGNORE_BATCH_LABEL_TIMEOUT_SECONDS=30 IGNORE_BATCH_LABEL_TIMEOUT_SECONDS=30
MIN_OBJECT_PER_BATCH=60 MIN_OBJECT_PER_BATCH=60
MIN_DURATION_PER_BATCH=60 MIN_DURATION_PER_BATCH=60
DASHBOARD_HOST=0.0.0.0
DASHBOARD_PORT=5001
SECRET_KEY=change-me-in-production
EXPORT_CSV=true EXPORT_CSV=true
RECORD_VIDEO=false RECORD_VIDEO=false
@@ -47,6 +57,14 @@ LIVE_STREAM_FRAME_PATH=/dev/shm/bytetrack-counter/live_frame.jpg
LIVE_STREAM_QUALITY=75 LIVE_STREAM_QUALITY=75
LIVE_STREAM_EVERY_N=2 LIVE_STREAM_EVERY_N=2
LOG_LEVEL=info # Lower high_thresh to match CONF so all valid detections create tracks
TRACK_HIGH_THRESH_0=0.3
TRACK_HIGH_THRESH_1=0.3
DASHBOARD_PORT=5001 # Relax match_thresh for ayam (0.8 is too strict, IoU must be > 0.8 to keep identity)
TRACK_MATCH_THRESH_0=0.5
TRACK_MATCH_THRESH_1=0.5
# Lower low_thresh below CONF so filtering doesn't interfere
TRACK_LOW_THRESH_0=0.1
TRACK_LOW_THRESH_1=0.1
+1 -1
View File
@@ -10,7 +10,7 @@ set(Eigen3_DIR /usr/share/eigen3/cmake)
find_package(Eigen3 REQUIRED NO_MODULE) find_package(Eigen3 REQUIRED NO_MODULE)
find_package(SQLite3 REQUIRED) find_package(SQLite3 REQUIRED)
option(HAS_RKNN "Build with RKNN NPU support" OFF) option(HAS_RKNN "Build with RKNN NPU support" ON)
if(HAS_RKNN) if(HAS_RKNN)
add_definitions(-DHAS_RKNN) add_definitions(-DHAS_RKNN)
+74
View File
@@ -0,0 +1,74 @@
# ByteTrack Counter C++ — How to Run
## Build
```bash
cd /opt/bytetrack-counter-cpp
mkdir -p build && cd build
cmake .. -DHAS_RKNN=ON
make -j$(nproc)
```
## Run
Run from the project root (looks for `.env` in CWD by default):
```bash
cd /opt/bytetrack-counter-cpp
./build/bytetrack-counter
```
### Specify a custom env file
Pass the path as the first argument:
```bash
./build/bytetrack-counter /path/to/custom.env
```
Or via environment variable:
```bash
ENV_FILE=/path/to/custom.env ./build/bytetrack-counter
```
### Priority
1. CLI argument (if provided)
2. `ENV_FILE` environment variable (if set)
3. `./.env` in the working directory
## Service (systemd)
```bash
sudo systemctl restart bytetrack-counter
journalctl -u bytetrack-counter -f
```
## Environment Variables
See `.env.example` for a full reference. Key variables:
| Variable | Default | Description |
|---|---|---|
| `SOURCE` | `rtsp://user:pass@192.168.0.100:554/stream1` | RTSP camera URL |
| `MODEL_PATH` | `/opt/models/yolo.rknn` | RKNN model file |
| `IMGSZ` | `320` | Model input size |
| `CONF` | `0.3` | Detection confidence threshold |
| `CORE_MASK` | `1` | RKNN NPU core mask (1, 2, 3, 7) |
| `CAMERA_NAME` | `CC1` | Camera ID label |
| `CROSS_DIRECTION` | `rtl` | Line crossing direction: `rtl`, `ltr`, `both` |
| `LINE_X_FRAC` | `0.5` | Counting line position (fraction of frame width) |
| `TRACK_HIGH_THRESH_0` | `0.5` | ByteTrack high threshold (ayam) |
| `TRACK_MATCH_THRESH_0` | `0.8` | ByteTrack match threshold (ayam) |
| `TRACK_HIGH_THRESH_1` | `0.5` | ByteTrack high threshold (talenan) |
| `TRACK_MATCH_THRESH_1` | `0.6` | ByteTrack match threshold (talenan) |
| `AYAM_CLASS_ID` | `0` | YOLO class ID for ayam |
| `TALENAN_CLASS_ID` | `1` | YOLO class ID for talenan |
| `RATE_WINDOW_SEC` | `60` | Rate calculation sliding window (seconds) |
| `RECONNECT_DELAY_SEC` | `3` | Reconnect delay on stream drop / initial connect |
| `MAX_RECONNECT_ATTEMPTS` | `0` | Max reconnect attempts (0 = unlimited) |
| `LIVE_STREAM_ENABLED` | `false` | Write JPEG snapshot for web dashboard |
| `RECORD_VIDEO` | `false` | Record MP4 video segments |
| `OUTPUT_DIR` | `/opt/bytetrack-counter-cpp` | Data output directory |
| `DB_PATH` | `{OUTPUT_DIR}/bytetrack_counter.db` | SQLite database path |
Binary file not shown.
+152
View File
@@ -0,0 +1,152 @@
# =============================================================================
# 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 (logs, DB, video, CSV)
OUTPUT_DIR=/opt/bytetrack-counter
# SQLite database path for batch entries & crossing logs
DB_PATH=/opt/bytetrack-counter/bytetrack_counter.db
# JSON file persisting the current active batch state
STATE_FILE=/tmp/bytetrack_current_batch.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)
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)
DEVICE=0
# --- YOLO decoder ---
# Number of object classes the model outputs (e.g. 2 = ayam + talenan)
NUM_CLASSES=2
# Apply sigmoid to raw class scores (true/false); set true if model head uses BCE logits
SCORE_SIGMOID=false
# --- Detection ---
# Confidence threshold – detections below this are discarded before NMS
CONF=0.3
# --- ByteTrack tracking ---
# General for ayam, index 0
# Detections with score ≥ this get priority matching in the first association stage
TRACK_HIGH_THRESH_0=0.5
# Detections with score between this and TRACK_HIGH_THRESH are matched in the second stage
TRACK_LOW_THRESH_0=0.1
# IoU threshold for the first-stage association (0–1). Higher = stricter overlap required
TRACK_MATCH_THRESH_0=0.8
# Frames a track survives without a match before being permanently removed
TRACK_BUFFER_0=30
# Minimum consecutive (or total) hits needed before a track is considered confirmed
TRACK_MIN_HITS_0=3
# For talenan, index 1
# Detections with score ≥ this get priority matching in the first association stage
TRACK_HIGH_THRESH_1=0.5
# Detections with score between this and TRACK_HIGH_THRESH are matched in the second stage
TRACK_LOW_THRESH_1=0.1
# IoU threshold for the first-stage association (0–1). Higher = stricter overlap required
TRACK_MATCH_THRESH_1=0.6
# Frames a track survives without a match before being permanently removed
TRACK_BUFFER_1=30
# Minimum consecutive (or total) hits needed before a track is considered confirmed
TRACK_MIN_HITS_1=3
# --- 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=ayam-potong
# Class name for the counted object (must match NUM_CLASSES order, index 0)
CLASS_AYAM=ayam
# Class name for the batch-closing trigger object (must match NUM_CLASSES order, index 1)
CLASS_TALENAN=talenan
# --- Line crossing ---
# Direction for counting: rtl (right-to-left, default) | ltr (left-to-right) | both
CROSS_DIRECTION=rtl
# Fixed x-coordinate for the counting line (overrides LINE_X_FRAC if set)
LINE_X=
# Fraction of frame width where the counting line is drawn (default 0.5 = centre)
LINE_X_FRAC=0.5
# --- Batch management ---
# Daily cutoff time (HH:MM) – a new day's batch numbering starts after this time.
# CUTOFF_TIME is an alias used by the dashboard; DAILY_CUTOFF_TIME takes priority in counter_live_rknn_bytetrack.py.
DAILY_CUTOFF_TIME=20:00
CUTOFF_TIME=20:00
# Seconds of inactivity after which the current batch is auto-closed
BATCH_TIMEOUT_SECONDS=300
# Seconds a newly-opened batch ignores the talenan label before accepting a close trigger
IGNORE_BATCH_LABEL_TIMEOUT_SECONDS=30
# Minimum number of objects required for a batch to be saved as valid
MIN_OBJECT_PER_BATCH=60
# Minimum duration in seconds a batch must be open to be saved as valid
MIN_DURATION_PER_BATCH=60
# --- 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/batch_crossings.csv
# --- Rate / performance ---
# 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
# Print status log every N processed frames
FLUSH_EVERY_N_FRAMES=100
# 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_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-batch JSON state file used by the dashboard
CURRENT_BATCH_PATH=/tmp/bytetrack_current_batch.json
+142 -73
View File
@@ -10,6 +10,7 @@ import csv
import os import os
import signal import signal
import time import time
from collections import deque
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
@@ -48,11 +49,18 @@ NUM_CLASSES = int(os.getenv("NUM_CLASSES", "2"))
SCORE_SIGMOID = os.getenv("SCORE_SIGMOID", "false").lower() == "true" SCORE_SIGMOID = os.getenv("SCORE_SIGMOID", "false").lower() == "true"
# ByteTrack settings # ByteTrack settings
TRACK_HIGH_THRESH = float(os.getenv("TRACK_HIGH_THRESH", "0.5")) # Ayam index 0
TRACK_LOW_THRESH = float(os.getenv("TRACK_LOW_THRESH", "0.1")) TRACK_HIGH_THRESH_0 = float(os.getenv("TRACK_HIGH_THRESH_0", "0.5"))
TRACK_MATCH_THRESH = float(os.getenv("TRACK_MATCH_THRESH", "0.8")) TRACK_LOW_THRESH_0 = float(os.getenv("TRACK_LOW_THRESH_0", "0.1"))
TRACK_BUFFER = int(os.getenv("TRACK_BUFFER", "30")) TRACK_MATCH_THRESH_0 = float(os.getenv("TRACK_MATCH_THRESH_0", "0.8"))
TRACK_MIN_HITS = int(os.getenv("TRACK_MIN_HITS", "3")) TRACK_BUFFER_0 = int(os.getenv("TRACK_BUFFER_0", "30"))
TRACK_MIN_HITS_0 = int(os.getenv("TRACK_MIN_HITS_0", "3"))
# Talenan index 1
TRACK_HIGH_THRESH_1 = float(os.getenv("TRACK_HIGH_THRESH_1", "0.5"))
TRACK_LOW_THRESH_1 = float(os.getenv("TRACK_LOW_THRESH_1", "0.1"))
TRACK_MATCH_THRESH_1 = float(os.getenv("TRACK_MATCH_THRESH_1", "0.6"))
TRACK_BUFFER_1 = int(os.getenv("TRACK_BUFFER_1", "30"))
TRACK_MIN_HITS_1 = int(os.getenv("TRACK_MIN_HITS_1", "3"))
DAILY_CUTOFF_TIME = os.getenv("DAILY_CUTOFF_TIME", "20:00") DAILY_CUTOFF_TIME = os.getenv("DAILY_CUTOFF_TIME", "20:00")
BATCH_TIMEOUT_SECONDS = float(os.getenv("BATCH_TIMEOUT_SECONDS", "300")) BATCH_TIMEOUT_SECONDS = float(os.getenv("BATCH_TIMEOUT_SECONDS", "300"))
@@ -65,6 +73,7 @@ MIN_DURATION_PER_BATCH = int(os.getenv("MIN_DURATION_PER_BATCH", "60"))
EXPORT_CSV = os.getenv("EXPORT_CSV", "true").lower() == "true" EXPORT_CSV = os.getenv("EXPORT_CSV", "true").lower() == "true"
CROSS_CSV = os.getenv("CROSS_CSV", f"{OUTPUT_DIR}/batch_crossings.csv") CROSS_CSV = os.getenv("CROSS_CSV", f"{OUTPUT_DIR}/batch_crossings.csv")
RATE_WINDOW_SEC = int(os.getenv("RATE_WINDOW_SEC", "60"))
WARMUP_FRAMES = int(os.getenv("WARMUP_FRAMES", "30")) WARMUP_FRAMES = int(os.getenv("WARMUP_FRAMES", "30"))
RECONNECT_DELAY_SEC = int(os.getenv("RECONNECT_DELAY_SEC", "3")) RECONNECT_DELAY_SEC = int(os.getenv("RECONNECT_DELAY_SEC", "3"))
MAX_RECONNECT_ATTEMPTS = int(os.getenv("MAX_RECONNECT_ATTEMPTS", "0")) MAX_RECONNECT_ATTEMPTS = int(os.getenv("MAX_RECONNECT_ATTEMPTS", "0"))
@@ -345,11 +354,13 @@ class ByteTracker:
# --- separate detections by score --- # --- separate detections by score ---
if len(boxes_xyxy) > 0: if len(boxes_xyxy) > 0:
remain = scores > self.low_thresh remain = scores > self.low_thresh
remain_orig_idx = np.where(remain)[0]
dets = boxes_xyxy[remain] dets = boxes_xyxy[remain]
det_scores = scores[remain] det_scores = scores[remain]
is_high = det_scores > self.high_thresh is_high = det_scores > self.high_thresh
is_low = ~is_high is_low = ~is_high
else: else:
remain_orig_idx = np.zeros(0, dtype=np.int64)
dets = np.zeros((0, 4), dtype=np.float32) dets = np.zeros((0, 4), dtype=np.float32)
det_scores = np.zeros(0, dtype=np.float32) det_scores = np.zeros(0, dtype=np.float32)
is_high = np.zeros(0, dtype=bool) is_high = np.zeros(0, dtype=bool)
@@ -389,10 +400,11 @@ class ByteTracker:
for di, ti in matches: for di, ti in matches:
det_global = int(high_idx[di]) det_global = int(high_idx[di])
orig_idx = int(remain_orig_idx[det_global])
track_pool[ti].update(dets[det_global]) track_pool[ti].update(dets[det_global])
track_pool[ti].hit_streak = max(1, track_pool[ti].hit_streak) track_pool[ti].hit_streak = max(1, track_pool[ti].hit_streak)
matched_track_idx.add(ti) matched_track_idx.add(ti)
det_to_track[det_global] = track_pool[ti].track_id det_to_track[orig_idx] = track_pool[ti].track_id
tracked_map[track_pool[ti].track_id] = track_pool[ti].get_cx() tracked_map[track_pool[ti].track_id] = track_pool[ti].get_cx()
match_pairs_high.append((det_global, ti)) match_pairs_high.append((det_global, ti))
@@ -408,17 +420,20 @@ class ByteTracker:
unmatched_boxes = track_boxes[unmatched_tracks] unmatched_boxes = track_boxes[unmatched_tracks]
iou_mat = _ious_xyxy(low_dets, unmatched_boxes) iou_mat = _ious_xyxy(low_dets, unmatched_boxes)
cost_mat = 1.0 - iou_mat cost_mat = 1.0 - iou_mat
matches2 = _greedy_match(cost_mat, threshold=0.5) matches2 = _greedy_match(
cost_mat, threshold=1.0 - self.match_thresh
)
for di, uti in matches2: for di, uti in matches2:
det_global = int(low_idx[di]) det_global = int(low_idx[di])
pool_idx = unmatched_tracks[uti] pool_idx = unmatched_tracks[uti]
orig_idx = int(remain_orig_idx[det_global])
track_pool[pool_idx].update(dets[det_global]) track_pool[pool_idx].update(dets[det_global])
track_pool[pool_idx].hit_streak = max( track_pool[pool_idx].hit_streak = max(
1, track_pool[pool_idx].hit_streak 1, track_pool[pool_idx].hit_streak
) )
matched_track_idx.add(pool_idx) matched_track_idx.add(pool_idx)
det_to_track[det_global] = track_pool[pool_idx].track_id det_to_track[orig_idx] = track_pool[pool_idx].track_id
tracked_map[track_pool[pool_idx].track_id] = track_pool[ tracked_map[track_pool[pool_idx].track_id] = track_pool[
pool_idx pool_idx
].get_cx() ].get_cx()
@@ -454,10 +469,11 @@ class ByteTracker:
high_all = np.where(is_high)[0] high_all = np.where(is_high)[0]
matched_det_ids = set(det_to_track.keys()) matched_det_ids = set(det_to_track.keys())
for dg in high_all: for dg in high_all:
if int(dg) not in matched_det_ids: orig_idx = int(remain_orig_idx[int(dg)])
trk = KalmanBoxTracker(dets[dg]) if orig_idx not in matched_det_ids:
trk = KalmanBoxTracker(dets[int(dg)])
self.tracked_tracks.append(trk) self.tracked_tracks.append(trk)
det_to_track[int(dg)] = trk.track_id det_to_track[orig_idx] = trk.track_id
tracked_map[trk.track_id] = trk.get_cx() tracked_map[trk.track_id] = trk.get_cx()
return tracked_map, det_to_track, lost_map return tracked_map, det_to_track, lost_map
@@ -749,61 +765,59 @@ def draw_hero_count(img, line_x, h, count, pulse_remaining=0):
text = str(count) text = str(count)
font = cv2.FONT_HERSHEY_SIMPLEX font = cv2.FONT_HERSHEY_SIMPLEX
boost = 0.35 * (pulse_remaining / max(COUNT_PULSE_FRAMES, 1)) boost = 0.35 * (pulse_remaining / max(COUNT_PULSE_FRAMES, 1))
font_scale, thickness = 1.6 + boost, 3 font_scale, thickness = 1.4 + boost, 3
(tw, th), _ = cv2.getTextSize(text, font, font_scale, thickness) (tw, th), _ = cv2.getTextSize(text, font, font_scale, thickness)
pad = 14 pad = 14
tx, ty = line_x - tw // 2, h // 2 + th // 2 tx, ty = line_x - tw // 2, h // 3 + th // 2
overlay_rect( overlay_rect(
img, tx - pad, ty - th - pad, tx + tw + pad, ty + pad // 2, C_PANEL, alpha=0.78 img, tx - pad, ty - th - pad, tx + tw + pad, ty + pad, C_PANEL, alpha=0.78
) )
cv2.rectangle( cv2.rectangle(
img, (tx - pad, ty - th - pad), (tx + tw + pad, ty + pad // 2), C_LINE_CORE, 2 img, (tx - pad, ty - th - pad), (tx + tw + pad, ty + pad), C_LINE_CORE, 2
) )
cv2.putText(img, text, (tx, ty), font, font_scale, C_GREEN, thickness, cv2.LINE_AA) cv2.putText(img, text, (tx, ty), font, font_scale, C_GREEN, thickness, cv2.LINE_AA)
def draw_hud( def draw_hud(img, w, batch_num, batch_count, total_ayam, elapsed_sec, rate):
img, w, batch_num, batch_count, total_ayam, elapsed_sec, rate, camera_id, clock bar_h = 40
):
bar_h = 52
overlay_rect(img, 0, 0, w, bar_h, C_PANEL, alpha=0.72) overlay_rect(img, 0, 0, w, bar_h, C_PANEL, alpha=0.72)
cv2.line(img, (0, bar_h), (w, bar_h), C_BORDER, 1) cv2.line(img, (0, bar_h), (w, bar_h), C_BORDER, 1)
cv2.putText( cv2.putText(
img, "BATCH", (16, 20), cv2.FONT_HERSHEY_SIMPLEX, 0.45, C_MUTED, 1, cv2.LINE_AA img, "BATCH", (14, 14), cv2.FONT_HERSHEY_SIMPLEX, 0.32, C_MUTED, 1, cv2.LINE_AA
) )
batch_label = str(batch_num) if batch_num else "\u2014" batch_label = str(batch_num) if batch_num else "--"
cv2.putText( cv2.putText(
img, img,
batch_label, batch_label,
(16, 44), (14, 32),
cv2.FONT_HERSHEY_SIMPLEX, cv2.FONT_HERSHEY_SIMPLEX,
0.9, 0.55,
C_ACCENT, C_ACCENT,
2, 1,
cv2.LINE_AA, cv2.LINE_AA,
) )
cv2.putText( cv2.putText(
img, "COUNT", (100, 20), cv2.FONT_HERSHEY_SIMPLEX, 0.45, C_MUTED, 1, cv2.LINE_AA img, "COUNT", (90, 14), cv2.FONT_HERSHEY_SIMPLEX, 0.32, C_MUTED, 1, cv2.LINE_AA
) )
cv2.putText( cv2.putText(
img, img,
str(batch_count), str(batch_count),
(100, 44), (90, 32),
cv2.FONT_HERSHEY_SIMPLEX, cv2.FONT_HERSHEY_SIMPLEX,
0.9, 0.55,
C_GREEN, C_GREEN,
2, 1,
cv2.LINE_AA, cv2.LINE_AA,
) )
cv2.putText( cv2.putText(
img, "TOTAL", (190, 20), cv2.FONT_HERSHEY_SIMPLEX, 0.45, C_MUTED, 1, cv2.LINE_AA img, "TOTAL", (170, 14), cv2.FONT_HERSHEY_SIMPLEX, 0.32, C_MUTED, 1, cv2.LINE_AA
) )
cv2.putText( cv2.putText(
img, img,
str(total_ayam), str(total_ayam),
(190, 44), (170, 32),
cv2.FONT_HERSHEY_SIMPLEX, cv2.FONT_HERSHEY_SIMPLEX,
0.7, 0.45,
C_TEXT, C_TEXT,
1, 1,
cv2.LINE_AA, cv2.LINE_AA,
@@ -811,9 +825,9 @@ def draw_hud(
cv2.putText( cv2.putText(
img, img,
"UPTIME", "UPTIME",
(280, 20), (250, 14),
cv2.FONT_HERSHEY_SIMPLEX, cv2.FONT_HERSHEY_SIMPLEX,
0.45, 0.32,
C_MUTED, C_MUTED,
1, 1,
cv2.LINE_AA, cv2.LINE_AA,
@@ -821,45 +835,34 @@ def draw_hud(
cv2.putText( cv2.putText(
img, img,
f"{elapsed_sec / 3600:.1f}h", f"{elapsed_sec / 3600:.1f}h",
(280, 44), (250, 32),
cv2.FONT_HERSHEY_SIMPLEX, cv2.FONT_HERSHEY_SIMPLEX,
0.7, 0.45,
C_TEXT, C_TEXT,
1, 1,
cv2.LINE_AA, cv2.LINE_AA,
) )
cv2.putText( cv2.putText(
img, "RATE", (380, 20), cv2.FONT_HERSHEY_SIMPLEX, 0.45, C_MUTED, 1, cv2.LINE_AA img, "RATE", (340, 14), cv2.FONT_HERSHEY_SIMPLEX, 0.32, C_MUTED, 1, cv2.LINE_AA
) )
cv2.putText( cv2.putText(
img, img,
f"{rate:.1f}/min", f"{rate:.1f}/min",
(380, 44), (340, 32),
cv2.FONT_HERSHEY_SIMPLEX, cv2.FONT_HERSHEY_SIMPLEX,
0.7, 0.45,
C_ACCENT, C_ACCENT,
1, 1,
cv2.LINE_AA, cv2.LINE_AA,
) )
# cv2.putText(img, clock, (w - 180, 36), cv2.FONT_HERSHEY_SIMPLEX, 0.55, C_TEXT, 1, cv2.LINE_AA)
cv2.putText(
img,
f"CAM {camera_id}",
(w - 180, 20),
cv2.FONT_HERSHEY_SIMPLEX,
0.45,
C_MUTED,
1,
cv2.LINE_AA,
)
def draw_footer(img, w, h, frame_idx, live_tag): def draw_footer(img, w, h, frame_idx, live_tag, inf_ms=0.0, model_name=""):
bar_h = 28 bar_h = 28
overlay_rect(img, 0, h - bar_h, w, h, C_PANEL, alpha=0.55) overlay_rect(img, 0, h - bar_h, w, h, C_PANEL, alpha=0.55)
cv2.putText( cv2.putText(
img, img,
f"{live_tag} | Frame {frame_idx}", f"{live_tag} | {model_name} | Frame {frame_idx} | Inf {inf_ms:.1f}ms",
(12, h - 9), (12, h - 9),
cv2.FONT_HERSHEY_SIMPLEX, cv2.FONT_HERSHEY_SIMPLEX,
0.45, 0.45,
@@ -982,26 +985,22 @@ def run():
score_sigmoid=SCORE_SIGMOID, score_sigmoid=SCORE_SIGMOID,
) )
CLASS_IDS = { ayam_cls = int(os.getenv("AYAM_CLASS_ID", "0"))
os.getenv("CLASS_AYAM", "ayam"): 0, talenan_cls = int(os.getenv("TALENAN_CLASS_ID", "1"))
os.getenv("CLASS_TALENAN", "talenan"): 1,
}
ayam_cls = CLASS_IDS[CLASS_AYAM]
talenan_cls = CLASS_IDS[CLASS_TALENAN]
ayam_tracker = ByteTracker( ayam_tracker = ByteTracker(
track_high_thresh=TRACK_HIGH_THRESH, track_high_thresh=TRACK_HIGH_THRESH_0,
track_low_thresh=TRACK_LOW_THRESH, track_low_thresh=TRACK_LOW_THRESH_0,
match_thresh=TRACK_MATCH_THRESH, match_thresh=TRACK_MATCH_THRESH_0,
track_buffer=TRACK_BUFFER, track_buffer=TRACK_BUFFER_0,
min_hits=TRACK_MIN_HITS, min_hits=TRACK_MIN_HITS_0,
) )
talenan_tracker = ByteTracker( talenan_tracker = ByteTracker(
track_high_thresh=TRACK_HIGH_THRESH, track_high_thresh=TRACK_HIGH_THRESH_1,
track_low_thresh=TRACK_LOW_THRESH, track_low_thresh=TRACK_LOW_THRESH_1,
match_thresh=TRACK_MATCH_THRESH, match_thresh=TRACK_MATCH_THRESH_1,
track_buffer=TRACK_BUFFER, track_buffer=TRACK_BUFFER_1,
min_hits=TRACK_MIN_HITS, min_hits=TRACK_MIN_HITS_1,
) )
ayam_line_crossed = set() ayam_line_crossed = set()
@@ -1014,7 +1013,9 @@ def run():
session_start = time.time() session_start = time.time()
frame_idx = 0 frame_idx = 0
inf_ms = 0.0
video_writer = None video_writer = None
crossing_times = deque()
cap, w, h, fps = connect_stream(SOURCE) cap, w, h, fps = connect_stream(SOURCE)
if cap is None: if cap is None:
@@ -1028,8 +1029,12 @@ def run():
) )
print(f"Model: {MODEL_PATH} | imgsz={IMGSZ} | core_mask={CORE_MASK}") print(f"Model: {MODEL_PATH} | imgsz={IMGSZ} | core_mask={CORE_MASK}")
print( print(
f"ByteTrack: high_thresh={TRACK_HIGH_THRESH} low_thresh={TRACK_LOW_THRESH} " f"ByteTrack Index 0: high_thresh={TRACK_HIGH_THRESH_0} low_thresh={TRACK_LOW_THRESH_0} "
f"match_thresh={TRACK_MATCH_THRESH} buffer={TRACK_BUFFER}" f"match_thresh={TRACK_MATCH_THRESH_0} buffer={TRACK_BUFFER_0}"
)
print(
f"ByteTrack Index 1: high_thresh={TRACK_HIGH_THRESH_1} low_thresh={TRACK_LOW_THRESH_1} "
f"match_thresh={TRACK_MATCH_THRESH_1} buffer={TRACK_BUFFER_1}"
) )
print(f"DB: {DB_PATH}") print(f"DB: {DB_PATH}")
print(f"State: {STATE_FILE}") print(f"State: {STATE_FILE}")
@@ -1061,7 +1066,9 @@ def run():
mono = time.monotonic() mono = time.monotonic()
ayam_crossed_frame = batch_closed_frame = batch_started_frame = False ayam_crossed_frame = batch_closed_frame = batch_started_frame = False
inf_start = time.time()
detections = model(frame) detections = model(frame)
inf_ms = inf_ms * 0.9 + (time.time() - inf_start) * 1000 * 0.1
if detections: if detections:
ayam_boxes_xyxy = [] ayam_boxes_xyxy = []
@@ -1104,6 +1111,56 @@ def run():
talenan_tracker.update(talenan_boxes_xyxy, talenan_scores) talenan_tracker.update(talenan_boxes_xyxy, talenan_scores)
) )
if os.getenv("DEBUG_TRACKING", "").lower() == "true":
if len(ayam_boxes_xyxy) > 0 or len(talenan_boxes_xyxy) > 0:
ayam_scores_str = (
f" ayam scores: {ayam_scores.round(3).tolist()}"
if len(ayam_scores) > 0
else ""
)
talenan_scores_str = (
f" talenan scores: {talenan_scores.round(3).tolist()}"
if len(talenan_scores) > 0
else ""
)
ayam_tracks_str = (
f" ayam det→track: {dict(ayam_det_to_track)}"
if ayam_det_to_track
else ""
)
talenan_tracks_str = (
f" talenan det→track: {dict(talenan_det_to_track)}"
if talenan_det_to_track
else ""
)
crossing_str = (
f" ayam_line_crossed: {sorted(ayam_line_crossed)}"
if ayam_line_crossed
else ""
)
# Show cx values for one tracked object to check movement
cx_sample = ""
if len(ayam_cx_list) > 0 and ayam_det_to_track:
sample_tid = list(ayam_det_to_track.values())[0]
sample_cx = ayam_cx_list[
list(ayam_det_to_track.keys())[0]
]
prev = (
ayam_tracked.get(sample_tid, (None,))[0]
if ayam_tracked.get(sample_tid)
else None
)
cx_sample = (
f" sample tid={sample_tid} prev_cx={prev} cx={sample_cx:.1f}"
)
print(
f"[DEBUG F{frame_idx}] ayam_dets={len(ayam_boxes_xyxy)} "
f"talenan_dets={len(talenan_boxes_xyxy)} "
f"line_x={line_x}{ayam_scores_str}{talenan_scores_str}"
f"{ayam_tracks_str}{talenan_tracks_str}{crossing_str}"
f"{cx_sample}"
)
# Process talenan crossings # Process talenan crossings
for di in range(len(talenan_boxes_xyxy)): for di in range(len(talenan_boxes_xyxy)):
tid = talenan_det_to_track.get(di) tid = talenan_det_to_track.get(di)
@@ -1145,6 +1202,11 @@ def run():
crossed_line(prev_cx, cx, line_x) crossed_line(prev_cx, cx, line_x)
and tid not in ayam_line_crossed and tid not in ayam_line_crossed
): ):
if os.getenv("DEBUG_TRACKING", "").lower() == "true":
print(
f"[DEBUG F{frame_idx}] CROSS DETECTED: tid={tid} "
f"prev_cx={prev_cx:.1f} → cx={cx:.1f} line_x={line_x}"
)
ayam_line_crossed.add(tid) ayam_line_crossed.add(tid)
_, started_new = store.record_ayam_crossing(tid) _, started_new = store.record_ayam_crossing(tid)
if cross_logger: if cross_logger:
@@ -1159,6 +1221,7 @@ def run():
ayam_crossed_frame = True ayam_crossed_frame = True
if started_new: if started_new:
batch_started_frame = True batch_started_frame = True
crossing_times.append(mono)
ayam_cross_flash[tid] = CROSS_FLASH_FRAMES ayam_cross_flash[tid] = CROSS_FLASH_FRAMES
popups.append( popups.append(
{ {
@@ -1216,7 +1279,9 @@ def run():
batch_num = store.current_batch_number or 0 batch_num = store.current_batch_number or 0
batch_count = store.current_batch_count batch_count = store.current_batch_count
display_total = store.display_total() display_total = store.display_total()
rate = (display_total / elapsed * 60) if elapsed > 0 else 0.0 while crossing_times and mono - crossing_times[0] > RATE_WINDOW_SEC:
crossing_times.popleft()
rate = (len(crossing_times) / RATE_WINDOW_SEC * 60) if crossing_times else 0.0
draw_elegant_counting_line(frame, line_x, h, line_pulse) draw_elegant_counting_line(frame, line_x, h, line_pulse)
draw_hero_count(frame, line_x, h, batch_count, count_pulse) draw_hero_count(frame, line_x, h, batch_count, count_pulse)
@@ -1228,12 +1293,16 @@ def run():
display_total, display_total,
elapsed, elapsed,
rate, rate,
CAMERA_NAME,
now_str(),
) )
draw_batch_banner(frame, w, batch_num, batch_pulse) draw_batch_banner(frame, w, batch_num, batch_pulse)
draw_footer( draw_footer(
frame, w, h, frame_idx, "LIVE-RKNN-BT" if IS_LIVE else "FILE-RKNN-BT" frame,
w,
h,
frame_idx,
"LIVE" if IS_LIVE else "FILE",
inf_ms,
Path(MODEL_PATH).name,
) )
popups = draw_popups(frame, popups, frame_idx) popups = draw_popups(frame, popups, frame_idx)
+28 -10
View File
@@ -73,17 +73,35 @@ void AppConfig::load_from_env(const char* env_path) {
if (!nc_s.empty()) num_classes = std::stoi(nc_s); if (!nc_s.empty()) num_classes = std::stoi(nc_s);
auto ss_s = get_env("SCORE_SIGMOID"); auto ss_s = get_env("SCORE_SIGMOID");
score_sigmoid = (ss_s == "true"); score_sigmoid = (ss_s == "true");
auto ac_s = get_env("AYAM_CLASS_ID");
if (!ac_s.empty()) ayam_class_id = std::stoi(ac_s);
auto tc_s = get_env("TALENAN_CLASS_ID");
if (!tc_s.empty()) talenan_class_id = std::stoi(tc_s);
auto th_s = get_env("TRACK_HIGH_THRESH"); auto th0_s = get_env("TRACK_HIGH_THRESH_0");
if (!th_s.empty()) track_high_thresh = std::stof(th_s); if (!th0_s.empty()) track_high_thresh_0 = std::stof(th0_s);
auto tl_s = get_env("TRACK_LOW_THRESH"); auto tl0_s = get_env("TRACK_LOW_THRESH_0");
if (!tl_s.empty()) track_low_thresh = std::stof(tl_s); if (!tl0_s.empty()) track_low_thresh_0 = std::stof(tl0_s);
auto tm_s = get_env("TRACK_MATCH_THRESH"); auto tm0_s = get_env("TRACK_MATCH_THRESH_0");
if (!tm_s.empty()) track_match_thresh = std::stof(tm_s); if (!tm0_s.empty()) track_match_thresh_0 = std::stof(tm0_s);
auto tb_s = get_env("TRACK_BUFFER"); auto tb0_s = get_env("TRACK_BUFFER_0");
if (!tb_s.empty()) track_buffer = std::stoi(tb_s); if (!tb0_s.empty()) track_buffer_0 = std::stoi(tb0_s);
auto tmh_s = get_env("TRACK_MIN_HITS"); auto tmh0_s = get_env("TRACK_MIN_HITS_0");
if (!tmh_s.empty()) track_min_hits = std::stoi(tmh_s); if (!tmh0_s.empty()) track_min_hits_0 = std::stoi(tmh0_s);
auto th1_s = get_env("TRACK_HIGH_THRESH_1");
if (!th1_s.empty()) track_high_thresh_1 = std::stof(th1_s);
auto tl1_s = get_env("TRACK_LOW_THRESH_1");
if (!tl1_s.empty()) track_low_thresh_1 = std::stof(tl1_s);
auto tm1_s = get_env("TRACK_MATCH_THRESH_1");
if (!tm1_s.empty()) track_match_thresh_1 = std::stof(tm1_s);
auto tb1_s = get_env("TRACK_BUFFER_1");
if (!tb1_s.empty()) track_buffer_1 = std::stoi(tb1_s);
auto tmh1_s = get_env("TRACK_MIN_HITS_1");
if (!tmh1_s.empty()) track_min_hits_1 = std::stoi(tmh1_s);
auto rw_s = get_env("RATE_WINDOW_SEC");
if (!rw_s.empty()) rate_window_sec = std::stoi(rw_s);
daily_cutoff_time = get_env("DAILY_CUTOFF_TIME", daily_cutoff_time); daily_cutoff_time = get_env("DAILY_CUTOFF_TIME", daily_cutoff_time);
auto bto_s = get_env("BATCH_TIMEOUT_SECONDS"); auto bto_s = get_env("BATCH_TIMEOUT_SECONDS");
+15 -5
View File
@@ -24,12 +24,22 @@ struct AppConfig {
int core_mask = 1; int core_mask = 1;
int num_classes = 2; int num_classes = 2;
bool score_sigmoid = false; bool score_sigmoid = false;
int ayam_class_id = 0;
int talenan_class_id = 1;
float track_high_thresh = 0.5f; float track_high_thresh_0 = 0.5f;
float track_low_thresh = 0.1f; float track_low_thresh_0 = 0.1f;
float track_match_thresh = 0.8f; float track_match_thresh_0 = 0.8f;
int track_buffer = 30; int track_buffer_0 = 30;
int track_min_hits = 3; int track_min_hits_0 = 3;
float track_high_thresh_1 = 0.5f;
float track_low_thresh_1 = 0.1f;
float track_match_thresh_1 = 0.6f;
int track_buffer_1 = 30;
int track_min_hits_1 = 3;
int rate_window_sec = 60;
std::string daily_cutoff_time = "20:00"; std::string daily_cutoff_time = "20:00";
float batch_timeout_seconds = 300.0f; float batch_timeout_seconds = 300.0f;
+24 -25
View File
@@ -9,8 +9,8 @@ const std::vector<std::pair<int, int>> SKELETON = {
}; };
const std::vector<cv::Scalar> SK_COLORS = { const std::vector<cv::Scalar> SK_COLORS = {
{255, 255, 0}, {255, 255, 0}, {255, 0, 255}, {255, 0, 255}, {0, 255, 255}, {0, 255, 255}, {255, 0, 255}, {255, 0, 255},
{0, 255, 0}, {255, 255, 0}, {255, 0, 0}, {0, 200, 200} {0, 255, 0}, {255, 255, 0}, {0, 0, 255}, {200, 200, 0}
}; };
int resolve_line_x(int frame_width, int line_x_opt, float line_x_frac) { int resolve_line_x(int frame_width, int line_x_opt, float line_x_frac) {
@@ -68,54 +68,53 @@ void draw_elegant_counting_line(cv::Mat& img, int line_x, int h, int pulse_remai
void draw_hero_count(cv::Mat& img, int line_x, int h, int count, int pulse_remaining) { void draw_hero_count(cv::Mat& img, int line_x, int h, int count, int pulse_remaining) {
std::string text = std::to_string(count); std::string text = std::to_string(count);
float boost = 0.35f * (pulse_remaining / std::max(15.0f, 1.0f)); float boost = 0.35f * (pulse_remaining / std::max(15.0f, 1.0f));
double font_scale = 1.6 + boost; double font_scale = 1.4 + boost;
int thickness = static_cast<int>(3 + boost); int thickness = static_cast<int>(3 + boost);
int baseline = 0; int baseline = 0;
cv::Size ts = cv::getTextSize(text, cv::FONT_HERSHEY_SIMPLEX, font_scale, thickness, &baseline); cv::Size ts = cv::getTextSize(text, cv::FONT_HERSHEY_SIMPLEX, font_scale, thickness, &baseline);
int pad = 14; int pad = 14;
int tx = line_x - ts.width / 2; int tx = line_x - ts.width / 2;
int ty = h / 2 + ts.height / 2; int ty = h / 3 + ts.height / 2;
overlay_rect(img, tx - pad, ty - ts.height - pad, tx + ts.width + pad, ty + pad / 2, C_PANEL, 0.78f); overlay_rect(img, tx - pad, ty - ts.height - pad, tx + ts.width + pad, ty + pad, C_PANEL, 0.78f);
cv::rectangle(img, cv::Rect(tx - pad, ty - ts.height - pad, cv::rectangle(img, cv::Rect(tx - pad, ty - ts.height - pad,
ts.width + pad * 2, ts.height + pad + pad / 2), ts.width + pad * 2, ts.height + pad * 2),
C_LINE_CORE, 2); C_LINE_CORE, 2);
cv::putText(img, text, cv::Point(tx, ty), cv::FONT_HERSHEY_SIMPLEX, font_scale, C_GREEN, thickness, cv::LINE_AA); cv::putText(img, text, cv::Point(tx, ty), cv::FONT_HERSHEY_SIMPLEX, font_scale, C_GREEN, thickness, cv::LINE_AA);
} }
void draw_hud(cv::Mat& img, int w, int batch_num, int batch_count, int total_ayam, void draw_hud(cv::Mat& img, int w, int batch_num, int batch_count, int total_ayam,
double elapsed_sec, double rate, const std::string& camera_id, const std::string& clock) { double elapsed_sec, double rate) {
int bar_h = 52; int bar_h = 40;
overlay_rect(img, 0, 0, w, bar_h, C_PANEL, 0.72f); overlay_rect(img, 0, 0, w, bar_h, C_PANEL, 0.72f);
cv::line(img, cv::Point(0, bar_h), cv::Point(w, bar_h), C_BORDER, 1); cv::line(img, cv::Point(0, bar_h), cv::Point(w, bar_h), C_BORDER, 1);
cv::putText(img, "BATCH", cv::Point(16, 20), cv::FONT_HERSHEY_SIMPLEX, 0.45, C_MUTED, 1, cv::LINE_AA); cv::putText(img, "BATCH", cv::Point(14, 14), cv::FONT_HERSHEY_SIMPLEX, 0.32, C_MUTED, 1, cv::LINE_AA);
std::string batch_label = batch_num ? std::to_string(batch_num) : "\xe2\x80\x94"; std::string batch_label = batch_num ? std::to_string(batch_num) : "--";
cv::putText(img, batch_label, cv::Point(16, 44), cv::FONT_HERSHEY_SIMPLEX, 0.9, C_ACCENT, 2, cv::LINE_AA); cv::putText(img, batch_label, cv::Point(14, 32), cv::FONT_HERSHEY_SIMPLEX, 0.55, C_ACCENT, 1, cv::LINE_AA);
cv::putText(img, "COUNT", cv::Point(100, 20), cv::FONT_HERSHEY_SIMPLEX, 0.45, C_MUTED, 1, cv::LINE_AA); cv::putText(img, "COUNT", cv::Point(90, 14), cv::FONT_HERSHEY_SIMPLEX, 0.32, C_MUTED, 1, cv::LINE_AA);
cv::putText(img, std::to_string(batch_count), cv::Point(100, 44), cv::FONT_HERSHEY_SIMPLEX, 0.9, C_GREEN, 2, cv::LINE_AA); cv::putText(img, std::to_string(batch_count), cv::Point(90, 32), cv::FONT_HERSHEY_SIMPLEX, 0.55, C_GREEN, 1, cv::LINE_AA);
cv::putText(img, "TOTAL", cv::Point(190, 20), cv::FONT_HERSHEY_SIMPLEX, 0.45, C_MUTED, 1, cv::LINE_AA); cv::putText(img, "TOTAL", cv::Point(170, 14), cv::FONT_HERSHEY_SIMPLEX, 0.32, C_MUTED, 1, cv::LINE_AA);
cv::putText(img, std::to_string(total_ayam), cv::Point(190, 44), cv::FONT_HERSHEY_SIMPLEX, 0.7, C_TEXT, 1, cv::LINE_AA); cv::putText(img, std::to_string(total_ayam), cv::Point(170, 32), cv::FONT_HERSHEY_SIMPLEX, 0.45, C_TEXT, 1, cv::LINE_AA);
cv::putText(img, "UPTIME", cv::Point(280, 20), cv::FONT_HERSHEY_SIMPLEX, 0.45, C_MUTED, 1, cv::LINE_AA); cv::putText(img, "UPTIME", cv::Point(250, 14), cv::FONT_HERSHEY_SIMPLEX, 0.32, C_MUTED, 1, cv::LINE_AA);
char buf[32]; char buf[32];
snprintf(buf, sizeof(buf), "%.1fh", elapsed_sec / 3600.0); snprintf(buf, sizeof(buf), "%.1fh", elapsed_sec / 3600.0);
cv::putText(img, buf, cv::Point(280, 44), cv::FONT_HERSHEY_SIMPLEX, 0.7, C_TEXT, 1, cv::LINE_AA); cv::putText(img, buf, cv::Point(250, 32), cv::FONT_HERSHEY_SIMPLEX, 0.45, C_TEXT, 1, cv::LINE_AA);
cv::putText(img, "RATE", cv::Point(380, 20), cv::FONT_HERSHEY_SIMPLEX, 0.45, C_MUTED, 1, cv::LINE_AA); cv::putText(img, "RATE", cv::Point(340, 14), cv::FONT_HERSHEY_SIMPLEX, 0.32, C_MUTED, 1, cv::LINE_AA);
snprintf(buf, sizeof(buf), "%.1f/min", rate); snprintf(buf, sizeof(buf), "%.1f/min", rate);
cv::putText(img, buf, cv::Point(380, 44), cv::FONT_HERSHEY_SIMPLEX, 0.7, C_ACCENT, 1, cv::LINE_AA); cv::putText(img, buf, cv::Point(340, 32), cv::FONT_HERSHEY_SIMPLEX, 0.45, C_ACCENT, 1, cv::LINE_AA);
std::string cam_label = "CAM " + camera_id;
cv::putText(img, cam_label, cv::Point(w - 180, 20), cv::FONT_HERSHEY_SIMPLEX, 0.45, C_MUTED, 1, cv::LINE_AA);
} }
void draw_footer(cv::Mat& img, int w, int h, int frame_idx, const std::string& live_tag) { void draw_footer(cv::Mat& img, int w, int h, int frame_idx, const std::string& live_tag,
float inf_ms, const std::string& model_name) {
int bar_h = 28; int bar_h = 28;
overlay_rect(img, 0, h - bar_h, w, h, C_PANEL, 0.55f); overlay_rect(img, 0, h - bar_h, w, h, C_PANEL, 0.55f);
char buf[64]; char buf[128];
snprintf(buf, sizeof(buf), "%s | Frame %d", live_tag.c_str(), frame_idx); snprintf(buf, sizeof(buf), "%s | %s | Frame %d | Inf %.1fms",
live_tag.c_str(), model_name.c_str(), frame_idx, inf_ms);
cv::putText(img, buf, cv::Point(12, h - 9), cv::FONT_HERSHEY_SIMPLEX, 0.45, C_MUTED, 1, cv::LINE_AA); cv::putText(img, buf, cv::Point(12, h - 9), cv::FONT_HERSHEY_SIMPLEX, 0.45, C_MUTED, 1, cv::LINE_AA);
} }
+11 -10
View File
@@ -5,16 +5,16 @@
#include <tuple> #include <tuple>
#include <cstdint> #include <cstdint>
inline const cv::Scalar C_PANEL(18, 24, 28); inline const cv::Scalar C_PANEL(28, 24, 18);
inline const cv::Scalar C_BORDER(75, 85, 90); inline const cv::Scalar C_BORDER(90, 85, 75);
inline const cv::Scalar C_ACCENT(60, 200, 255); inline const cv::Scalar C_ACCENT(255, 200, 60);
inline const cv::Scalar C_GREEN(100, 220, 80); inline const cv::Scalar C_GREEN(80, 220, 100);
inline const cv::Scalar C_TEXT(235, 235, 235); inline const cv::Scalar C_TEXT(235, 235, 235);
inline const cv::Scalar C_MUTED(150, 150, 150); inline const cv::Scalar C_MUTED(150, 150, 150);
inline const cv::Scalar C_AYAM_BOX(255, 165, 0); inline const cv::Scalar C_AYAM_BOX(255, 150, 60);
inline const cv::Scalar C_TALENAN_BOX(60, 120, 220); inline const cv::Scalar C_TALENAN_BOX(60, 60, 255);
inline const cv::Scalar C_LINE_CORE(255, 220, 180); inline const cv::Scalar C_LINE_CORE(180, 220, 255);
inline const cv::Scalar C_LINE_GLOW(220, 160, 100); inline const cv::Scalar C_LINE_GLOW(100, 160, 220);
extern const std::vector<std::pair<int, int>> SKELETON; extern const std::vector<std::pair<int, int>> SKELETON;
extern const std::vector<cv::Scalar> SK_COLORS; extern const std::vector<cv::Scalar> SK_COLORS;
@@ -30,8 +30,9 @@ void draw_pill(cv::Mat& img, const std::string& text, int x, int y,
void draw_elegant_counting_line(cv::Mat& img, int line_x, int h, int pulse_remaining = 0); void draw_elegant_counting_line(cv::Mat& img, int line_x, int h, int pulse_remaining = 0);
void draw_hero_count(cv::Mat& img, int line_x, int h, int count, int pulse_remaining = 0); void draw_hero_count(cv::Mat& img, int line_x, int h, int count, int pulse_remaining = 0);
void draw_hud(cv::Mat& img, int w, int batch_num, int batch_count, int total_ayam, void draw_hud(cv::Mat& img, int w, int batch_num, int batch_count, int total_ayam,
double elapsed_sec, double rate, const std::string& camera_id, const std::string& clock); double elapsed_sec, double rate);
void draw_footer(cv::Mat& img, int w, int h, int frame_idx, const std::string& live_tag); void draw_footer(cv::Mat& img, int w, int h, int frame_idx, const std::string& live_tag,
float inf_ms = 0.0f, const std::string& model_name = "");
void draw_skeleton_bold(cv::Mat& img, const std::vector<std::vector<cv::Point2f>>& kpts); void draw_skeleton_bold(cv::Mat& img, const std::vector<std::vector<cv::Point2f>>& kpts);
struct Popup { struct Popup {
+82 -34
View File
@@ -50,6 +50,35 @@ static void warmup_stream(cv::VideoCapture& cap, int n) {
std::cout << "Stream ready!" << std::endl; std::cout << "Stream ready!" << std::endl;
} }
static bool connect_stream(cv::VideoCapture& cap, int& w, int& h, double& fps) {
bool is_live = g_config.source.substr(0, 7) == "rtsp://" ||
g_config.source.substr(0, 7) == "http://";
int attempts = 0;
while (!shutdown_requested) {
cap = open_capture(g_config.source);
if (cap.isOpened()) break;
attempts++;
if (g_config.max_reconnect_attempts > 0 && attempts >= g_config.max_reconnect_attempts) {
std::cerr << "Cannot open source after " << attempts
<< " attempts: " << g_config.source << std::endl;
return false;
}
std::cout << "Cannot open source, retry in " << g_config.reconnect_delay_sec
<< "s..." << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(g_config.reconnect_delay_sec));
}
if (shutdown_requested) return false;
if (is_live && g_config.warmup_frames > 0)
warmup_stream(cap, g_config.warmup_frames);
w = static_cast<int>(cap.get(cv::CAP_PROP_FRAME_WIDTH));
h = static_cast<int>(cap.get(cv::CAP_PROP_FRAME_HEIGHT));
fps = cap.get(cv::CAP_PROP_FPS);
if (fps <= 1) fps = g_config.output_fps;
return true;
}
struct TrackedInfo { struct TrackedInfo {
float cx; float cx;
std::chrono::steady_clock::time_point ts; std::chrono::steady_clock::time_point ts;
@@ -75,11 +104,23 @@ int main(int argc, char* argv[]) {
std::cout << "Starting ByteTrack Counter (C++)" << std::endl; std::cout << "Starting ByteTrack Counter (C++)" << std::endl;
std::cout << "Camera: " << g_config.camera_name << std::endl; std::cout << "Camera: " << g_config.camera_name << std::endl;
std::cout << "Source: " << g_config.source << std::endl; std::cout << "Source: " << g_config.source << std::endl;
std::cout << "Model: " << g_config.model_path << std::endl; std::cout << "Model: " << g_config.model_path << " | imgsz=" << g_config.imgsz
<< " | core_mask=" << g_config.core_mask << std::endl;
std::cout << "DB: " << g_config.db_path << std::endl; std::cout << "DB: " << g_config.db_path << std::endl;
std::cout << "State: " << g_config.state_file << std::endl;
std::cout << "ByteTrack Index 0: high_thresh=" << g_config.track_high_thresh_0
<< " low_thresh=" << g_config.track_low_thresh_0
<< " match_thresh=" << g_config.track_match_thresh_0
<< " buffer=" << g_config.track_buffer_0
<< " min_hits=" << g_config.track_min_hits_0 << std::endl;
std::cout << "ByteTrack Index 1: high_thresh=" << g_config.track_high_thresh_1
<< " low_thresh=" << g_config.track_low_thresh_1
<< " match_thresh=" << g_config.track_match_thresh_1
<< " buffer=" << g_config.track_buffer_1
<< " min_hits=" << g_config.track_min_hits_1 << std::endl;
int class_ayam = 0; int class_ayam = g_config.ayam_class_id;
int class_talenan = 1; int class_talenan = g_config.talenan_class_id;
auto logger = [](const std::string& msg) { auto logger = [](const std::string& msg) {
std::cout << "[" << now_str() << "] " << msg << std::endl; std::cout << "[" << now_str() << "] " << msg << std::endl;
@@ -105,12 +146,12 @@ int main(int argc, char* argv[]) {
g_config.imgsz, g_config.conf, 0.45f, g_config.imgsz, g_config.conf, 0.45f,
g_config.num_classes, 0, g_config.score_sigmoid); g_config.num_classes, 0, g_config.score_sigmoid);
ByteTracker ayam_tracker(g_config.track_high_thresh, g_config.track_low_thresh, ByteTracker ayam_tracker(g_config.track_high_thresh_0, g_config.track_low_thresh_0,
g_config.track_match_thresh, g_config.track_buffer, g_config.track_match_thresh_0, g_config.track_buffer_0,
g_config.track_min_hits); g_config.track_min_hits_0);
ByteTracker talenan_tracker(g_config.track_high_thresh, g_config.track_low_thresh, ByteTracker talenan_tracker(g_config.track_high_thresh_1, g_config.track_low_thresh_1,
g_config.track_match_thresh, g_config.track_buffer, g_config.track_match_thresh_1, g_config.track_buffer_1,
g_config.track_min_hits); g_config.track_min_hits_1);
std::unordered_set<int> ayam_line_crossed; std::unordered_set<int> ayam_line_crossed;
std::unordered_set<int> talenan_line_crossed; std::unordered_set<int> talenan_line_crossed;
@@ -119,27 +160,27 @@ int main(int argc, char* argv[]) {
std::unordered_map<int, int> talenan_cross_flash; std::unordered_map<int, int> talenan_cross_flash;
int line_pulse = 0, count_pulse = 0, batch_pulse = 0; int line_pulse = 0, count_pulse = 0, batch_pulse = 0;
std::vector<Popup> popups; std::vector<Popup> popups;
std::deque<std::chrono::steady_clock::time_point> crossing_times;
auto session_start = std::chrono::steady_clock::now(); auto session_start = std::chrono::steady_clock::now();
int frame_idx = 0; int frame_idx = 0;
float inf_ema = 0.0f;
cv::VideoCapture cap = open_capture(g_config.source); std::string model_name = g_config.model_path;
auto slash = model_name.find_last_of("/\\");
if (slash != std::string::npos) model_name = model_name.substr(slash + 1);
cv::VideoCapture cap;
int w, h;
double fps_prop;
if (!connect_stream(cap, w, h, fps_prop)) {
store.shutdown();
return shutdown_requested ? 0 : 1;
}
bool is_live = g_config.source.substr(0, 7) == "rtsp://" || bool is_live = g_config.source.substr(0, 7) == "rtsp://" ||
g_config.source.substr(0, 7) == "http://"; g_config.source.substr(0, 7) == "http://";
if (!cap.isOpened()) {
std::cerr << "Cannot open source: " << g_config.source << std::endl;
store.shutdown();
return 1;
}
if (is_live && g_config.warmup_frames > 0)
warmup_stream(cap, g_config.warmup_frames);
int w = static_cast<int>(cap.get(cv::CAP_PROP_FRAME_WIDTH));
int h = static_cast<int>(cap.get(cv::CAP_PROP_FRAME_HEIGHT));
double fps_prop = cap.get(cv::CAP_PROP_FPS);
if (fps_prop <= 1) fps_prop = g_config.output_fps;
int line_x = resolve_line_x(w, g_config.line_x, g_config.line_x_frac); int line_x = resolve_line_x(w, g_config.line_x, g_config.line_x_frac);
std::cout << "RKNN+ByteTrack counter | " << w << "x" << h << " @ " std::cout << "RKNN+ByteTrack counter | " << w << "x" << h << " @ "
@@ -161,21 +202,22 @@ int main(int argc, char* argv[]) {
<< "), reconnecting..." << std::endl; << "), reconnecting..." << std::endl;
cap.release(); cap.release();
std::this_thread::sleep_for(std::chrono::seconds(g_config.reconnect_delay_sec)); std::this_thread::sleep_for(std::chrono::seconds(g_config.reconnect_delay_sec));
cap = open_capture(g_config.source); if (!connect_stream(cap, w, h, fps_prop)) break;
if (!cap.isOpened()) break;
w = static_cast<int>(cap.get(cv::CAP_PROP_FRAME_WIDTH));
h = static_cast<int>(cap.get(cv::CAP_PROP_FRAME_HEIGHT));
line_x = resolve_line_x(w, g_config.line_x, g_config.line_x_frac); line_x = resolve_line_x(w, g_config.line_x, g_config.line_x_frac);
continue; continue;
} }
auto elapsed_sec = std::chrono::duration_cast<std::chrono::seconds>( auto elapsed_sec = std::chrono::duration<double>(
std::chrono::steady_clock::now() - session_start).count(); std::chrono::steady_clock::now() - session_start).count();
auto mono = std::chrono::steady_clock::now(); auto mono = std::chrono::steady_clock::now();
bool ayam_crossed_frame = false, batch_closed_frame = false, batch_started_frame = false; bool ayam_crossed_frame = false, batch_closed_frame = false, batch_started_frame = false;
auto inf_start = std::chrono::steady_clock::now();
std::vector<Detection> detections = model(frame); std::vector<Detection> detections = model(frame);
float inf_ms = std::chrono::duration<float, std::milli>(
std::chrono::steady_clock::now() - inf_start).count();
inf_ema = inf_ema * 0.9f + inf_ms * 0.1f;
if (!detections.empty()) { if (!detections.empty()) {
std::vector<Eigen::Vector4f> ayam_boxes, talenan_boxes; std::vector<Eigen::Vector4f> ayam_boxes, talenan_boxes;
@@ -317,6 +359,7 @@ int main(int argc, char* argv[]) {
} }
ayam_crossed_frame = true; ayam_crossed_frame = true;
if (started_new) batch_started_frame = true; if (started_new) batch_started_frame = true;
crossing_times.push_back(mono);
ayam_cross_flash[tid] = g_config.cross_flash_frames; ayam_cross_flash[tid] = g_config.cross_flash_frames;
popups.push_back({ popups.push_back({
static_cast<int>(cx) - 12, static_cast<int>(cx) - 12,
@@ -379,15 +422,19 @@ int main(int argc, char* argv[]) {
int batch_num = store.current_batch_number(); int batch_num = store.current_batch_number();
int batch_count = store.current_batch_count(); int batch_count = store.current_batch_count();
int display_total = store.display_total(); int display_total = store.display_total();
double elapsed = static_cast<double>(elapsed_sec);
double rate = elapsed > 0 ? (display_total / elapsed * 60.0) : 0.0; while (!crossing_times.empty() &&
std::chrono::duration<double>(mono - crossing_times.front()).count() > g_config.rate_window_sec)
crossing_times.pop_front();
double rate = crossing_times.empty() ? 0.0 :
(crossing_times.size() / static_cast<double>(g_config.rate_window_sec) * 60.0);
draw_elegant_counting_line(frame, line_x, h, line_pulse); draw_elegant_counting_line(frame, line_x, h, line_pulse);
draw_hero_count(frame, line_x, h, batch_count, count_pulse); draw_hero_count(frame, line_x, h, batch_count, count_pulse);
draw_hud(frame, w, batch_num, batch_count, display_total, draw_hud(frame, w, batch_num, batch_count, display_total,
elapsed, rate, g_config.camera_name, now_str()); elapsed_sec, rate);
draw_batch_banner(frame, w, batch_num, batch_pulse); draw_batch_banner(frame, w, batch_num, batch_pulse);
draw_footer(frame, w, h, frame_idx, is_live ? "LIVE-RKNN-BT" : "FILE-RKNN-BT"); draw_footer(frame, w, h, frame_idx, is_live ? "LIVE" : "FILE", inf_ema, model_name);
popups = draw_popups(frame, popups, frame_idx); popups = draw_popups(frame, popups, frame_idx);
// Flash decay // Flash decay
@@ -418,9 +465,10 @@ int main(int argc, char* argv[]) {
frame_idx++; frame_idx++;
if (frame_idx % g_config.flush_every_n_frames == 0) { if (frame_idx % g_config.flush_every_n_frames == 0) {
std::cout << "[" << now_str() << "] Frame " << frame_idx std::cout << "[" << now_str() << "] Frame " << frame_idx
<< " | Batch " << batch_num << ": " << batch_count << " | Batch " << (batch_num ? std::to_string(batch_num) : "-")
<< ": " << (batch_num ? std::to_string(batch_count) : "-")
<< " | Total: " << display_total << " | Total: " << display_total
<< " | Uptime " << (elapsed / 3600.0) << "h" << std::endl; << " | Uptime " << (elapsed_sec / 3600.0) << "h" << std::endl;
} }
prune_stale_tracks(ayam_tracked, g_config.tracked_prune_sec); prune_stale_tracks(ayam_tracked, g_config.tracked_prune_sec);