Porting All features from RKNN to Jetson

This commit is contained in:
proitlab committed 2026-07-07 08:07:40 +07:00
1 parent b451b84885
commit 8bb2fbbea5
4 files changed
+282 -382961

No files matched your search

+220
View File
@@ -0,0 +1,220 @@
# counter_dashboard.py — API Documentation
## Page
### `GET /`
Renders the main dashboard HTML page (`dashboard.html`).
---
## API Endpoints
### `GET /api/live-video`
MJPEG streaming endpoint. Serves live frames from the path configured in `LIVE_STREAM_FRAME_PATH` (default `/dev/shm/jetson-counter/live_frame.jpg`) as `multipart/x-mixed-replace`.
**Response:** JPEG stream with `--frame` boundary delimiters.
---
### `GET /api/current-batch`
Returns the currently active batch's data read from `current_batch.json`.
**Success response:**
```json
{
"success": true,
"counting_date": "2024-01-01",
"batch_number": 12,
"count": 342,
"start_time": "2024-01-01T14:30:00",
"last_detection_time": "2024-01-01T14:35:00"
}
```
**Error response (no active batch, 200):**
```json
{
"success": false,
"error": "No active batch",
"count": 0,
"batch_number": null,
"counting_date": null
}
```
---
### `GET /api/previous-batch`
Returns the most recently completed batch from the `batches` database table, with computed `duration_minutes`.
**Success response:**
```json
{
"success": true,
"date": "2024-01-01",
"batch_number": 11,
"count": 287,
"start_time": "2024-01-01T14:00:00",
"end_time": "2024-01-01T14:27:00",
"duration_minutes": 27.0
}
```
---
### `GET /api/summary`
Returns today, yesterday, and all-time aggregate statistics plus average per day and best day.
**Success response:**
```json
{
"today": {
"date": "2024-01-01",
"total_count": 1500,
"total_batches": 8
},
"yesterday": {
"date": "2023-12-31",
"total_count": 1342,
"total_batches": 7
},
"all_time": {
"grand_total": 50000,
"grand_batches": 260,
"total_days": 45
},
"average_per_day": 1111.1,
"best_day": {
"date": "2023-12-15",
"count": 2100
}
}
```
---
### `GET /api/daily-data`
Returns daily summary rows for charting.
**Query params:**
| Param | Type | Default | Description |
|--------|------|---------|---------------------------------|
| `days` | int | 30 | Number of days to look back |
**Response:**
```json
[
{
"date": "2024-01-01",
"total_count": 1500,
"total_batches": 8,
"avg_per_batch": 187.5
}
]
```
---
### `GET /api/day-detail/<date>`
Returns all batch records for a specific `counting_date` along with summary totals.
**Path params:**
| Param | Type | Description |
|--------|--------|------------------------|
| `date` | string | Date in `YYYY-MM-DD` |
**Success response:**
```json
{
"date": "2024-01-01",
"total_count": 1500,
"total_batches": 8,
"total_duration_minutes": 216.0,
"avg_duration_minutes": 27.0,
"batches": [
{
"batch_number": 1,
"count": 187,
"start_time": "2024-01-01T08:00:00",
"end_time": "2024-01-01T08:27:00",
"duration_minutes": 27.0
}
]
}
```
---
### `GET /api/recent-batches`
Returns the most recent batches with computed durations.
**Query params:**
| Param | Type | Default | Description |
|---------|------|---------|---------------------------------|
| `limit` | int | 10 | Max number of batches to return |
**Response:**
```json
[
{
"date": "2024-01-01",
"batch_number": 8,
"count": 213,
"start_time": "2024-01-01T16:30:00",
"end_time": "2024-01-01T16:58:00",
"duration_minutes": 28.0
}
]
```
---
### `GET /api/available-dates`
Returns all dates with data from the `daily_summaries` table, ordered by date descending.
**Response:**
```json
[
{
"date": "2024-01-01",
"total_count": 1500,
"total_batches": 8
}
]
```
---
## Export Endpoints (XLSX)
### `GET /api/export-daily-csv`
Exports batch detail records for the last N days as an `.xlsx` file.
**Query params:**
| Param | Type | Default | Description |
|--------|------|---------|-----------------------------|
| `days` | int | 30 | Number of days to look back |
**Response:** XLSX file download with columns: Date, Batch #, Count, Start Time, End Time, Duration (min).
Filename format: `{SITE_NAME}_daily_records_{timestamp}.xlsx`
---
### `GET /api/export-day-csv/<date>`
Exports all batch records for a single date as an `.xlsx` file.
**Path params:**
| Param | Type | Description |
|--------|--------|----------------------|
| `date` | string | Date in `YYYY-MM-DD` |
**Response:** XLSX file download with columns: Batch Number, Count, Start Time, End Time, Duration (min).
Filename format: `{SITE_NAME}_day_detail_{date}.xlsx`
---
## Error Handling
All endpoints return `{"success": false, "error": "<message>"}` with HTTP 500 on unexpected errors. Database-unavailable errors (`sqlite3.OperationalError`) return HTTP 200 with an empty/default data structure. The `/api/live-video` endpoint returns HTTP 503 if the frame file is not found.
-382942
View File
File diff suppressed because it is too large. Load diff
Binary file not shown.
+62 -19
View File
@@ -9,6 +9,7 @@ import numpy as np
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
@@ -68,6 +69,11 @@ RTSP_FFMPEG_OPTIONS = os.getenv(
IS_LIVE = SOURCE.lower().startswith(('rtsp://', 'http://')) IS_LIVE = SOURCE.lower().startswith(('rtsp://', 'http://'))
MOTION_DETECTION_ENABLED = os.getenv('MOTION_DETECTION_ENABLED', 'false').lower() == 'true'
MOTION_THRESHOLD = float(os.getenv('MOTION_THRESHOLD', '5.0'))
RATE_WINDOW_SEC = int(os.getenv('RATE_WINDOW_SEC', '60'))
CROSS_FLASH_FRAMES = 12 CROSS_FLASH_FRAMES = 12
POPUP_LIFETIME = 20 POPUP_LIFETIME = 20
LINE_PULSE_FRAMES = 12 LINE_PULSE_FRAMES = 12
@@ -251,12 +257,12 @@ 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(img, tx - pad, ty - th - pad, tx + tw + pad, ty + pad // 2, C_PANEL, alpha=0.78) overlay_rect(img, tx - pad, ty - th - pad, tx + tw + pad, ty + pad, C_PANEL, alpha=0.78)
cv2.rectangle(img, (tx - pad, ty - th - pad), (tx + tw + pad, ty + pad // 2), C_LINE_CORE, 2) cv2.rectangle(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)
@@ -279,10 +285,10 @@ def draw_hud(img, w, batch_num, batch_count, total_ayam, elapsed_sec, rate, came
cv2.putText(img, f'CAM {camera_id}', (w - 180, 20), cv2.FONT_HERSHEY_SIMPLEX, 0.45, C_MUTED, 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(img, f'{live_tag} | Frame {frame_idx}', (12, h - 9), cv2.FONT_HERSHEY_SIMPLEX, 0.45, C_MUTED, 1, cv2.LINE_AA) cv2.putText(img, f'{live_tag} | {model_name} | Frame {frame_idx} | Inf {inf_ms:.1f}ms', (12, h - 9), cv2.FONT_HERSHEY_SIMPLEX, 0.45, C_MUTED, 1, cv2.LINE_AA)
def draw_skeleton_bold(img, kpts): def draw_skeleton_bold(img, kpts):
@@ -384,7 +390,10 @@ 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()
prev_gray = None
cap, w, h, fps = connect_stream(SOURCE) cap, w, h, fps = connect_stream(SOURCE)
if cap is None: if cap is None:
@@ -422,19 +431,34 @@ 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
results = model.track( skip_inference = False
frame, if MOTION_DETECTION_ENABLED:
device=DEVICE, gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
persist=True, if prev_gray is not None:
conf=CONF, diff = cv2.absdiff(gray, prev_gray)
imgsz=IMGSZ, mean_diff = cv2.mean(diff)[0]
half=HALF, skip_inference = mean_diff < MOTION_THRESHOLD
tracker=TRACKER, prev_gray = gray
verbose=False,
) if not skip_inference:
inf_start = time.time()
results = model.track(
frame,
device=DEVICE,
persist=True,
conf=CONF,
imgsz=IMGSZ,
half=HALF,
tracker=TRACKER,
verbose=False,
)
inf_ms = inf_ms * 0.9 + (time.time() - inf_start) * 1000 * 0.1
else:
results = [None]
r = results[0] r = results[0]
if r.boxes.id is not None: if r is not None and r.boxes.id is not None:
ids = r.boxes.id.int().tolist() ids = r.boxes.id.int().tolist()
boxes = r.boxes.xyxy.tolist() boxes = r.boxes.xyxy.tolist()
clss = r.boxes.cls.int().tolist() clss = r.boxes.cls.int().tolist()
@@ -474,12 +498,29 @@ def run():
datetime.now().isoformat(), track_id, datetime.now().isoformat(), track_id,
]) ])
ayam_crossed_frame = True ayam_crossed_frame = True
crossing_times.append(mono)
if started_new: if started_new:
batch_started_frame = True batch_started_frame = True
ayam_cross_flash[track_id] = CROSS_FLASH_FRAMES ayam_cross_flash[track_id] = CROSS_FLASH_FRAMES
popups.append({'x': cx - 12, 'y': (y1 + y2) // 2, 'born': frame_idx, 'text': '+1'}) popups.append({'x': cx - 12, 'y': (y1 + y2) // 2, 'born': frame_idx, 'text': '+1'})
ayam_tracked[track_id] = (cx, mono) ayam_tracked[track_id] = (cx, mono)
if os.getenv('DEBUG_TRACKING', '').lower() == 'true':
ayam_tracks_str = f' ayam_tracked: {sorted(ayam_tracked.keys())}' if ayam_tracked else ''
talenan_tracks_str = f' talenan_tracked: {sorted(talenan_tracked.keys())}' if talenan_tracked else ''
cx_sample = ''
if ayam_items:
track_id, cx, _, _, _, _, _ = ayam_items[0]
prev = ayam_tracked.get(track_id, (None,))[0] if ayam_tracked.get(track_id) else None
cx_sample = f' sample tid={track_id} prev_cx={prev} cx={cx}'
print(
f'[DEBUG F{frame_idx}] ayam_dets={len(ayam_items)} '
f'talenan_dets={len(talenan_items)} '
f'line_x={line_x}{talenan_tracks_str}{ayam_tracks_str}'
f' crossed: a={sorted(ayam_line_crossed)} t={sorted(talenan_line_crossed)}'
f'{cx_sample}'
)
for track_id, cx, x1, y1, x2, y2, _ in talenan_items: for track_id, cx, x1, y1, x2, y2, _ in talenan_items:
flash = talenan_cross_flash.get(track_id, 0) flash = talenan_cross_flash.get(track_id, 0)
color = C_GREEN if flash > 0 else C_TALENAN_BOX color = C_GREEN if flash > 0 else C_TALENAN_BOX
@@ -505,13 +546,15 @@ 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)
draw_hud(frame, w, batch_num, batch_count, display_total, elapsed, rate, CAMERA_NAME, now_str()) draw_hud(frame, w, batch_num, batch_count, display_total, elapsed, 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(frame, w, h, frame_idx, 'LIVE' if IS_LIVE else 'FILE') draw_footer(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)
for flash_store in (ayam_cross_flash, talenan_cross_flash): for flash_store in (ayam_cross_flash, talenan_cross_flash):