forked from zakaria/chicken-counting-sukawarna-det
Compare commits
16
Commits
cplusplus
...
3b524d4a65
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3b524d4a65 | ||
|
|
6cddf2c9d3 | ||
|
|
b9f4ba2ef8 | ||
|
|
94c4dd81a7 | ||
|
|
8cb5b01c00 | ||
|
|
3eeb20ca56 | ||
|
|
f9fc6c0571 | ||
|
|
e7c26d3f86 | ||
|
|
dc447264b7 | ||
|
|
9512fc7f4d | ||
|
|
ae86821165 | ||
|
|
efd4726bc6 | ||
|
|
6dbaa517dc | ||
|
|
a54d0e6b2c | ||
|
|
d31ad05f0a | ||
|
|
af4e514357 |
No files matched your search
@@ -0,0 +1,149 @@
|
|||||||
|
# Chicken Counter API
|
||||||
|
|
||||||
|
Base URL: `http://<jetson-ip>:8080`
|
||||||
|
|
||||||
|
## Live Dashboard
|
||||||
|
|
||||||
|
### `GET /`
|
||||||
|
Returns the dashboard HTML page.
|
||||||
|
|
||||||
|
### `GET /api/cameras`
|
||||||
|
List cameras currently writing to `/dev/shm`.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"cameras": ["CC1", "CC2", "CC3"]}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `GET /shm/<camera_id>/stats.json`
|
||||||
|
Live stats for the active pipeline.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"frame_index": 5120,
|
||||||
|
"inside_box_count": 42,
|
||||||
|
"total_entered_count": 1858,
|
||||||
|
"track_count": 99,
|
||||||
|
"backward_active": false,
|
||||||
|
"smoothed_speed": 4.3,
|
||||||
|
"count_events": 0,
|
||||||
|
"run_date": "2026-06-10"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `GET /shm/<camera_id>/frame.jpg`
|
||||||
|
Live JPEG frame from the active pipeline.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Database
|
||||||
|
|
||||||
|
All endpoints require the dashboard to be started with `--db <path>`. If no DB exists, endpoints return `[]` or `{}`.
|
||||||
|
|
||||||
|
### `GET /api/db/summary`
|
||||||
|
Overall totals across all dates and locations.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"days": 12,
|
||||||
|
"locations": 2,
|
||||||
|
"total_runs": 48,
|
||||||
|
"total_chickens": 125000,
|
||||||
|
"total_hours": 8.5
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `GET /api/db/history`
|
||||||
|
Per-date summary, newest first (max 50 rows).
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"date": "2026-06-10",
|
||||||
|
"location": "kandang-atas",
|
||||||
|
"cams": 4,
|
||||||
|
"total": 5570,
|
||||||
|
"minutes": 40.2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### `GET /api/db/date/<date>`
|
||||||
|
Detail for a specific date. Format: `YYYY-MM-DD`.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"date": "2026-06-10",
|
||||||
|
"total": {"total": 5570, "minutes": 40.2},
|
||||||
|
"cameras": [
|
||||||
|
{
|
||||||
|
"camera_id": "CC1",
|
||||||
|
"total_entered": 1500,
|
||||||
|
"frames_processed": 24800,
|
||||||
|
"elapsed_seconds": 600.5,
|
||||||
|
"stopped_reason": "backward",
|
||||||
|
"source_video": "kandang_1_camera_1_2026-06-10_120056.mp4",
|
||||||
|
"location": "kandang-atas"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `GET /api/db/camera/<camera_id>`
|
||||||
|
History for a specific camera across all dates (max 50 rows).
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"date": "2026-06-10",
|
||||||
|
"location": "kandang-atas",
|
||||||
|
"total_entered": 1500,
|
||||||
|
"frames_processed": 24800,
|
||||||
|
"elapsed_seconds": 600.5,
|
||||||
|
"stopped_reason": "backward"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### `GET /api/db/location/<location>`
|
||||||
|
Summary and history for a specific location.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"location": "kandang-atas",
|
||||||
|
"summary": {"days": 5, "total": 25000, "hours": 3.2},
|
||||||
|
"history": [
|
||||||
|
{
|
||||||
|
"date": "2026-06-10",
|
||||||
|
"cameras": "CC1, CC2, CC3, CC4",
|
||||||
|
"total": 5570,
|
||||||
|
"minutes": 40.2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Database Schema
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE batch_runs (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
date TEXT NOT NULL,
|
||||||
|
location TEXT NOT NULL,
|
||||||
|
camera_id TEXT NOT NULL,
|
||||||
|
total_entered INTEGER NOT NULL DEFAULT 0,
|
||||||
|
frames_processed INTEGER NOT NULL DEFAULT 0,
|
||||||
|
elapsed_seconds REAL NOT NULL DEFAULT 0.0,
|
||||||
|
stopped_reason TEXT NOT NULL DEFAULT '',
|
||||||
|
source_video TEXT NOT NULL DEFAULT '',
|
||||||
|
generated_at TEXT NOT NULL DEFAULT '',
|
||||||
|
UNIQUE(date, location, camera_id)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
Data is inserted automatically by the batch runner when `location` and `db_path` are configured in the batch YAML, or manually via:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 store_results.py output/counts_2026-06-10.json --location kandang-atas --db chicken_counts.db
|
||||||
|
```
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Chicken Counter Dashboard
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=dsutanto
|
||||||
|
WorkingDirectory=/media/jetson/DATA/.Codes/chicken-counting-sukawarna-det
|
||||||
|
ExecStart=/media/jetson/DATA/karung-sukawarna/venv/bin/python dashboard.py --port 8080 --db db/chicken_counts.db
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
Environment=PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
@@ -5,10 +5,12 @@ batch:
|
|||||||
compress_max_mb: 200
|
compress_max_mb: 200
|
||||||
delete_intermediate: false
|
delete_intermediate: false
|
||||||
checkpoint_every_n_frames: 3000
|
checkpoint_every_n_frames: 3000
|
||||||
|
location: kandang-atas
|
||||||
|
db_path: /media/jetson/DATA/.Codes/chicken-counting-sukawarna-det/db/chicken_counts.db
|
||||||
|
|
||||||
defaults:
|
defaults:
|
||||||
detection:
|
detection:
|
||||||
model_path: /media/jetson/DATA/.Codes/chicken-counting-sukawarna-det/chicken-detection-model-v26n-300e-best-2026-05-02-NEW.engine
|
model_path: /media/jetson/DATA/.Codes/chicken-counting-sukawarna-det/models/chicken-detection-model-v26n-300e-best-2026-05-02-NEW.engine
|
||||||
classes: [0]
|
classes: [0]
|
||||||
ignored_classes: [1, 2]
|
ignored_classes: [1, 2]
|
||||||
conf: 0.35
|
conf: 0.35
|
||||||
|
|||||||
Executable
+94
@@ -0,0 +1,94 @@
|
|||||||
|
batch:
|
||||||
|
root_dir: /media/jetson/DATA/.Codes/VIDEOS/cycle7/kandang-atas
|
||||||
|
camera_glob: "kandang_*_camera_{num}_*.mp4"
|
||||||
|
output_subdir: output
|
||||||
|
compress_max_mb: 200
|
||||||
|
delete_intermediate: false
|
||||||
|
checkpoint_every_n_frames: 3000
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
detection:
|
||||||
|
model_path: /media/jetson/DATA/.Codes/chicken-counting-sukawarna-det/chicken-detection-model-v26n-300e-best-2026-05-02-NEW.engine
|
||||||
|
classes: [0]
|
||||||
|
ignored_classes: [1, 2]
|
||||||
|
conf: 0.35
|
||||||
|
iou: 0.55
|
||||||
|
imgsz: 640
|
||||||
|
device: "0"
|
||||||
|
min_box_area_px: 3000
|
||||||
|
validate_while_inside: true
|
||||||
|
detection_zone:
|
||||||
|
enabled: true
|
||||||
|
buffer_above_px: 250
|
||||||
|
buffer_below_px: 250
|
||||||
|
show_in_overlay: true
|
||||||
|
tracker:
|
||||||
|
tracker_config_path: /media/jetson/DATA/.Codes/chicken-counting-sukawarna-det/configs/trackers/botsort_chicken.yaml
|
||||||
|
persist: true
|
||||||
|
track_buffer: 75
|
||||||
|
gate:
|
||||||
|
mode: two_line
|
||||||
|
lines_y: [420, 730]
|
||||||
|
direction: bottom_to_up
|
||||||
|
motion:
|
||||||
|
enabled: true
|
||||||
|
axis: vertical
|
||||||
|
forward_sign: 1.0
|
||||||
|
ema_alpha: 0.2
|
||||||
|
reverse_enter_threshold: -1.5
|
||||||
|
reverse_exit_threshold: -0.5
|
||||||
|
debounce_frames: 12
|
||||||
|
min_features: 60
|
||||||
|
max_corners: 80
|
||||||
|
stride_frames: 2
|
||||||
|
flow_scale: 0.5
|
||||||
|
quality_level: 0.01
|
||||||
|
min_distance: 8
|
||||||
|
block_radius: 6
|
||||||
|
overlay:
|
||||||
|
show_boxes: true
|
||||||
|
show_track_trails: false
|
||||||
|
trail_length: 20
|
||||||
|
show_center_marker: true
|
||||||
|
show_track_ring: false
|
||||||
|
count_anchor: [780, 120]
|
||||||
|
inside_box_only: true
|
||||||
|
pending_blink: true
|
||||||
|
pending_colors:
|
||||||
|
- [255, 255, 0]
|
||||||
|
- [0, 255, 255]
|
||||||
|
display:
|
||||||
|
show_window: false
|
||||||
|
encoder: auto
|
||||||
|
output_bitrate_kbps: 4000
|
||||||
|
codec_preference: [avc1, mp4v, H264]
|
||||||
|
performance:
|
||||||
|
half: false
|
||||||
|
overlay_buffer_reuse: true
|
||||||
|
inference_stride: 2
|
||||||
|
stream:
|
||||||
|
enabled: true
|
||||||
|
shm_dir: /dev/shm
|
||||||
|
interval_frames: 5
|
||||||
|
feedback:
|
||||||
|
enabled: true
|
||||||
|
every_n_frames: 3000
|
||||||
|
save_images: true
|
||||||
|
log_to_terminal: true
|
||||||
|
roi:
|
||||||
|
inset_left_px: 60
|
||||||
|
inset_right_px: 60
|
||||||
|
inset_top_px: 0
|
||||||
|
inset_bottom_px: 0
|
||||||
|
min_overlap_ratio: 0.30
|
||||||
|
|
||||||
|
cameras:
|
||||||
|
CC2:
|
||||||
|
camera_num: 2
|
||||||
|
count_anchor: [900, 120]
|
||||||
|
roi:
|
||||||
|
points:
|
||||||
|
- [20, 380]
|
||||||
|
- [1880, 380]
|
||||||
|
- [1880, 720]
|
||||||
|
- [20, 720]
|
||||||
Executable
+121
@@ -0,0 +1,121 @@
|
|||||||
|
batch:
|
||||||
|
root_dir: /media/jetson/DATA/.Codes/VIDEOS/cycle7/kandang-atas
|
||||||
|
camera_glob: "kandang_*_camera_{num}_*.mp4"
|
||||||
|
output_subdir: output
|
||||||
|
compress_max_mb: 200
|
||||||
|
delete_intermediate: false
|
||||||
|
checkpoint_every_n_frames: 3000
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
detection:
|
||||||
|
model_path: /media/jetson/DATA/.Codes/chicken-counting-sukawarna-det/chicken-detection-model-v26n-300e-best-2026-05-02-NEW.engine
|
||||||
|
classes: [0]
|
||||||
|
ignored_classes: [1, 2]
|
||||||
|
conf: 0.35
|
||||||
|
iou: 0.55
|
||||||
|
imgsz: 640
|
||||||
|
device: "0"
|
||||||
|
min_box_area_px: 3000
|
||||||
|
validate_while_inside: true
|
||||||
|
detection_zone:
|
||||||
|
enabled: true
|
||||||
|
buffer_above_px: 250
|
||||||
|
buffer_below_px: 250
|
||||||
|
show_in_overlay: true
|
||||||
|
tracker:
|
||||||
|
tracker_config_path: /media/jetson/DATA/.Codes/chicken-counting-sukawarna-det/configs/trackers/botsort_chicken.yaml
|
||||||
|
persist: true
|
||||||
|
track_buffer: 75
|
||||||
|
gate:
|
||||||
|
mode: two_line
|
||||||
|
lines_y: [420, 730]
|
||||||
|
direction: bottom_to_up
|
||||||
|
motion:
|
||||||
|
enabled: true
|
||||||
|
axis: vertical
|
||||||
|
forward_sign: 1.0
|
||||||
|
ema_alpha: 0.2
|
||||||
|
reverse_enter_threshold: -1.5
|
||||||
|
reverse_exit_threshold: -0.5
|
||||||
|
debounce_frames: 12
|
||||||
|
min_features: 60
|
||||||
|
max_corners: 80
|
||||||
|
stride_frames: 2
|
||||||
|
flow_scale: 0.5
|
||||||
|
quality_level: 0.01
|
||||||
|
min_distance: 8
|
||||||
|
block_radius: 6
|
||||||
|
overlay:
|
||||||
|
show_boxes: true
|
||||||
|
show_track_trails: false
|
||||||
|
trail_length: 20
|
||||||
|
show_center_marker: true
|
||||||
|
show_track_ring: false
|
||||||
|
count_anchor: [780, 120]
|
||||||
|
inside_box_only: true
|
||||||
|
pending_blink: true
|
||||||
|
pending_colors:
|
||||||
|
- [255, 255, 0]
|
||||||
|
- [0, 255, 255]
|
||||||
|
display:
|
||||||
|
show_window: false
|
||||||
|
encoder: auto
|
||||||
|
output_bitrate_kbps: 4000
|
||||||
|
codec_preference: [avc1, mp4v, H264]
|
||||||
|
performance:
|
||||||
|
half: false
|
||||||
|
overlay_buffer_reuse: true
|
||||||
|
inference_stride: 2
|
||||||
|
stream:
|
||||||
|
enabled: true
|
||||||
|
shm_dir: /dev/shm
|
||||||
|
interval_frames: 5
|
||||||
|
feedback:
|
||||||
|
enabled: true
|
||||||
|
every_n_frames: 3000
|
||||||
|
save_images: true
|
||||||
|
log_to_terminal: true
|
||||||
|
roi:
|
||||||
|
inset_left_px: 60
|
||||||
|
inset_right_px: 60
|
||||||
|
inset_top_px: 0
|
||||||
|
inset_bottom_px: 0
|
||||||
|
min_overlap_ratio: 0.30
|
||||||
|
|
||||||
|
cameras:
|
||||||
|
CC1:
|
||||||
|
camera_num: 1
|
||||||
|
count_anchor: [780, 120]
|
||||||
|
roi:
|
||||||
|
points:
|
||||||
|
- [250, 330]
|
||||||
|
- [1650, 330]
|
||||||
|
- [1650, 720]
|
||||||
|
- [250, 720]
|
||||||
|
CC2:
|
||||||
|
camera_num: 2
|
||||||
|
count_anchor: [900, 120]
|
||||||
|
roi:
|
||||||
|
points:
|
||||||
|
- [20, 380]
|
||||||
|
- [1880, 380]
|
||||||
|
- [1880, 720]
|
||||||
|
- [20, 720]
|
||||||
|
CC3:
|
||||||
|
camera_num: 3
|
||||||
|
count_anchor: [900, 120]
|
||||||
|
roi:
|
||||||
|
points:
|
||||||
|
- [20, 330]
|
||||||
|
- [1880, 330]
|
||||||
|
- [1880, 720]
|
||||||
|
- [20, 720]
|
||||||
|
CC4:
|
||||||
|
camera_num: 4
|
||||||
|
count_anchor: [700, 120]
|
||||||
|
roi:
|
||||||
|
points:
|
||||||
|
- [50, 330]
|
||||||
|
- [1450, 330]
|
||||||
|
- [1450, 720]
|
||||||
|
- [50, 720]
|
||||||
Executable
+121
@@ -0,0 +1,121 @@
|
|||||||
|
batch:
|
||||||
|
root_dir: /media/jetson/DATA/.Codes/VIDEOS/cycle7/kandang-atas
|
||||||
|
camera_glob: "kandang_*_camera_{num}_*.mp4"
|
||||||
|
output_subdir: output
|
||||||
|
compress_max_mb: 200
|
||||||
|
delete_intermediate: false
|
||||||
|
checkpoint_every_n_frames: 3000
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
detection:
|
||||||
|
model_path: /media/jetson/DATA/.Codes/chicken-counting-sukawarna-det/chicken-detection-model-v26n-300e-best-2026-05-02-NEW.engine
|
||||||
|
classes: [0]
|
||||||
|
ignored_classes: [1, 2]
|
||||||
|
conf: 0.35
|
||||||
|
iou: 0.55
|
||||||
|
imgsz: 640
|
||||||
|
device: "0"
|
||||||
|
min_box_area_px: 3000
|
||||||
|
validate_while_inside: true
|
||||||
|
detection_zone:
|
||||||
|
enabled: true
|
||||||
|
buffer_above_px: 250
|
||||||
|
buffer_below_px: 250
|
||||||
|
show_in_overlay: true
|
||||||
|
tracker:
|
||||||
|
tracker_config_path: /media/jetson/DATA/.Codes/chicken-counting-sukawarna-det/configs/trackers/botsort_chicken.yaml
|
||||||
|
persist: true
|
||||||
|
track_buffer: 75
|
||||||
|
gate:
|
||||||
|
mode: two_line
|
||||||
|
lines_y: [420, 730]
|
||||||
|
direction: bottom_to_up
|
||||||
|
motion:
|
||||||
|
enabled: true
|
||||||
|
axis: vertical
|
||||||
|
forward_sign: 1.0
|
||||||
|
ema_alpha: 0.2
|
||||||
|
reverse_enter_threshold: -1.5
|
||||||
|
reverse_exit_threshold: -0.5
|
||||||
|
debounce_frames: 12
|
||||||
|
min_features: 60
|
||||||
|
max_corners: 80
|
||||||
|
stride_frames: 2
|
||||||
|
flow_scale: 0.5
|
||||||
|
quality_level: 0.01
|
||||||
|
min_distance: 8
|
||||||
|
block_radius: 6
|
||||||
|
overlay:
|
||||||
|
show_boxes: true
|
||||||
|
show_track_trails: false
|
||||||
|
trail_length: 20
|
||||||
|
show_center_marker: true
|
||||||
|
show_track_ring: false
|
||||||
|
count_anchor: [780, 120]
|
||||||
|
inside_box_only: true
|
||||||
|
pending_blink: true
|
||||||
|
pending_colors:
|
||||||
|
- [255, 255, 0]
|
||||||
|
- [0, 255, 255]
|
||||||
|
display:
|
||||||
|
show_window: false
|
||||||
|
encoder: auto
|
||||||
|
output_bitrate_kbps: 4000
|
||||||
|
codec_preference: [avc1, mp4v, H264]
|
||||||
|
performance:
|
||||||
|
half: false
|
||||||
|
overlay_buffer_reuse: true
|
||||||
|
inference_stride: 2
|
||||||
|
stream:
|
||||||
|
enabled: true
|
||||||
|
shm_dir: /dev/shm
|
||||||
|
interval_frames: 5
|
||||||
|
feedback:
|
||||||
|
enabled: true
|
||||||
|
every_n_frames: 3000
|
||||||
|
save_images: true
|
||||||
|
log_to_terminal: true
|
||||||
|
roi:
|
||||||
|
inset_left_px: 60
|
||||||
|
inset_right_px: 60
|
||||||
|
inset_top_px: 0
|
||||||
|
inset_bottom_px: 0
|
||||||
|
min_overlap_ratio: 0.30
|
||||||
|
|
||||||
|
cameras:
|
||||||
|
CC1:
|
||||||
|
camera_num: 1
|
||||||
|
count_anchor: [780, 120]
|
||||||
|
roi:
|
||||||
|
points:
|
||||||
|
- [250, 330]
|
||||||
|
- [1650, 330]
|
||||||
|
- [1650, 720]
|
||||||
|
- [250, 720]
|
||||||
|
CC2:
|
||||||
|
camera_num: 2
|
||||||
|
count_anchor: [900, 120]
|
||||||
|
roi:
|
||||||
|
points:
|
||||||
|
- [20, 380]
|
||||||
|
- [1880, 380]
|
||||||
|
- [1880, 720]
|
||||||
|
- [20, 720]
|
||||||
|
CC3:
|
||||||
|
camera_num: 3
|
||||||
|
count_anchor: [900, 120]
|
||||||
|
roi:
|
||||||
|
points:
|
||||||
|
- [20, 330]
|
||||||
|
- [1880, 330]
|
||||||
|
- [1880, 720]
|
||||||
|
- [20, 720]
|
||||||
|
CC4:
|
||||||
|
camera_num: 4
|
||||||
|
count_anchor: [700, 120]
|
||||||
|
roi:
|
||||||
|
points:
|
||||||
|
- [50, 330]
|
||||||
|
- [1450, 330]
|
||||||
|
- [1450, 720]
|
||||||
|
- [50, 720]
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
batch:
|
||||||
|
root_dir: /media/jetson/DATA/.Codes/VIDEOS/cycle7/kandang-atas
|
||||||
|
camera_glob: "kandang_*_camera_{num}_*.mp4"
|
||||||
|
output_subdir: output
|
||||||
|
compress_max_mb: 200
|
||||||
|
delete_intermediate: false
|
||||||
|
checkpoint_every_n_frames: 3000
|
||||||
|
location: kandang-atas
|
||||||
|
db_path: db/chicken_counts.db
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
detection:
|
||||||
|
model_path: /media/jetson/DATA/.Codes/chicken-counting-sukawarna-det/chicken-detection-model-v26n-300e-best-2026-05-02-NEW.engine
|
||||||
|
classes: [0]
|
||||||
|
ignored_classes: [1, 2]
|
||||||
|
conf: 0.55 # ↑ 0.35 → fewer false positives, less tracking CPU
|
||||||
|
iou: 0.55
|
||||||
|
imgsz: 640
|
||||||
|
device: "0"
|
||||||
|
min_box_area_px: 5000 # ↑ 3000 → filter small false positives
|
||||||
|
validate_while_inside: true
|
||||||
|
detection_zone:
|
||||||
|
enabled: true
|
||||||
|
buffer_above_px: 250
|
||||||
|
buffer_below_px: 250
|
||||||
|
show_in_overlay: true
|
||||||
|
tracker:
|
||||||
|
tracker_config_path: /media/jetson/DATA/.Codes/chicken-counting-sukawarna-det/configs/trackers/botsort_chicken.yaml
|
||||||
|
persist: true
|
||||||
|
track_buffer: 75
|
||||||
|
gate:
|
||||||
|
mode: two_line
|
||||||
|
lines_y: [420, 730]
|
||||||
|
direction: bottom_to_up
|
||||||
|
motion:
|
||||||
|
enabled: true
|
||||||
|
axis: vertical
|
||||||
|
forward_sign: 1.0
|
||||||
|
ema_alpha: 0.2
|
||||||
|
reverse_enter_threshold: -1.5
|
||||||
|
reverse_exit_threshold: -0.5
|
||||||
|
debounce_frames: 12
|
||||||
|
min_features: 40 # ↓ 60 → less optical flow computation
|
||||||
|
max_corners: 50 # ↓ 80 → fewer corner features
|
||||||
|
stride_frames: 3 # ↑ 2 → motion detection every 3rd frame
|
||||||
|
flow_scale: 0.5
|
||||||
|
quality_level: 0.01
|
||||||
|
min_distance: 8
|
||||||
|
block_radius: 6
|
||||||
|
overlay:
|
||||||
|
show_boxes: true
|
||||||
|
show_track_trails: false
|
||||||
|
trail_length: 20
|
||||||
|
show_center_marker: false # ✗ → save 1 circle per chicken
|
||||||
|
show_track_ring: false
|
||||||
|
count_anchor: [780, 120]
|
||||||
|
inside_box_only: true
|
||||||
|
validated_only: true # NEW → only draw counted chickens
|
||||||
|
pending_blink: true
|
||||||
|
pending_colors:
|
||||||
|
- [255, 255, 0]
|
||||||
|
- [0, 255, 255]
|
||||||
|
display:
|
||||||
|
show_window: false
|
||||||
|
encoder: auto
|
||||||
|
output_bitrate_kbps: 2000 # ↓ 4000 → faster encoding
|
||||||
|
codec_preference: [avc1, mp4v, H264]
|
||||||
|
performance:
|
||||||
|
half: false
|
||||||
|
overlay_buffer_reuse: true
|
||||||
|
inference_stride: 2
|
||||||
|
stream:
|
||||||
|
enabled: true # required for dashboard live feed
|
||||||
|
shm_dir: /dev/shm
|
||||||
|
interval_frames: 5
|
||||||
|
feedback:
|
||||||
|
enabled: true
|
||||||
|
every_n_frames: 3000
|
||||||
|
save_images: false # ✗ → disable checkpoint JPEG I/O spikes
|
||||||
|
log_to_terminal: true
|
||||||
|
roi:
|
||||||
|
inset_left_px: 60
|
||||||
|
inset_right_px: 60
|
||||||
|
inset_top_px: 0
|
||||||
|
inset_bottom_px: 0
|
||||||
|
min_overlap_ratio: 0.35 # ↑ 0.30 → stricter counting validation
|
||||||
|
|
||||||
|
cameras:
|
||||||
|
CC1:
|
||||||
|
camera_num: 1
|
||||||
|
count_anchor: [780, 120]
|
||||||
|
roi:
|
||||||
|
points:
|
||||||
|
- [250, 330]
|
||||||
|
- [1650, 330]
|
||||||
|
- [1650, 720]
|
||||||
|
- [250, 720]
|
||||||
|
CC2:
|
||||||
|
camera_num: 2
|
||||||
|
count_anchor: [900, 120]
|
||||||
|
detection: # per-camera override: wider angle = smaller bboxes
|
||||||
|
min_box_area_px: 3000
|
||||||
|
roi:
|
||||||
|
points:
|
||||||
|
- [20, 380]
|
||||||
|
- [1880, 380]
|
||||||
|
- [1880, 720]
|
||||||
|
- [20, 720]
|
||||||
|
CC3:
|
||||||
|
camera_num: 3
|
||||||
|
count_anchor: [900, 120]
|
||||||
|
roi:
|
||||||
|
points:
|
||||||
|
- [20, 330]
|
||||||
|
- [1880, 330]
|
||||||
|
- [1880, 720]
|
||||||
|
- [20, 720]
|
||||||
|
CC4:
|
||||||
|
camera_num: 4
|
||||||
|
count_anchor: [700, 120]
|
||||||
|
roi:
|
||||||
|
points:
|
||||||
|
- [50, 330]
|
||||||
|
- [1450, 330]
|
||||||
|
- [1450, 720]
|
||||||
|
- [50, 720]
|
||||||
+184
-178
@@ -1,15 +1,13 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Standalone live dashboard for chicken-counter pipeline.
|
"""Live dashboard for chicken-counter pipeline."""
|
||||||
|
|
||||||
Serve from project root:
|
|
||||||
PYTHONPATH=src python3 dashboard.py [--port 8080]
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import json
|
import json
|
||||||
import os
|
import sqlite3
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
from http.server import HTTPServer, SimpleHTTPRequestHandler
|
from http.server import HTTPServer, SimpleHTTPRequestHandler
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from socketserver import ThreadingMixIn
|
from socketserver import ThreadingMixIn
|
||||||
@@ -19,208 +17,202 @@ from urllib.parse import unquote, urlparse
|
|||||||
class ThreadingHTTPServer(ThreadingMixIn, HTTPServer):
|
class ThreadingHTTPServer(ThreadingMixIn, HTTPServer):
|
||||||
daemon_threads = True
|
daemon_threads = True
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_SHM_DIR = "/dev/shm"
|
DEFAULT_SHM_DIR = "/dev/shm"
|
||||||
DEFAULT_PORT = 8080
|
DEFAULT_PORT = 8080
|
||||||
|
TEMPLATE_DIR = Path(__file__).resolve().parent / "templates"
|
||||||
|
|
||||||
DASHBOARD_HTML = r"""<!DOCTYPE html>
|
_db_conn = None
|
||||||
<html lang="en">
|
_db_lock = threading.Lock()
|
||||||
<head>
|
_db_path = ""
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
||||||
<title>Chicken Counter — Live Dashboard</title>
|
|
||||||
<style>
|
|
||||||
*{margin:0;padding:0;box-sizing:border-box}
|
|
||||||
body{font-family:system-ui,monospace;background:#0f0f14;color:#e0e0e0;overflow:hidden}
|
|
||||||
#app{display:flex;height:100vh}
|
|
||||||
#sidebar{width:260px;background:#16161e;padding:16px;overflow-y:auto;flex-shrink:0}
|
|
||||||
#sidebar h1{font-size:18px;color:#80dc5a;margin-bottom:16px}
|
|
||||||
#sidebar .stat{margin-bottom:12px}
|
|
||||||
#sidebar .stat label{display:block;font-size:11px;color:#888;text-transform:uppercase;letter-spacing:1px}
|
|
||||||
#sidebar .stat .value{font-size:22px;font-weight:700;color:#e0e0e0}
|
|
||||||
#sidebar .stat .value.warn{color:#ff9f43}
|
|
||||||
#sidebar .stat .value.good{color:#80dc5a}
|
|
||||||
#cam-list{list-style:none;margin-top:16px}
|
|
||||||
#cam-list li{padding:8px 10px;margin:2px 0;border-radius:6px;cursor:pointer;font-size:13px;transition:background .2s}
|
|
||||||
#cam-list li:hover{background:#222}
|
|
||||||
#cam-list li.active{background:#1a3a2a;color:#80dc5a;font-weight:700}
|
|
||||||
#cam-list li .cam-badge{float:right;font-size:10px;padding:1px 6px;border-radius:8px;background:#222;color:#888}
|
|
||||||
#cam-list li.active .cam-badge{background:#2a5a3a;color:#80dc5a}
|
|
||||||
#main{flex:1;display:flex;flex-direction:column}
|
|
||||||
#frame-container{flex:1;display:flex;align-items:center;justify-content:center;background:#000;position:relative}
|
|
||||||
#frame-img{max-width:100%;max-height:100%;object-fit:contain}
|
|
||||||
#no-frame{color:#555;font-size:18px}
|
|
||||||
#top-bar{display:flex;justify-content:space-between;align-items:center;padding:10px 16px;background:#16161e;font-size:12px}
|
|
||||||
#top-bar .refresh{color:#888}
|
|
||||||
#top-bar .status-dot{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:6px}
|
|
||||||
#top-bar .status-dot.online{background:#80dc5a;box-shadow:0 0 6px #80dc5a}
|
|
||||||
#top-bar .status-dot.offline{background:#555}
|
|
||||||
.refresh-btn{padding:4px 12px;border-radius:4px;background:#222;border:1px solid #444;color:#ccc;cursor:pointer;font-size:11px}
|
|
||||||
.refresh-btn:hover{background:#333}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="app">
|
|
||||||
<div id="sidebar">
|
|
||||||
<h1>🐔 Chicken Counter</h1>
|
|
||||||
<div class="stat"><label>Total Entered</label><div class="value good" id="stat-total">--</div></div>
|
|
||||||
<div class="stat"><label>Inside Box</label><div class="value" id="stat-inside">--</div></div>
|
|
||||||
<div class="stat"><label>Tracks</label><div class="value" id="stat-tracks">--</div></div>
|
|
||||||
<div class="stat"><label>Frame</label><div class="value" id="stat-frame">--</div></div>
|
|
||||||
<div class="stat"><label>Motion Speed</label><div class="value" id="stat-speed">--</div></div>
|
|
||||||
<div class="stat"><label>Status</label><div class="value" id="stat-status">--</div></div>
|
|
||||||
<ul id="cam-list"></ul>
|
|
||||||
</div>
|
|
||||||
<div id="main">
|
|
||||||
<div id="top-bar">
|
|
||||||
<span><span class="status-dot" id="status-dot"></span><span id="status-text">waiting for pipeline...</span></span>
|
|
||||||
<span><span class="refresh" id="refresh-counter"></span> ago
|
|
||||||
<button class="refresh-btn" onclick="load()">↻ Refresh</button></span>
|
|
||||||
</div>
|
|
||||||
<div id="frame-container">
|
|
||||||
<img id="frame-img" alt="live stream">
|
|
||||||
<div id="no-frame"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<script>
|
|
||||||
var POLL_MS = %%POLL_MS%%;
|
|
||||||
var SHM = "%%SHM_DIR%%";
|
|
||||||
var cameras = [];
|
|
||||||
var activeCam = null;
|
|
||||||
var lastUpdate = 0;
|
|
||||||
var img = document.getElementById("frame-img");
|
|
||||||
var noFrame = document.getElementById("no-frame");
|
|
||||||
|
|
||||||
function loadCameras() {{
|
|
||||||
fetch("/api/cameras").then(r => r.json()).then(data => {{
|
|
||||||
cameras = data.cameras || [];
|
|
||||||
renderCamList();
|
|
||||||
if (cameras.length && !activeCam) selectCam(cameras[0]);
|
|
||||||
if (!cameras.length) {{ noFrame.textContent = "No cameras found in " + SHM; img.style.display = "none"; }}
|
|
||||||
}});
|
|
||||||
}}
|
|
||||||
|
|
||||||
function renderCamList() {{
|
def _get_db():
|
||||||
var ul = document.getElementById("cam-list");
|
global _db_conn
|
||||||
ul.innerHTML = cameras.map(function(c) {{
|
if not _db_path:
|
||||||
return '<li class="' + (c === activeCam ? "active" : "") + '" onclick="selectCam(\'' + c + '\')">' +
|
return None
|
||||||
c + '<span class="cam-badge">▶</span></li>';
|
with _db_lock:
|
||||||
}}).join("");
|
if _db_conn is None:
|
||||||
}}
|
_db_conn = sqlite3.connect(_db_path, check_same_thread=False)
|
||||||
|
_db_conn.row_factory = sqlite3.Row
|
||||||
|
_db_conn.execute("PRAGMA journal_mode=WAL")
|
||||||
|
_db_conn.execute("PRAGMA cache_size=-8000")
|
||||||
|
return _db_conn
|
||||||
|
|
||||||
function selectCam(id) {{
|
|
||||||
activeCam = id;
|
|
||||||
renderCamList();
|
|
||||||
load();
|
|
||||||
}}
|
|
||||||
|
|
||||||
function load() {{
|
def _init_db(db_path: str) -> None:
|
||||||
if (!activeCam) return;
|
conn = sqlite3.connect(db_path)
|
||||||
var t = Date.now();
|
conn.execute("PRAGMA journal_mode=WAL")
|
||||||
img.src = "/shm/" + activeCam + "/frame.jpg?t=" + t;
|
conn.execute("""CREATE TABLE IF NOT EXISTS batch_runs (
|
||||||
fetch("/shm/" + activeCam + "/stats.json?t=" + t).then(function(r) {{
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
if (!r.ok) {{ setOffline(); return; }}
|
date TEXT NOT NULL, location TEXT NOT NULL, camera_id TEXT NOT NULL,
|
||||||
return r.json();
|
total_entered INTEGER NOT NULL DEFAULT 0,
|
||||||
}}).then(function(s) {{
|
frames_processed INTEGER NOT NULL DEFAULT 0,
|
||||||
if (!s) return;
|
elapsed_seconds REAL NOT NULL DEFAULT 0.0,
|
||||||
lastUpdate = Date.now();
|
stopped_reason TEXT NOT NULL DEFAULT '',
|
||||||
document.getElementById("stat-total").textContent = s.total_entered_count;
|
source_video TEXT NOT NULL DEFAULT '',
|
||||||
document.getElementById("stat-inside").textContent = s.inside_box_count;
|
generated_at TEXT NOT NULL DEFAULT '',
|
||||||
document.getElementById("stat-tracks").textContent = s.track_count;
|
UNIQUE(date, location, camera_id))""")
|
||||||
document.getElementById("stat-frame").textContent = s.frame_index;
|
conn.commit()
|
||||||
document.getElementById("stat-speed").textContent = s.smoothed_speed;
|
conn.close()
|
||||||
document.getElementById("stat-status").textContent = s.backward_active ? "BACKWARD STOP" : "RUNNING";
|
|
||||||
var el = document.getElementById("stat-status");
|
|
||||||
el.className = "value" + (s.backward_active ? " warn" : " good");
|
|
||||||
document.getElementById("status-dot").className = "status-dot online";
|
|
||||||
document.getElementById("status-text").textContent = activeCam + " \u2022 frame " + s.frame_index;
|
|
||||||
}});
|
|
||||||
}}
|
|
||||||
|
|
||||||
function setOffline() {{
|
|
||||||
document.getElementById("status-dot").className = "status-dot offline";
|
|
||||||
document.getElementById("status-text").textContent = activeCam + " \u2022 offline";
|
|
||||||
}}
|
|
||||||
|
|
||||||
function updateRefresh() {{
|
def _discover_cameras(shm_dir):
|
||||||
var ago = Math.round((Date.now() - lastUpdate) / 1000);
|
shm = Path(shm_dir)
|
||||||
document.getElementById("refresh-counter").textContent = ago + "s";
|
cameras = []
|
||||||
}}
|
if shm.is_dir():
|
||||||
|
for entry in sorted(shm.iterdir()):
|
||||||
img.onerror = function() {{ img.style.display = "none"; noFrame.style.display = "block"; noFrame.textContent = "Waiting for frame..."; }};
|
if entry.is_dir() and entry.name.startswith("chicken_counter_"):
|
||||||
img.onload = function() {{ img.style.display = "block"; noFrame.style.display = "none"; }};
|
cameras.append(entry.name[len("chicken_counter_"):])
|
||||||
|
return cameras
|
||||||
setInterval(function() {{ load(); }}, POLL_MS);
|
|
||||||
setInterval(loadCameras, 3000);
|
|
||||||
setInterval(updateRefresh, 1000);
|
|
||||||
loadCameras();
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>"""
|
|
||||||
|
|
||||||
|
|
||||||
class DashboardHandler(SimpleHTTPRequestHandler):
|
class DashboardHandler(SimpleHTTPRequestHandler):
|
||||||
shm_dir = DEFAULT_SHM_DIR
|
shm_dir = DEFAULT_SHM_DIR
|
||||||
poll_ms = 500
|
poll_ms = 1000
|
||||||
|
run_date = ""
|
||||||
|
|
||||||
def log_message(self, format, *args):
|
def log_message(self, format, *args):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def do_GET(self):
|
def do_GET(self):
|
||||||
try:
|
try:
|
||||||
self._handle_request()
|
self._handle()
|
||||||
except (BrokenPipeError, ConnectionResetError):
|
except (BrokenPipeError, ConnectionResetError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _handle_request(self):
|
def _handle(self):
|
||||||
parsed = urlparse(self.path)
|
parsed = urlparse(self.path)
|
||||||
path = unquote(parsed.path)
|
path = unquote(parsed.path)
|
||||||
|
|
||||||
if path == "/" or path == "/index.html":
|
if path == "/":
|
||||||
html = DASHBOARD_HTML.replace("%%POLL_MS%%", str(self.poll_ms)).replace("%%SHM_DIR%%", self.shm_dir)
|
self._serve_html()
|
||||||
self._send_html(html)
|
return
|
||||||
|
|
||||||
|
if path.startswith("/stream/"):
|
||||||
|
self._handle_stream(path)
|
||||||
return
|
return
|
||||||
|
|
||||||
if path == "/api/cameras":
|
if path == "/api/cameras":
|
||||||
cameras = self._discover_cameras()
|
self._send_json({"cameras": _discover_cameras(self.shm_dir)})
|
||||||
self._send_json({"cameras": cameras})
|
return
|
||||||
|
|
||||||
|
if path.startswith("/api/db/"):
|
||||||
|
self._handle_db(path)
|
||||||
return
|
return
|
||||||
|
|
||||||
if path.startswith("/shm/"):
|
if path.startswith("/shm/"):
|
||||||
rel = path[len("/shm/"):]
|
self._handle_shm(path)
|
||||||
parts = rel.split("/", 1)
|
|
||||||
if len(parts) >= 1:
|
|
||||||
parts[0] = f"chicken_counter_{parts[0]}"
|
|
||||||
rel = "/".join(parts)
|
|
||||||
shm_path = Path(self.shm_dir) / rel
|
|
||||||
resolved = shm_path.resolve()
|
|
||||||
if not str(resolved).startswith(str(Path(self.shm_dir).resolve())):
|
|
||||||
self.send_error(403)
|
|
||||||
return
|
|
||||||
if not resolved.exists():
|
|
||||||
self.send_error(404)
|
|
||||||
return
|
|
||||||
ct = "image/jpeg" if resolved.suffix in (".jpg", ".jpeg") else "application/json"
|
|
||||||
self.send_response(200)
|
|
||||||
self.send_header("Content-Type", ct)
|
|
||||||
self.send_header("Cache-Control", "no-cache, no-store, must-revalidate")
|
|
||||||
self.end_headers()
|
|
||||||
self.wfile.write(resolved.read_bytes())
|
|
||||||
return
|
return
|
||||||
|
|
||||||
self.send_error(404)
|
self._send_error(404)
|
||||||
|
|
||||||
def _discover_cameras(self):
|
def _handle_stream(self, path):
|
||||||
shm = Path(self.shm_dir)
|
cam_id = path[len("/stream/"):]
|
||||||
cameras = []
|
frame_path = Path(self.shm_dir) / f"chicken_counter_{cam_id}" / "frame.jpg"
|
||||||
if shm.is_dir():
|
if not frame_path.exists():
|
||||||
for entry in sorted(shm.iterdir()):
|
self._send_error(404)
|
||||||
if entry.is_dir() and entry.name.startswith("chicken_counter_"):
|
return
|
||||||
cam_id = entry.name[len("chicken_counter_"):]
|
|
||||||
cameras.append(cam_id)
|
|
||||||
return cameras
|
|
||||||
|
|
||||||
def _send_html(self, html: str):
|
self.send_response(200)
|
||||||
|
self.send_header("Content-Type", "multipart/x-mixed-replace; boundary=frame")
|
||||||
|
self.send_header("Cache-Control", "no-cache")
|
||||||
|
self.end_headers()
|
||||||
|
|
||||||
|
last_mtime = 0
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
mtime = frame_path.stat().st_mtime
|
||||||
|
if mtime != last_mtime:
|
||||||
|
last_mtime = mtime
|
||||||
|
data = frame_path.read_bytes()
|
||||||
|
self.wfile.write(
|
||||||
|
b"--frame\r\n"
|
||||||
|
b"Content-Type: image/jpeg\r\n"
|
||||||
|
b"Content-Length: " + str(len(data)).encode() + b"\r\n\r\n" +
|
||||||
|
data + b"\r\n"
|
||||||
|
)
|
||||||
|
self.wfile.flush()
|
||||||
|
except (FileNotFoundError, OSError):
|
||||||
|
pass
|
||||||
|
time.sleep(0.1)
|
||||||
|
except (BrokenPipeError, ConnectionResetError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _handle_shm(self, path):
|
||||||
|
rel = path[len("/shm/"):]
|
||||||
|
parts = rel.split("/", 1)
|
||||||
|
if len(parts) < 2:
|
||||||
|
self._send_error(404)
|
||||||
|
return
|
||||||
|
|
||||||
|
cam_id = parts[0]
|
||||||
|
file = parts[1]
|
||||||
|
fpath = Path(self.shm_dir) / f"chicken_counter_{cam_id}" / file
|
||||||
|
|
||||||
|
if str(fpath.resolve()).startswith(str(Path(self.shm_dir).resolve())):
|
||||||
|
if fpath.exists():
|
||||||
|
ct = "image/jpeg" if file.endswith(".jpg") else "application/json"
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header("Content-Type", ct)
|
||||||
|
self.send_header("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(fpath.read_bytes())
|
||||||
|
return
|
||||||
|
self._send_error(404)
|
||||||
|
|
||||||
|
def _handle_db(self, path):
|
||||||
|
conn = _get_db()
|
||||||
|
if not conn:
|
||||||
|
self._send_json({})
|
||||||
|
return
|
||||||
|
|
||||||
|
if path == "/api/db/summary":
|
||||||
|
row = conn.execute("SELECT COUNT(DISTINCT date) AS days, COUNT(DISTINCT location) AS locations, COUNT(*) AS total_runs, SUM(total_entered) AS total_chickens, ROUND(SUM(elapsed_seconds)/3600.0,1) AS total_hours FROM batch_runs").fetchone()
|
||||||
|
self._send_json(dict(row))
|
||||||
|
return
|
||||||
|
|
||||||
|
if path == "/api/db/history":
|
||||||
|
rows = conn.execute("SELECT date, location, COUNT(*) AS cams, SUM(total_entered) AS total, ROUND(SUM(elapsed_seconds)/60.0,1) AS minutes FROM batch_runs GROUP BY date, location ORDER BY date DESC, location LIMIT 50").fetchall()
|
||||||
|
self._send_json([dict(r) for r in rows])
|
||||||
|
return
|
||||||
|
|
||||||
|
# /api/db/date/<date>
|
||||||
|
prefix = "/api/db/date/"
|
||||||
|
if path.startswith(prefix):
|
||||||
|
date = path[len(prefix):]
|
||||||
|
cameras = conn.execute("SELECT camera_id, total_entered, frames_processed, ROUND(elapsed_seconds,1) AS elapsed_seconds, stopped_reason, source_video, location FROM batch_runs WHERE date=? ORDER BY camera_id", (date,)).fetchall()
|
||||||
|
total = conn.execute("SELECT SUM(total_entered) AS total, ROUND(SUM(elapsed_seconds)/60.0,1) AS minutes FROM batch_runs WHERE date=?", (date,)).fetchone()
|
||||||
|
self._send_json({"date": date, "total": dict(total), "cameras": [dict(r) for r in cameras]})
|
||||||
|
return
|
||||||
|
|
||||||
|
# /api/db/camera/<id>
|
||||||
|
prefix = "/api/db/camera/"
|
||||||
|
if path.startswith(prefix):
|
||||||
|
cam_id = path[len(prefix):]
|
||||||
|
rows = conn.execute("SELECT date, location, total_entered, frames_processed, ROUND(elapsed_seconds,1) AS elapsed_seconds, stopped_reason FROM batch_runs WHERE camera_id=? ORDER BY date DESC LIMIT 50", (cam_id,)).fetchall()
|
||||||
|
self._send_json([dict(r) for r in rows])
|
||||||
|
return
|
||||||
|
|
||||||
|
# /api/db/location/<name>
|
||||||
|
prefix = "/api/db/location/"
|
||||||
|
if path.startswith(prefix):
|
||||||
|
loc = path[len(prefix):]
|
||||||
|
history = conn.execute("SELECT date, GROUP_CONCAT(camera_id,', ') AS cameras, SUM(total_entered) AS total, ROUND(SUM(elapsed_seconds)/60.0,1) AS minutes FROM batch_runs WHERE location=? GROUP BY date ORDER BY date DESC LIMIT 50", (loc,)).fetchall()
|
||||||
|
summary = conn.execute("SELECT COUNT(DISTINCT date) AS days, SUM(total_entered) AS total, ROUND(SUM(elapsed_seconds)/3600.0,1) AS hours FROM batch_runs WHERE location=?", (loc,)).fetchone()
|
||||||
|
self._send_json({"location": loc, "summary": dict(summary), "history": [dict(r) for r in history]})
|
||||||
|
return
|
||||||
|
|
||||||
|
self._send_json({})
|
||||||
|
|
||||||
|
def _serve_html(self):
|
||||||
|
html_path = TEMPLATE_DIR / "index.html"
|
||||||
|
html = html_path.read_text(encoding="utf-8")
|
||||||
|
html = html.replace("{{ poll_ms }}", str(self.poll_ms))
|
||||||
|
html = html.replace("{{ shm_dir }}", self.shm_dir)
|
||||||
|
html = html.replace("{{ date }}", self.run_date or "today")
|
||||||
|
html = html.replace("{{ db_path }}", _db_path)
|
||||||
data = html.encode("utf-8")
|
data = html.encode("utf-8")
|
||||||
self.send_response(200)
|
self.send_response(200)
|
||||||
self.send_header("Content-Type", "text/html; charset=utf-8")
|
self.send_header("Content-Type", "text/html; charset=utf-8")
|
||||||
@@ -236,20 +228,34 @@ class DashboardHandler(SimpleHTTPRequestHandler):
|
|||||||
self.end_headers()
|
self.end_headers()
|
||||||
self.wfile.write(data)
|
self.wfile.write(data)
|
||||||
|
|
||||||
|
def _send_error(self, code):
|
||||||
|
self.send_response(code)
|
||||||
|
self.send_header("Content-Length", "0")
|
||||||
|
self.end_headers()
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
global _db_path
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(description="Chicken Counter live dashboard")
|
parser = argparse.ArgumentParser(description="Chicken Counter live dashboard")
|
||||||
parser.add_argument("--port", type=int, default=DEFAULT_PORT, help=f"HTTP port (default: {DEFAULT_PORT})")
|
parser.add_argument("--port", type=int, default=DEFAULT_PORT)
|
||||||
parser.add_argument("--shm-dir", default=DEFAULT_SHM_DIR, help=f"Shared memory directory (default: {DEFAULT_SHM_DIR})")
|
parser.add_argument("--shm-dir", default=DEFAULT_SHM_DIR)
|
||||||
parser.add_argument("--poll-ms", type=int, default=500, help="Image poll interval in ms (default: 500)")
|
parser.add_argument("--poll-ms", type=int, default=1000)
|
||||||
|
parser.add_argument("--date", default="")
|
||||||
|
parser.add_argument("--db", default="db/chicken_counts.db")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
DashboardHandler.shm_dir = args.shm_dir
|
DashboardHandler.shm_dir = args.shm_dir
|
||||||
DashboardHandler.poll_ms = args.poll_ms
|
DashboardHandler.poll_ms = args.poll_ms
|
||||||
|
DashboardHandler.run_date = args.date
|
||||||
|
_db_path = str(Path(args.db).resolve()) if args.db else ""
|
||||||
|
|
||||||
|
if _db_path:
|
||||||
|
_init_db(_db_path)
|
||||||
|
|
||||||
server = ThreadingHTTPServer(("0.0.0.0", args.port), DashboardHandler)
|
server = ThreadingHTTPServer(("0.0.0.0", args.port), DashboardHandler)
|
||||||
print(f"[dashboard] serving at http://0.0.0.0:{args.port}")
|
date_info = f" date={args.date}" if args.date else ""
|
||||||
print(f"[dashboard] shm_dir={args.shm_dir} poll={args.poll_ms}ms")
|
print(f"[dashboard] http://0.0.0.0:{args.port} shm={args.shm_dir} db={args.db}{date_info}")
|
||||||
try:
|
try:
|
||||||
server.serve_forever()
|
server.serve_forever()
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Whitespace-only changes.
@@ -0,0 +1,78 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Export a YOLO .pt model to TensorRT .engine.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python3 export_engine.py chicken-detection-model-v26n-300e-best-2026-05-02-NEW.pt
|
||||||
|
python3 export_engine.py model.pt --imgsz 640 --half --workspace 4
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def export_engine(
|
||||||
|
model_path: str | Path,
|
||||||
|
*,
|
||||||
|
imgsz: int = 640,
|
||||||
|
half: bool = True,
|
||||||
|
int8: bool = False,
|
||||||
|
batch: int = 1,
|
||||||
|
workspace: int = 4, # GB
|
||||||
|
simplify: bool = True,
|
||||||
|
opset: int = 17,
|
||||||
|
verbose: bool = True,
|
||||||
|
) -> str:
|
||||||
|
from ultralytics import YOLO
|
||||||
|
|
||||||
|
model = YOLO(model_path, task="detect")
|
||||||
|
|
||||||
|
output = model.export(
|
||||||
|
format="engine",
|
||||||
|
imgsz=imgsz,
|
||||||
|
half=half,
|
||||||
|
int8=int8,
|
||||||
|
batch=batch,
|
||||||
|
workspace=workspace,
|
||||||
|
simplify=simplify,
|
||||||
|
opset=opset,
|
||||||
|
verbose=verbose,
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"\nExported to: {output}")
|
||||||
|
return str(output)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description="Export YOLO .pt → TensorRT .engine")
|
||||||
|
parser.add_argument("model", help="Path to .pt model file")
|
||||||
|
parser.add_argument("--imgsz", type=int, default=640, help="Input image size (default: 640)")
|
||||||
|
parser.add_argument("--half", action="store_true", default=True, help="FP16 precision (default: on)")
|
||||||
|
parser.add_argument("--no-half", dest="half", action="store_false", help="FP32 precision")
|
||||||
|
parser.add_argument("--int8", action="store_true", help="INT8 quantization (needs calibration)")
|
||||||
|
parser.add_argument("--batch", type=int, default=1, help="Batch size (default: 1)")
|
||||||
|
parser.add_argument("--workspace", type=int, default=4, help="GPU workspace in GB (default: 4)")
|
||||||
|
parser.add_argument("--opset", type=int, default=17, help="ONNX opset version (default: 17)")
|
||||||
|
parser.add_argument("--quiet", action="store_true", help="Suppress verbose output")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if not Path(args.model).exists():
|
||||||
|
print(f"error: model file not found: {args.model}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
export_engine(
|
||||||
|
args.model,
|
||||||
|
imgsz=args.imgsz,
|
||||||
|
half=args.half,
|
||||||
|
int8=args.int8,
|
||||||
|
batch=args.batch,
|
||||||
|
workspace=args.workspace,
|
||||||
|
opset=args.opset,
|
||||||
|
verbose=not args.quiet,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import shutil
|
||||||
from datetime import date as date_type
|
from datetime import date as date_type
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -10,10 +11,62 @@ from chicken_counter.compress import compress_video_to_target
|
|||||||
from chicken_counter.config import BatchSettings, build_camera_config_from_batch
|
from chicken_counter.config import BatchSettings, build_camera_config_from_batch
|
||||||
from chicken_counter.pipeline import run_pipeline
|
from chicken_counter.pipeline import run_pipeline
|
||||||
from chicken_counter.report import build_batch_report, persist_batch_reports
|
from chicken_counter.report import build_batch_report, persist_batch_reports
|
||||||
from chicken_counter.tracking import DetectionTracker
|
|
||||||
from chicken_counter.types import CameraBatchResult
|
from chicken_counter.types import CameraBatchResult
|
||||||
|
|
||||||
|
|
||||||
|
def _store_to_db(report_path: Path, location: str, db_path: str) -> None:
|
||||||
|
if not location or not db_path:
|
||||||
|
return
|
||||||
|
import json
|
||||||
|
import sqlite3
|
||||||
|
try:
|
||||||
|
with open(report_path) as f:
|
||||||
|
report = json.load(f)
|
||||||
|
except Exception:
|
||||||
|
return
|
||||||
|
|
||||||
|
Path(db_path).parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
conn = sqlite3.connect(db_path)
|
||||||
|
conn.execute("PRAGMA journal_mode=WAL")
|
||||||
|
conn.execute("""CREATE TABLE IF NOT EXISTS batch_runs (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
date TEXT NOT NULL, location TEXT NOT NULL, camera_id TEXT NOT NULL,
|
||||||
|
total_entered INTEGER NOT NULL DEFAULT 0,
|
||||||
|
frames_processed INTEGER NOT NULL DEFAULT 0,
|
||||||
|
elapsed_seconds REAL NOT NULL DEFAULT 0.0,
|
||||||
|
stopped_reason TEXT NOT NULL DEFAULT '',
|
||||||
|
source_video TEXT NOT NULL DEFAULT '',
|
||||||
|
generated_at TEXT NOT NULL DEFAULT '',
|
||||||
|
UNIQUE(date, location, camera_id))""")
|
||||||
|
|
||||||
|
date = report["date"]
|
||||||
|
for camera_id, entry in report["cameras"].items():
|
||||||
|
if entry.get("skipped"):
|
||||||
|
continue
|
||||||
|
conn.execute("""INSERT INTO batch_runs
|
||||||
|
(date, location, camera_id, total_entered, frames_processed,
|
||||||
|
elapsed_seconds, stopped_reason, source_video, generated_at)
|
||||||
|
VALUES (?,?,?,?,?,?,?,?,?)
|
||||||
|
ON CONFLICT(date, location, camera_id) DO UPDATE SET
|
||||||
|
total_entered=excluded.total_entered,
|
||||||
|
frames_processed=excluded.frames_processed,
|
||||||
|
elapsed_seconds=excluded.elapsed_seconds,
|
||||||
|
stopped_reason=excluded.stopped_reason,
|
||||||
|
source_video=excluded.source_video,
|
||||||
|
generated_at=excluded.generated_at""",
|
||||||
|
(date, location, camera_id,
|
||||||
|
entry.get("total_entered", 0),
|
||||||
|
entry.get("frames_processed", 0),
|
||||||
|
entry.get("elapsed_seconds", 0),
|
||||||
|
entry.get("stopped_reason", ""),
|
||||||
|
entry.get("source_video", ""),
|
||||||
|
report.get("generated_at", "")))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
print(f"[db] stored {date} ({location}) → {db_path}")
|
||||||
|
|
||||||
|
|
||||||
def run_daily_batch(settings: BatchSettings, date: str | None = None, *, verbose: bool = False, no_video: bool = False, show_progress: bool = False) -> Path:
|
def run_daily_batch(settings: BatchSettings, date: str | None = None, *, verbose: bool = False, no_video: bool = False, show_progress: bool = False) -> Path:
|
||||||
run_date = date or date_type.today().isoformat()
|
run_date = date or date_type.today().isoformat()
|
||||||
day_dir = Path(settings.batch.root_dir) / run_date
|
day_dir = Path(settings.batch.root_dir) / run_date
|
||||||
@@ -23,26 +76,19 @@ def run_daily_batch(settings: BatchSettings, date: str | None = None, *, verbose
|
|||||||
print(f"[batch] starting daily run for {run_date}")
|
print(f"[batch] starting daily run for {run_date}")
|
||||||
print(f"[batch] input folder: {day_dir}")
|
print(f"[batch] input folder: {day_dir}")
|
||||||
print(f"[batch] output folder: {output_dir}")
|
print(f"[batch] output folder: {output_dir}")
|
||||||
|
|
||||||
|
# Clean all /dev/shm counters from previous runs
|
||||||
|
shm_dir = Path("/dev/shm")
|
||||||
|
for d in shm_dir.glob("chicken_counter_*"):
|
||||||
|
if d.is_dir():
|
||||||
|
shutil.rmtree(str(d))
|
||||||
|
print(f"[batch] cleaned {d}")
|
||||||
if no_video:
|
if no_video:
|
||||||
print("[batch] --no-video: skipping video output, overlay, and compression")
|
print("[batch] --no-video: skipping video output, overlay, and compression")
|
||||||
|
|
||||||
discovery = discover_camera_videos(day_dir, settings)
|
discovery = discover_camera_videos(day_dir, settings)
|
||||||
camera_order = sorted(settings.cameras.items(), key=lambda item: item[1].camera_num)
|
camera_order = sorted(settings.cameras.items(), key=lambda item: item[1].camera_num)
|
||||||
|
|
||||||
first_camera_id = next(
|
|
||||||
camera_id for camera_id, _preset in camera_order if camera_id in discovery.found
|
|
||||||
)
|
|
||||||
first_source = discovery.found[first_camera_id]
|
|
||||||
init_output_path = output_dir / f"{first_camera_id}_vis.mp4" if not no_video else None
|
|
||||||
init_config = build_camera_config_from_batch(
|
|
||||||
settings,
|
|
||||||
first_camera_id,
|
|
||||||
source=first_source,
|
|
||||||
output_path=init_output_path,
|
|
||||||
checkpoint_dir=output_dir / "checkpoints" / first_camera_id,
|
|
||||||
)
|
|
||||||
shared_tracker = DetectionTracker(init_config)
|
|
||||||
|
|
||||||
camera_results: list[CameraBatchResult] = []
|
camera_results: list[CameraBatchResult] = []
|
||||||
report_path = output_dir / f"counts_{run_date}.json"
|
report_path = output_dir / f"counts_{run_date}.json"
|
||||||
|
|
||||||
@@ -58,6 +104,7 @@ def run_daily_batch(settings: BatchSettings, date: str | None = None, *, verbose
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
persist_batch_reports(run_date, camera_results, output_dir)
|
persist_batch_reports(run_date, camera_results, output_dir)
|
||||||
|
_store_to_db(report_path, settings.batch.location, settings.batch.db_path)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
source_path = discovery.found[camera_id]
|
source_path = discovery.found[camera_id]
|
||||||
@@ -73,7 +120,7 @@ def run_daily_batch(settings: BatchSettings, date: str | None = None, *, verbose
|
|||||||
checkpoint_dir=checkpoint_dir,
|
checkpoint_dir=checkpoint_dir,
|
||||||
)
|
)
|
||||||
camera_config.performance.verbose = verbose
|
camera_config.performance.verbose = verbose
|
||||||
pipeline_result = run_pipeline(camera_config, tracker=shared_tracker, show_progress=show_progress)
|
pipeline_result = run_pipeline(camera_config, show_progress=show_progress, run_date=run_date)
|
||||||
camera_results.append(
|
camera_results.append(
|
||||||
CameraBatchResult(
|
CameraBatchResult(
|
||||||
camera_id=camera_id,
|
camera_id=camera_id,
|
||||||
@@ -85,13 +132,16 @@ def run_daily_batch(settings: BatchSettings, date: str | None = None, *, verbose
|
|||||||
f"frames={pipeline_result.frames_processed} reason={pipeline_result.stopped_reason}"
|
f"frames={pipeline_result.frames_processed} reason={pipeline_result.stopped_reason}"
|
||||||
)
|
)
|
||||||
persist_batch_reports(run_date, camera_results, output_dir)
|
persist_batch_reports(run_date, camera_results, output_dir)
|
||||||
|
_store_to_db(report_path, settings.batch.location, settings.batch.db_path)
|
||||||
|
|
||||||
if no_video:
|
if no_video:
|
||||||
|
persist_batch_reports(run_date, camera_results, output_dir)
|
||||||
report = build_batch_report(run_date, camera_results, output_dir=output_dir)
|
report = build_batch_report(run_date, camera_results, output_dir=output_dir)
|
||||||
print(
|
print(
|
||||||
f"[batch] complete for {run_date}: total_entered_sum={report.total_entered_sum} "
|
f"[batch] complete for {run_date}: total_entered_sum={report.total_entered_sum} "
|
||||||
f"report={report_path}"
|
f"report={report_path}"
|
||||||
)
|
)
|
||||||
|
_store_to_db(report_path, settings.batch.location, settings.batch.db_path)
|
||||||
return report_path
|
return report_path
|
||||||
|
|
||||||
print("[batch] all cameras complete; starting compression")
|
print("[batch] all cameras complete; starting compression")
|
||||||
@@ -120,4 +170,5 @@ def run_daily_batch(settings: BatchSettings, date: str | None = None, *, verbose
|
|||||||
f"[batch] complete for {run_date}: total_entered_sum={report.total_entered_sum} "
|
f"[batch] complete for {run_date}: total_entered_sum={report.total_entered_sum} "
|
||||||
f"report={report_path}"
|
f"report={report_path}"
|
||||||
)
|
)
|
||||||
|
_store_to_db(report_path, settings.batch.location, settings.batch.db_path)
|
||||||
return report_path
|
return report_path
|
||||||
@@ -212,6 +212,8 @@ class BatchConfig:
|
|||||||
compress_max_mb: int = 200
|
compress_max_mb: int = 200
|
||||||
delete_intermediate: bool = False
|
delete_intermediate: bool = False
|
||||||
checkpoint_every_n_frames: int = 3000
|
checkpoint_every_n_frames: int = 3000
|
||||||
|
location: str = ""
|
||||||
|
db_path: str = ""
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|||||||
@@ -51,12 +51,30 @@ class CountingZone:
|
|||||||
counting_paused: bool = False,
|
counting_paused: bool = False,
|
||||||
) -> list[CountEvent]:
|
) -> list[CountEvent]:
|
||||||
events: list[CountEvent] = []
|
events: list[CountEvent] = []
|
||||||
|
|
||||||
|
if not tracks:
|
||||||
|
self.inside_box_count = 0
|
||||||
|
self.current_inside_ids.clear()
|
||||||
|
self.prev_inside_ids.clear()
|
||||||
|
self._purge_stale(frame_index, set())
|
||||||
|
return events
|
||||||
|
|
||||||
active_ids = set()
|
active_ids = set()
|
||||||
inside_ids = set()
|
inside_ids = set()
|
||||||
|
|
||||||
|
rx1, ry1, rx2, ry2 = self._counting_rect
|
||||||
|
|
||||||
for track in tracks:
|
for track in tracks:
|
||||||
active_ids.add(track.track_id)
|
active_ids.add(track.track_id)
|
||||||
self.last_seen_frame[track.track_id] = frame_index
|
self.last_seen_frame[track.track_id] = frame_index
|
||||||
self.histories[track.track_id].append(track.centroid)
|
|
||||||
|
if self.trail_length > 0:
|
||||||
|
self.histories[track.track_id].append(track.centroid)
|
||||||
|
|
||||||
|
# fast-reject: bounding rect check before pointPolygonTest
|
||||||
|
cx, cy = track.centroid
|
||||||
|
if not (rx1 <= cx <= rx2 and ry1 <= cy <= ry2):
|
||||||
|
continue
|
||||||
|
|
||||||
if self._inside_roi(track.centroid):
|
if self._inside_roi(track.centroid):
|
||||||
inside_ids.add(track.track_id)
|
inside_ids.add(track.track_id)
|
||||||
@@ -64,10 +82,9 @@ class CountingZone:
|
|||||||
if counting_paused:
|
if counting_paused:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if track.track_id not in inside_ids or track.track_id in self.counted_ids:
|
if track.track_id in self.counted_ids:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
should_validate = False
|
|
||||||
if self.validate_while_inside:
|
if self.validate_while_inside:
|
||||||
should_validate = self._meets_validation_thresholds(track)
|
should_validate = self._meets_validation_thresholds(track)
|
||||||
else:
|
else:
|
||||||
@@ -102,7 +119,8 @@ class CountingZone:
|
|||||||
self.inside_box_count = len(inside_ids)
|
self.inside_box_count = len(inside_ids)
|
||||||
self.current_inside_ids = inside_ids
|
self.current_inside_ids = inside_ids
|
||||||
self.prev_inside_ids = inside_ids
|
self.prev_inside_ids = inside_ids
|
||||||
self._purge_stale(frame_index, active_ids)
|
if frame_index % 30 == 0:
|
||||||
|
self._purge_stale(frame_index, active_ids)
|
||||||
return events
|
return events
|
||||||
|
|
||||||
def trail_for(self, track_id: int) -> list[tuple[int, int]]:
|
def trail_for(self, track_id: int) -> list[tuple[int, int]]:
|
||||||
|
|||||||
@@ -90,11 +90,14 @@ class PipelineArtifacts:
|
|||||||
total_source_frames: int | None
|
total_source_frames: int | None
|
||||||
owns_tracker: bool
|
owns_tracker: bool
|
||||||
detection_zone_rect: tuple[int, int, int, int] | None = None
|
detection_zone_rect: tuple[int, int, int, int] | None = None
|
||||||
|
run_date: str = ""
|
||||||
|
|
||||||
|
|
||||||
def build_pipeline(
|
def build_pipeline(
|
||||||
config: CameraConfig,
|
config: CameraConfig,
|
||||||
tracker: DetectionTracker | None = None,
|
tracker: DetectionTracker | None = None,
|
||||||
|
*,
|
||||||
|
run_date: str = "",
|
||||||
) -> PipelineArtifacts:
|
) -> PipelineArtifacts:
|
||||||
capture = open_capture(config.source)
|
capture = open_capture(config.source)
|
||||||
owns_tracker = tracker is None
|
owns_tracker = tracker is None
|
||||||
@@ -157,6 +160,7 @@ def build_pipeline(
|
|||||||
total_source_frames=total_source_frames,
|
total_source_frames=total_source_frames,
|
||||||
owns_tracker=owns_tracker,
|
owns_tracker=owns_tracker,
|
||||||
detection_zone_rect=detection_zone_rect,
|
detection_zone_rect=detection_zone_rect,
|
||||||
|
run_date=run_date,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -165,12 +169,13 @@ def run_pipeline(
|
|||||||
tracker: DetectionTracker | None = None,
|
tracker: DetectionTracker | None = None,
|
||||||
*,
|
*,
|
||||||
show_progress: bool = False,
|
show_progress: bool = False,
|
||||||
|
run_date: str = "",
|
||||||
) -> PipelineResult:
|
) -> PipelineResult:
|
||||||
if tracker is not None:
|
if tracker is not None:
|
||||||
tracker.config = config
|
tracker.config = config
|
||||||
tracker.reset_tracking()
|
tracker.reset_tracking()
|
||||||
|
|
||||||
artifacts = build_pipeline(config, tracker=tracker)
|
artifacts = build_pipeline(config, tracker=tracker, run_date=run_date)
|
||||||
inference_stride = max(1, config.performance.inference_stride)
|
inference_stride = max(1, config.performance.inference_stride)
|
||||||
print(
|
print(
|
||||||
f"[perf] inference_stride={inference_stride} "
|
f"[perf] inference_stride={inference_stride} "
|
||||||
@@ -252,7 +257,7 @@ def run_pipeline(
|
|||||||
last_annotated = annotated
|
last_annotated = annotated
|
||||||
|
|
||||||
if config.stream.enabled and frame_index % max(1, config.stream.interval_frames) == 0:
|
if config.stream.enabled and frame_index % max(1, config.stream.interval_frames) == 0:
|
||||||
_write_stream_frame(config.stream.shm_dir, config.camera_id, annotated, result)
|
_write_stream_frame(config.stream.shm_dir, config.camera_id, annotated, result, run_date=artifacts.run_date)
|
||||||
|
|
||||||
if verbose:
|
if verbose:
|
||||||
t_write = time.monotonic()
|
t_write = time.monotonic()
|
||||||
@@ -366,7 +371,7 @@ def _consume_result(
|
|||||||
_emit_periodic_feedback(config, artifacts, annotated, result, progress)
|
_emit_periodic_feedback(config, artifacts, annotated, result, progress)
|
||||||
|
|
||||||
|
|
||||||
def _write_stream_frame(shm_dir: str, camera_id: str, frame: np.ndarray, result: FrameResult) -> None:
|
def _write_stream_frame(shm_dir: str, camera_id: str, frame: np.ndarray, result: FrameResult, *, run_date: str = "") -> None:
|
||||||
cam_dir = Path(shm_dir) / f"chicken_counter_{camera_id}"
|
cam_dir = Path(shm_dir) / f"chicken_counter_{camera_id}"
|
||||||
cam_dir.mkdir(parents=True, exist_ok=True)
|
cam_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
@@ -383,6 +388,7 @@ def _write_stream_frame(shm_dir: str, camera_id: str, frame: np.ndarray, result:
|
|||||||
"backward_active": result.motion_state.backward_active,
|
"backward_active": result.motion_state.backward_active,
|
||||||
"smoothed_speed": round(result.motion_state.smoothed_speed, 1),
|
"smoothed_speed": round(result.motion_state.smoothed_speed, 1),
|
||||||
"count_events": len(result.count_events),
|
"count_events": len(result.count_events),
|
||||||
|
"run_date": run_date,
|
||||||
}
|
}
|
||||||
stats_path = cam_dir / "stats.json"
|
stats_path = cam_dir / "stats.json"
|
||||||
stats_tmp = cam_dir / ".stats_tmp.json"
|
stats_tmp = cam_dir / ".stats_tmp.json"
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ class DetectionTracker:
|
|||||||
self.config = config
|
self.config = config
|
||||||
model_path = Path(config.detection.model_path)
|
model_path = Path(config.detection.model_path)
|
||||||
self.model_kind = model_path.suffix.lower().lstrip(".") or "unknown"
|
self.model_kind = model_path.suffix.lower().lstrip(".") or "unknown"
|
||||||
self.model = YOLO(config.detection.model_path)
|
self.model = YOLO(config.detection.model_path, task="detect")
|
||||||
self.tracker_config_path = str(Path(config.tracker.tracker_config_path))
|
self.tracker_config_path = str(Path(config.tracker.tracker_config_path))
|
||||||
self.verbose = config.performance.verbose
|
self.verbose = config.performance.verbose
|
||||||
self._infer_count = 0
|
self._infer_count = 0
|
||||||
|
|||||||
@@ -0,0 +1,131 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Store batch run results into a SQLite database.
|
||||||
|
|
||||||
|
Reads the aggregate JSON report written by the batch runner and inserts
|
||||||
|
all camera-level + summary data. Safe to run multiple times — uses
|
||||||
|
(date, location, camera_id) as the unique key, so re-runs update
|
||||||
|
existing rows instead of duplicating.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python3 store_results.py /path/to/output/counts_2026-06-10.json --location kandang-atas
|
||||||
|
python3 store_results.py /path/to/output/counts_2026-06-10.json --db /var/lib/chickens.db
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import sqlite3
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
CREATE_TABLE = """
|
||||||
|
CREATE TABLE IF NOT EXISTS batch_runs (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
date TEXT NOT NULL,
|
||||||
|
location TEXT NOT NULL,
|
||||||
|
camera_id TEXT NOT NULL,
|
||||||
|
total_entered INTEGER NOT NULL DEFAULT 0,
|
||||||
|
frames_processed INTEGER NOT NULL DEFAULT 0,
|
||||||
|
elapsed_seconds REAL NOT NULL DEFAULT 0.0,
|
||||||
|
stopped_reason TEXT NOT NULL DEFAULT '',
|
||||||
|
source_video TEXT NOT NULL DEFAULT '',
|
||||||
|
generated_at TEXT NOT NULL DEFAULT '',
|
||||||
|
UNIQUE(date, location, camera_id)
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
|
||||||
|
INSERT_SQL = """
|
||||||
|
INSERT INTO batch_runs
|
||||||
|
(date, location, camera_id, total_entered, frames_processed,
|
||||||
|
elapsed_seconds, stopped_reason, source_video, generated_at)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
ON CONFLICT(date, location, camera_id) DO UPDATE SET
|
||||||
|
total_entered = excluded.total_entered,
|
||||||
|
frames_processed = excluded.frames_processed,
|
||||||
|
elapsed_seconds = excluded.elapsed_seconds,
|
||||||
|
stopped_reason = excluded.stopped_reason,
|
||||||
|
source_video = excluded.source_video,
|
||||||
|
generated_at = excluded.generated_at
|
||||||
|
"""
|
||||||
|
|
||||||
|
SUMMARY_QUERY = """
|
||||||
|
SELECT
|
||||||
|
date,
|
||||||
|
location,
|
||||||
|
COUNT(*) AS camera_count,
|
||||||
|
SUM(total_entered) AS total_chickens,
|
||||||
|
SUM(elapsed_seconds) AS total_seconds,
|
||||||
|
ROUND(SUM(elapsed_seconds) / 60.0, 1) AS total_minutes
|
||||||
|
FROM batch_runs
|
||||||
|
WHERE date = ? AND location = ?
|
||||||
|
GROUP BY date, location
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def store_report(report_path: str, location: str, db_path: str) -> None:
|
||||||
|
with open(report_path) as f:
|
||||||
|
report = json.load(f)
|
||||||
|
|
||||||
|
date = report["date"]
|
||||||
|
cameras = report["cameras"]
|
||||||
|
generated_at = report.get("generated_at", "")
|
||||||
|
|
||||||
|
Path(db_path).parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
conn = sqlite3.connect(db_path)
|
||||||
|
conn.execute("PRAGMA journal_mode=WAL")
|
||||||
|
conn.execute(CREATE_TABLE)
|
||||||
|
|
||||||
|
rows = 0
|
||||||
|
for camera_id, entry in cameras.items():
|
||||||
|
if entry.get("skipped"):
|
||||||
|
continue
|
||||||
|
conn.execute(INSERT_SQL, (
|
||||||
|
date, location, camera_id,
|
||||||
|
entry.get("total_entered", 0),
|
||||||
|
entry.get("frames_processed", 0),
|
||||||
|
entry.get("elapsed_seconds", 0),
|
||||||
|
entry.get("stopped_reason", ""),
|
||||||
|
entry.get("source_video", ""),
|
||||||
|
generated_at,
|
||||||
|
))
|
||||||
|
rows += 1
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
# print summary
|
||||||
|
row = conn.execute(SUMMARY_QUERY, (date, location)).fetchone()
|
||||||
|
if row:
|
||||||
|
print(f"\n[db] {row[0]} | {row[1]} | {row[2]} cameras | "
|
||||||
|
f"{row[3]} chickens | {row[4]:.0f}s ({row[5]} min)")
|
||||||
|
|
||||||
|
# also print per-camera breakdown
|
||||||
|
cur = conn.execute(
|
||||||
|
"SELECT camera_id, total_entered, elapsed_seconds "
|
||||||
|
"FROM batch_runs WHERE date=? AND location=? ORDER BY camera_id",
|
||||||
|
(date, location))
|
||||||
|
for cam_id, count, secs in cur:
|
||||||
|
print(f" {cam_id}: {count} chickens, {secs:.0f}s")
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
print(f"\n[db] wrote {rows} rows to {db_path}")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Store batch run results into SQLite")
|
||||||
|
parser.add_argument("report", help="Path to counts_YYYY-MM-DD.json")
|
||||||
|
parser.add_argument("--location", required=True, help="Location name (e.g. kandang-atas)")
|
||||||
|
parser.add_argument("--db", default="chicken_counts.db", help="SQLite database path")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if not Path(args.report).exists():
|
||||||
|
print(f"error: report not found: {args.report}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
store_report(args.report, args.location, args.db)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,213 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<title>Chicken Counter — Live</title>
|
||||||
|
<style>
|
||||||
|
*{margin:0;padding:0;box-sizing:border-box}
|
||||||
|
body{font-family:system-ui,monospace;background:#0f0f14;color:#e0e0e0;overflow:hidden;height:100vh;display:flex;flex-direction:column}
|
||||||
|
#top-bar{display:flex;justify-content:space-between;align-items:center;padding:8px 16px;background:#16161e;font-size:12px;flex-shrink:0}
|
||||||
|
#top-bar h1{font-size:16px;color:#80dc5a}
|
||||||
|
#top-bar .dot{display:inline-block;width:6px;height:6px;border-radius:50%;margin:0 6px}
|
||||||
|
#top-bar .dot.online{background:#80dc5a;box-shadow:0 0 4px #80dc5a}
|
||||||
|
#top-bar .dot.offline{background:#555}
|
||||||
|
#main{flex:1;display:flex}
|
||||||
|
#frame-area{flex:1;background:#000;display:flex;align-items:center;justify-content:center;position:relative}
|
||||||
|
#frame-area img{max-width:100%;max-height:100%;object-fit:contain}
|
||||||
|
#frame-area .overlay{position:absolute;top:12px;left:12px;pointer-events:none}
|
||||||
|
#frame-area .overlay .tag{display:inline-block;padding:4px 12px;border-radius:6px;font-size:12px;font-weight:700;margin-bottom:4px}
|
||||||
|
#frame-area .overlay .tag.live{background:#1a3a2a;color:#80dc5a}
|
||||||
|
#frame-area .overlay .tag.off{background:#333;color:#888}
|
||||||
|
#frame-area .overlay .count{font-size:36px;font-weight:900;color:#fff;text-shadow:0 0 16px rgba(0,0,0,.8)}
|
||||||
|
#sidebar{width:280px;background:#12121a;padding:16px;overflow-y:auto;flex-shrink:0;display:flex;flex-direction:column;gap:12px}
|
||||||
|
#sidebar .stat{padding:12px;background:#16161e;border-radius:8px}
|
||||||
|
#sidebar .stat label{display:block;font-size:9px;color:#555;text-transform:uppercase;letter-spacing:1px;margin-bottom:2px}
|
||||||
|
#sidebar .stat .val{font-size:22px;font-weight:700}
|
||||||
|
#sidebar .stat .val.good{color:#80dc5a}
|
||||||
|
#sidebar .stat .val.warn{color:#ff9f43}
|
||||||
|
#sidebar .cam-list{}
|
||||||
|
#sidebar .cam-list .cam-row{display:flex;justify-content:space-between;align-items:center;padding:6px 10px;margin:2px 0;border-radius:6px;font-size:12px;cursor:pointer;transition:background .15s}
|
||||||
|
#sidebar .cam-list .cam-row:hover{background:#1a1a24}
|
||||||
|
#sidebar .cam-list .cam-row.act{background:#1a3a2a;color:#80dc5a;font-weight:700}
|
||||||
|
#sidebar .cam-list .cam-row .cam-total{font-size:11px;color:#666}
|
||||||
|
#sidebar .cam-list .cam-row.act .cam-total{color:#5a9a4a}
|
||||||
|
#db-panel{padding:12px;background:#16161e;border-radius:8px;font-size:11px;max-height:200px;overflow-y:auto}
|
||||||
|
#db-panel h3{font-size:10px;color:#555;text-transform:uppercase;letter-spacing:1px;margin-bottom:6px}
|
||||||
|
#db-panel .db-row{display:flex;justify-content:space-between;padding:2px 0;color:#888}
|
||||||
|
#db-panel .db-row .db-total{color:#aaa;font-weight:600}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="top-bar">
|
||||||
|
<h1>🐔 Chicken Counter</h1>
|
||||||
|
<span style="color:#888"><span id="stat-date">{{ date }}</span> | <span id="clock"></span></span>
|
||||||
|
</div>
|
||||||
|
<div id="main">
|
||||||
|
<div id="frame-area">
|
||||||
|
<canvas id="frame-canvas" style="display:none"></canvas>
|
||||||
|
<img id="frame-img" src="" alt="live stream" style="display:none">
|
||||||
|
<div id="waiting-msg" style="color:#555;font-size:16px;text-align:center">waiting for pipeline...</div>
|
||||||
|
<div class="overlay" id="frame-overlay" style="display:none">
|
||||||
|
<div class="tag live" id="cam-tag"></div>
|
||||||
|
<div class="count" id="cam-count"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="sidebar">
|
||||||
|
<div class="stat"><label>Total Entered</label><div class="val good" id="s-total">--</div></div>
|
||||||
|
<div class="stat"><label>Inside Box</label><div class="val" id="s-inside">--</div></div>
|
||||||
|
<div class="stat"><label>Tracks</label><div class="val" id="s-tracks">--</div></div>
|
||||||
|
<div class="stat"><label>Frame</label><div class="val" id="s-frame">--</div></div>
|
||||||
|
<div class="stat"><label>Motion</label><div class="val" id="s-speed">--</div></div>
|
||||||
|
<div class="cam-list" id="cam-list"></div>
|
||||||
|
<div class="db-panel" id="db-panel">
|
||||||
|
<h3>📊 History</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
var POLL_MS = {{ poll_ms }};
|
||||||
|
var SHM = "{{ shm_dir }}";
|
||||||
|
var DB_PATH = "{{ db_path }}";
|
||||||
|
var RUN_DATE = "{{ date }}";
|
||||||
|
var cameras = [], activeCam = null, lastUpdate = 0;
|
||||||
|
var perCam = {}, lastFramePerCam = {};
|
||||||
|
|
||||||
|
function boot() {
|
||||||
|
loadCameras();
|
||||||
|
setInterval(loadCameras, 3000);
|
||||||
|
setInterval(poll, POLL_MS);
|
||||||
|
setInterval(updateClock, 1000);
|
||||||
|
loadHistory();
|
||||||
|
setInterval(loadHistory, 60000);
|
||||||
|
updateClock();
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadCameras() {
|
||||||
|
fetch("/api/cameras").then(r => r.json()).then(data => {
|
||||||
|
cameras = data.cameras || [];
|
||||||
|
renderCamList();
|
||||||
|
if (cameras.length && !activeCam) selectCam(cameras[0]);
|
||||||
|
detectActive();
|
||||||
|
if (!cameras.length) showWaiting("no cameras detected");
|
||||||
|
});
|
||||||
|
updateSidebar();
|
||||||
|
}
|
||||||
|
|
||||||
|
function detectActive() {
|
||||||
|
var pending = cameras.length;
|
||||||
|
cameras.forEach(function(cam) {
|
||||||
|
fetch("/shm/" + cam + "/stats.json?t=" + Date.now()).then(r => r.ok ? r.json() : null).then(s => {
|
||||||
|
pending--;
|
||||||
|
if (!s) return;
|
||||||
|
var prev = lastFramePerCam[cam] || 0;
|
||||||
|
perCam[cam] = { total: s.total_entered_count, inside: s.inside_box_count, frame: s.frame_index, tracks: s.track_count, speed: s.smoothed_speed };
|
||||||
|
if (s.frame_index > prev) {
|
||||||
|
lastFramePerCam[cam] = s.frame_index;
|
||||||
|
if (activeCam !== cam) selectCam(cam);
|
||||||
|
}
|
||||||
|
}).finally(function() { if (pending === 0) renderCamList(); });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectCam(id) {
|
||||||
|
activeCam = id;
|
||||||
|
document.getElementById("waiting-msg").style.display = "none";
|
||||||
|
document.getElementById("frame-overlay").style.display = "block";
|
||||||
|
document.getElementById("frame-img").style.display = "block";
|
||||||
|
document.getElementById("frame-img").src = "/stream/" + id;
|
||||||
|
renderCamList();
|
||||||
|
refreshNow();
|
||||||
|
}
|
||||||
|
|
||||||
|
function poll() {
|
||||||
|
if (!activeCam) return;
|
||||||
|
var t = Date.now();
|
||||||
|
fetch("/shm/" + activeCam + "/stats.json?t=" + t).then(r => r.ok ? r.json() : null).then(s => {
|
||||||
|
if (!s) { setOffline(); return; }
|
||||||
|
lastUpdate = Date.now();
|
||||||
|
lastFramePerCam[activeCam] = s.frame_index;
|
||||||
|
perCam[activeCam] = { total: s.total_entered_count, inside: s.inside_box_count, frame: s.frame_index, tracks: s.track_count, speed: s.smoothed_speed };
|
||||||
|
document.getElementById("s-total").textContent = s.total_entered_count;
|
||||||
|
document.getElementById("s-inside").textContent = s.inside_box_count;
|
||||||
|
document.getElementById("s-tracks").textContent = s.track_count;
|
||||||
|
document.getElementById("s-frame").textContent = s.frame_index;
|
||||||
|
document.getElementById("s-speed").textContent = s.smoothed_speed.toFixed(1);
|
||||||
|
var tag = document.getElementById("cam-tag");
|
||||||
|
var status = s.backward_active ? "BACKWARD STOP" : "RUNNING";
|
||||||
|
tag.textContent = activeCam + " \u2022 " + status;
|
||||||
|
tag.className = "tag " + (s.backward_active ? "off" : "live");
|
||||||
|
document.getElementById("cam-count").textContent = s.total_entered_count;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshNow() {
|
||||||
|
poll();
|
||||||
|
}
|
||||||
|
|
||||||
|
function showWaiting(msg) {
|
||||||
|
document.getElementById("waiting-msg").textContent = msg;
|
||||||
|
document.getElementById("waiting-msg").style.display = "block";
|
||||||
|
document.getElementById("frame-overlay").style.display = "none";
|
||||||
|
document.getElementById("frame-img").style.display = "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
function setOffline() {
|
||||||
|
document.getElementById("cam-tag").textContent = activeCam + " \u2022 OFFLINE";
|
||||||
|
document.getElementById("cam-tag").className = "tag off";
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSidebar() {}
|
||||||
|
|
||||||
|
function renderCamList() {
|
||||||
|
var ul = document.getElementById("cam-list");
|
||||||
|
ul.innerHTML = cameras.map(function(c) {
|
||||||
|
var info = perCam[c] || {};
|
||||||
|
var cls = c === activeCam ? " act" : "";
|
||||||
|
return '<div class="cam-row' + cls + '" onclick="selectCam(\'' + c + '\')">' +
|
||||||
|
'<span>' + c + '</span>' +
|
||||||
|
'<span class="cam-total">' + (info.total || 0) + '</span></div>';
|
||||||
|
}).join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateClock() {
|
||||||
|
document.getElementById("clock").textContent = new Date().toLocaleTimeString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadHistory() {
|
||||||
|
if (!DB_PATH) return;
|
||||||
|
var panel = document.getElementById("db-panel");
|
||||||
|
panel.style.display = "block";
|
||||||
|
|
||||||
|
// per-day totals
|
||||||
|
fetch("/api/db/history").then(r => r.json()).then(rows => {
|
||||||
|
var html = "<h3>📊 History</h3>";
|
||||||
|
if (rows.length) {
|
||||||
|
html += rows.map(function(r) {
|
||||||
|
return '<div class="db-row"><span>' + r.date + ' ' + r.location + '</span>' +
|
||||||
|
'<span class="db-total">' + r.total.toLocaleString() + '</span></div>';
|
||||||
|
}).join("");
|
||||||
|
}
|
||||||
|
panel.innerHTML = html;
|
||||||
|
});
|
||||||
|
|
||||||
|
// per-camera breakdown for current date
|
||||||
|
if (RUN_DATE && RUN_DATE !== "today") {
|
||||||
|
fetch("/api/db/date/" + RUN_DATE).then(r => r.json()).then(data => {
|
||||||
|
if (!data.cameras || !data.cameras.length) return;
|
||||||
|
var html = panel.innerHTML;
|
||||||
|
html += '<h3 style="margin-top:12px">📹 ' + RUN_DATE + ' (' + (data.total.total || 0).toLocaleString() + ')</h3>';
|
||||||
|
data.cameras.forEach(function(c) {
|
||||||
|
html += '<div class="db-row"><span>' + c.camera_id + '</span>' +
|
||||||
|
'<span class="db-total">' + c.total_entered.toLocaleString() + '</span></div>';
|
||||||
|
});
|
||||||
|
panel.innerHTML = html;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(function() { if (activeCam) refreshNow(); }, 500);
|
||||||
|
boot();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Executable
+11
@@ -0,0 +1,11 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
alias chicken-counter='PYTHONPATH=/media/jetson/DATA/.Codes/chicken-counting-sukawarna-det/src /media/jetson/DATA/karung-sukawarna/venv/bin/python -m chicken_counter.cli'
|
||||||
|
|
||||||
|
# Declare the array
|
||||||
|
my_array=("2026-06-10" "2026-06-11" "2026-06-12" "2026-06-13" "2026-06-14" "2026-06-15" "2026-06-16" "2026-06-17" "2026-06-18" "2026-06-19")
|
||||||
|
|
||||||
|
# Loop through each item
|
||||||
|
for item in "${my_array[@]}"; do
|
||||||
|
chicken-counter batch --config configs/cycle7_batch.yaml --date ${item} --no-video
|
||||||
|
done
|
||||||
Executable
+15
@@ -0,0 +1,15 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
export PYTHONPATH=/media/jetson/DATA/.Codes/chicken-counting-sukawarna-det/src
|
||||||
|
PYTHON=/media/jetson/DATA/karung-sukawarna/venv/bin/python
|
||||||
|
|
||||||
|
dates=(
|
||||||
|
"2026-06-10" "2026-06-11" "2026-06-12" "2026-06-13" "2026-06-14"
|
||||||
|
"2026-06-15" "2026-06-16" "2026-06-17" "2026-06-18" "2026-06-19"
|
||||||
|
)
|
||||||
|
|
||||||
|
for date in "${dates[@]}"; do
|
||||||
|
echo "=== Processing $date ==="
|
||||||
|
$PYTHON -m chicken_counter.cli batch --config configs/cycle7_batch.yaml --date "$date" --no-video
|
||||||
|
done
|
||||||
Reference in new issue
Block a user