forked from zakaria/chicken-counting-sukawarna-det
Compare commits
10
Commits
ae86821165
...
3b524d4a65
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3b524d4a65 | ||
|
|
6cddf2c9d3 | ||
|
|
b9f4ba2ef8 | ||
|
|
94c4dd81a7 | ||
|
|
8cb5b01c00 | ||
|
|
3eeb20ca56 | ||
|
|
f9fc6c0571 | ||
|
|
e7c26d3f86 | ||
|
|
dc447264b7 | ||
|
|
9512fc7f4d |
No files matched your search
@@ -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
|
||||
delete_intermediate: false
|
||||
checkpoint_every_n_frames: 3000
|
||||
location: kandang-atas
|
||||
db_path: /media/jetson/DATA/.Codes/chicken-counting-sukawarna-det/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
|
||||
model_path: /media/jetson/DATA/.Codes/chicken-counting-sukawarna-det/models/chicken-detection-model-v26n-300e-best-2026-05-02-NEW.engine
|
||||
classes: [0]
|
||||
ignored_classes: [1, 2]
|
||||
conf: 0.35
|
||||
|
||||
@@ -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]
|
||||
+210
-129
@@ -1,31 +1,64 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Live dashboard for chicken-counter pipeline using Flask.
|
||||
|
||||
Serve from project root:
|
||||
python3 dashboard.py [--port 8080] [--date 2026-06-10] [--db chicken_counts.db]
|
||||
"""
|
||||
"""Live dashboard for chicken-counter pipeline."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sqlite3
|
||||
import threading
|
||||
import time
|
||||
from http.server import HTTPServer, SimpleHTTPRequestHandler
|
||||
from pathlib import Path
|
||||
from socketserver import ThreadingMixIn
|
||||
from urllib.parse import unquote, urlparse
|
||||
|
||||
|
||||
class ThreadingHTTPServer(ThreadingMixIn, HTTPServer):
|
||||
daemon_threads = True
|
||||
|
||||
from flask import Flask, Response, jsonify, render_template, send_file
|
||||
|
||||
DEFAULT_SHM_DIR = "/dev/shm"
|
||||
DEFAULT_PORT = 8080
|
||||
TEMPLATE_DIR = Path(__file__).resolve().parent / "templates"
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config["shm_dir"] = DEFAULT_SHM_DIR
|
||||
app.config["poll_ms"] = 500
|
||||
app.config["run_date"] = ""
|
||||
app.config["db_path"] = ""
|
||||
_db_conn = None
|
||||
_db_lock = threading.Lock()
|
||||
_db_path = ""
|
||||
|
||||
|
||||
def _discover_cameras():
|
||||
shm = Path(app.config["shm_dir"])
|
||||
def _get_db():
|
||||
global _db_conn
|
||||
if not _db_path:
|
||||
return None
|
||||
with _db_lock:
|
||||
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
|
||||
|
||||
|
||||
def _init_db(db_path: str) -> None:
|
||||
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))""")
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
|
||||
def _discover_cameras(shm_dir):
|
||||
shm = Path(shm_dir)
|
||||
cameras = []
|
||||
if shm.is_dir():
|
||||
for entry in sorted(shm.iterdir()):
|
||||
@@ -34,152 +67,200 @@ def _discover_cameras():
|
||||
return cameras
|
||||
|
||||
|
||||
@app.route("/")
|
||||
def index():
|
||||
return render_template(
|
||||
"index.html",
|
||||
poll_ms=app.config["poll_ms"],
|
||||
shm_dir=app.config["shm_dir"],
|
||||
date=app.config["run_date"] or "today",
|
||||
db_path=app.config["db_path"],
|
||||
)
|
||||
class DashboardHandler(SimpleHTTPRequestHandler):
|
||||
shm_dir = DEFAULT_SHM_DIR
|
||||
poll_ms = 1000
|
||||
run_date = ""
|
||||
|
||||
def log_message(self, format, *args):
|
||||
pass
|
||||
|
||||
@app.route("/api/cameras")
|
||||
def api_cameras():
|
||||
return jsonify({"cameras": _discover_cameras()})
|
||||
def do_GET(self):
|
||||
try:
|
||||
self._handle()
|
||||
except (BrokenPipeError, ConnectionResetError):
|
||||
pass
|
||||
|
||||
def _handle(self):
|
||||
parsed = urlparse(self.path)
|
||||
path = unquote(parsed.path)
|
||||
|
||||
@app.route("/shm/<camera_id>/stats.json")
|
||||
def shm_stats(camera_id):
|
||||
stats_path = Path(app.config["shm_dir"]) / f"chicken_counter_{camera_id}" / "stats.json"
|
||||
if not stats_path.exists():
|
||||
return jsonify({"error": "not found"}), 404
|
||||
return jsonify(json.loads(stats_path.read_text()))
|
||||
if path == "/":
|
||||
self._serve_html()
|
||||
return
|
||||
|
||||
if path.startswith("/stream/"):
|
||||
self._handle_stream(path)
|
||||
return
|
||||
|
||||
@app.route("/shm/<camera_id>/frame.jpg")
|
||||
def shm_frame(camera_id):
|
||||
frame_path = Path(app.config["shm_dir"]) / f"chicken_counter_{camera_id}" / "frame.jpg"
|
||||
if path == "/api/cameras":
|
||||
self._send_json({"cameras": _discover_cameras(self.shm_dir)})
|
||||
return
|
||||
|
||||
if path.startswith("/api/db/"):
|
||||
self._handle_db(path)
|
||||
return
|
||||
|
||||
if path.startswith("/shm/"):
|
||||
self._handle_shm(path)
|
||||
return
|
||||
|
||||
self._send_error(404)
|
||||
|
||||
def _handle_stream(self, path):
|
||||
cam_id = path[len("/stream/"):]
|
||||
frame_path = Path(self.shm_dir) / f"chicken_counter_{cam_id}" / "frame.jpg"
|
||||
if not frame_path.exists():
|
||||
return jsonify({"error": "not found"}), 404
|
||||
return send_file(frame_path, mimetype="image/jpeg", max_age=0, download_name=None)
|
||||
self._send_error(404)
|
||||
return
|
||||
|
||||
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()
|
||||
|
||||
def _get_db():
|
||||
db = app.config["db_path"]
|
||||
if not db or not Path(db).exists():
|
||||
return None
|
||||
conn = sqlite3.connect(db)
|
||||
conn.row_factory = sqlite3.Row
|
||||
return conn
|
||||
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
|
||||
|
||||
@app.route("/api/db/summary")
|
||||
def db_summary():
|
||||
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:
|
||||
return jsonify({})
|
||||
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()
|
||||
conn.close()
|
||||
return jsonify(dict(row))
|
||||
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
|
||||
|
||||
@app.route("/api/db/history")
|
||||
def db_history():
|
||||
conn = _get_db()
|
||||
if not conn:
|
||||
return jsonify([])
|
||||
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()
|
||||
conn.close()
|
||||
return jsonify([dict(r) for r in rows])
|
||||
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
|
||||
|
||||
@app.route("/api/db/date/<date>")
|
||||
def db_date(date):
|
||||
conn = _get_db()
|
||||
if not conn:
|
||||
return jsonify({})
|
||||
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()
|
||||
conn.close()
|
||||
return jsonify({"date": date, "total": dict(total), "cameras": [dict(r) for r in cameras]})
|
||||
# /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
|
||||
|
||||
@app.route("/api/db/camera/<camera_id>")
|
||||
def db_camera(camera_id):
|
||||
conn = _get_db()
|
||||
if not conn:
|
||||
return jsonify([])
|
||||
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
|
||||
""", (camera_id,)).fetchall()
|
||||
conn.close()
|
||||
return jsonify([dict(r) for r in rows])
|
||||
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")
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "text/html; charset=utf-8")
|
||||
self.send_header("Content-Length", str(len(data)))
|
||||
self.end_headers()
|
||||
self.wfile.write(data)
|
||||
|
||||
@app.route("/api/db/location/<location>")
|
||||
def db_location(location):
|
||||
conn = _get_db()
|
||||
if not conn:
|
||||
return jsonify({})
|
||||
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
|
||||
""", (location,)).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=?
|
||||
""", (location,)).fetchone()
|
||||
conn.close()
|
||||
return jsonify({"location": location, "summary": dict(summary), "history": [dict(r) for r in history]})
|
||||
def _send_json(self, obj):
|
||||
data = json.dumps(obj).encode("utf-8")
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Content-Length", str(len(data)))
|
||||
self.end_headers()
|
||||
self.wfile.write(data)
|
||||
|
||||
def _send_error(self, code):
|
||||
self.send_response(code)
|
||||
self.send_header("Content-Length", "0")
|
||||
self.end_headers()
|
||||
|
||||
|
||||
def main():
|
||||
global _db_path
|
||||
|
||||
parser = argparse.ArgumentParser(description="Chicken Counter live dashboard")
|
||||
parser.add_argument("--port", type=int, default=DEFAULT_PORT)
|
||||
parser.add_argument("--shm-dir", default=DEFAULT_SHM_DIR)
|
||||
parser.add_argument("--poll-ms", type=int, default=500)
|
||||
parser.add_argument("--date", default="", help="Processing date")
|
||||
parser.add_argument("--db", default="chicken_counts.db", help="SQLite database path")
|
||||
parser.add_argument("--debug", action="store_true")
|
||||
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()
|
||||
|
||||
app.config["shm_dir"] = args.shm_dir
|
||||
app.config["poll_ms"] = args.poll_ms
|
||||
app.config["run_date"] = args.date
|
||||
app.config["db_path"] = str(Path(args.db).resolve()) if args.db else ""
|
||||
DashboardHandler.shm_dir = args.shm_dir
|
||||
DashboardHandler.poll_ms = args.poll_ms
|
||||
DashboardHandler.run_date = args.date
|
||||
_db_path = str(Path(args.db).resolve()) if args.db else ""
|
||||
|
||||
print(f"[dashboard] http://0.0.0.0:{args.port} shm={args.shm_dir} db={args.db}" + (f" date={args.date}" if args.date else ""))
|
||||
app.run(host="0.0.0.0", port=args.port, debug=args.debug, threaded=True)
|
||||
if _db_path:
|
||||
_init_db(_db_path)
|
||||
|
||||
server = ThreadingHTTPServer(("0.0.0.0", args.port), DashboardHandler)
|
||||
date_info = f" date={args.date}" if args.date else ""
|
||||
print(f"[dashboard] http://0.0.0.0:{args.port} shm={args.shm_dir} db={args.db}{date_info}")
|
||||
try:
|
||||
server.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
print("\n[dashboard] stopped")
|
||||
server.server_close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Whitespace-only changes.
BIN
Binary file not shown.
BIN
Binary file not shown.
File renamed without changes.
File renamed without changes.
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import shutil
|
||||
from datetime import date as date_type
|
||||
from pathlib import Path
|
||||
|
||||
@@ -75,6 +76,13 @@ def run_daily_batch(settings: BatchSettings, date: str | None = None, *, verbose
|
||||
print(f"[batch] starting daily run for {run_date}")
|
||||
print(f"[batch] input folder: {day_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:
|
||||
print("[batch] --no-video: skipping video output, overlay, and compression")
|
||||
|
||||
|
||||
@@ -212,6 +212,8 @@ class BatchConfig:
|
||||
compress_max_mb: int = 200
|
||||
delete_intermediate: bool = False
|
||||
checkpoint_every_n_frames: int = 3000
|
||||
location: str = ""
|
||||
db_path: str = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
+45
-13
@@ -45,10 +45,12 @@ body{font-family:system-ui,monospace;background:#0f0f14;color:#e0e0e0;overflow:h
|
||||
</div>
|
||||
<div id="main">
|
||||
<div id="frame-area">
|
||||
<img id="frame-img" src="" alt="live stream">
|
||||
<div class="overlay">
|
||||
<div class="tag live" id="cam-tag">CC1 • RUNNING</div>
|
||||
<div class="count" id="cam-count">--</div>
|
||||
<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">
|
||||
@@ -67,6 +69,7 @@ body{font-family:system-ui,monospace;background:#0f0f14;color:#e0e0e0;overflow:h
|
||||
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 = {};
|
||||
|
||||
@@ -76,7 +79,7 @@ function boot() {
|
||||
setInterval(poll, POLL_MS);
|
||||
setInterval(updateClock, 1000);
|
||||
loadHistory();
|
||||
setInterval(loadHistory, 30000);
|
||||
setInterval(loadHistory, 60000);
|
||||
updateClock();
|
||||
}
|
||||
|
||||
@@ -86,7 +89,9 @@ function loadCameras() {
|
||||
renderCamList();
|
||||
if (cameras.length && !activeCam) selectCam(cameras[0]);
|
||||
detectActive();
|
||||
if (!cameras.length) showWaiting("no cameras detected");
|
||||
});
|
||||
updateSidebar();
|
||||
}
|
||||
|
||||
function detectActive() {
|
||||
@@ -107,6 +112,10 @@ function detectActive() {
|
||||
|
||||
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();
|
||||
}
|
||||
@@ -114,8 +123,6 @@ function selectCam(id) {
|
||||
function poll() {
|
||||
if (!activeCam) return;
|
||||
var t = Date.now();
|
||||
var img = document.getElementById("frame-img");
|
||||
img.src = "/shm/" + activeCam + "/frame.jpg?t=" + t;
|
||||
fetch("/shm/" + activeCam + "/stats.json?t=" + t).then(r => r.ok ? r.json() : null).then(s => {
|
||||
if (!s) { setOffline(); return; }
|
||||
lastUpdate = Date.now();
|
||||
@@ -136,9 +143,13 @@ function poll() {
|
||||
|
||||
function refreshNow() {
|
||||
poll();
|
||||
// also update the frame immediately
|
||||
var t = Date.now();
|
||||
document.getElementById("frame-img").src = "/shm/" + activeCam + "/frame.jpg?t=" + t;
|
||||
}
|
||||
|
||||
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() {
|
||||
@@ -146,6 +157,8 @@ function setOffline() {
|
||||
document.getElementById("cam-tag").className = "tag off";
|
||||
}
|
||||
|
||||
function updateSidebar() {}
|
||||
|
||||
function renderCamList() {
|
||||
var ul = document.getElementById("cam-list");
|
||||
ul.innerHTML = cameras.map(function(c) {
|
||||
@@ -163,15 +176,34 @@ function updateClock() {
|
||||
|
||||
function loadHistory() {
|
||||
if (!DB_PATH) return;
|
||||
fetch("/api/db/history").then(r => r.json()).then(rows => {
|
||||
var panel = document.getElementById("db-panel");
|
||||
if (!rows.length) { panel.style.display = "none"; return; }
|
||||
panel.style.display = "block";
|
||||
panel.innerHTML = "<h3>📊 History</h3>" + rows.map(function(r) {
|
||||
|
||||
// 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);
|
||||
|
||||
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