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 signal
import time
from collections import deque
from datetime import datetime
from pathlib import Path
@@ -68,6 +69,11 @@ RTSP_FFMPEG_OPTIONS = os.getenv(
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
POPUP_LIFETIME = 20
LINE_PULSE_FRAMES = 12
@@ -251,12 +257,12 @@ def draw_hero_count(img, line_x, h, count, pulse_remaining=0):
text = str(count)
font = cv2.FONT_HERSHEY_SIMPLEX
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)
pad = 14
tx, ty = line_x - tw // 2, h // 2 + th // 2
overlay_rect(img, tx - pad, ty - th - pad, tx + tw + pad, ty + pad // 2, C_PANEL, alpha=0.78)
cv2.rectangle(img, (tx - pad, ty - th - pad), (tx + tw + pad, ty + pad // 2), C_LINE_CORE, 2)
tx, ty = line_x - tw // 2, h // 3 + th // 2
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), C_LINE_CORE, 2)
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)
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
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):
@@ -384,7 +390,10 @@ def run():
session_start = time.time()
frame_idx = 0
inf_ms = 0.0
video_writer = None
crossing_times = deque()
prev_gray = None
cap, w, h, fps = connect_stream(SOURCE)
if cap is None:
@@ -422,19 +431,34 @@ def run():
mono = time.monotonic()
ayam_crossed_frame = batch_closed_frame = batch_started_frame = False
results = model.track(
frame,
device=DEVICE,
persist=True,
conf=CONF,
imgsz=IMGSZ,
half=HALF,
tracker=TRACKER,
verbose=False,
)
skip_inference = False
if MOTION_DETECTION_ENABLED:
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
if prev_gray is not None:
diff = cv2.absdiff(gray, prev_gray)
mean_diff = cv2.mean(diff)[0]
skip_inference = mean_diff < MOTION_THRESHOLD
prev_gray = gray
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]
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()
boxes = r.boxes.xyxy.tolist()
clss = r.boxes.cls.int().tolist()
@@ -474,12 +498,29 @@ def run():
datetime.now().isoformat(), track_id,
])
ayam_crossed_frame = True
crossing_times.append(mono)
if started_new:
batch_started_frame = True
ayam_cross_flash[track_id] = CROSS_FLASH_FRAMES
popups.append({'x': cx - 12, 'y': (y1 + y2) // 2, 'born': frame_idx, 'text': '+1'})
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:
flash = talenan_cross_flash.get(track_id, 0)
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_count = store.current_batch_count
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_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_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)
for flash_store in (ayam_cross_flash, talenan_cross_flash):