forked from zakaria/chicken-counting-sukawarna-det
- batch_runner: _store_to_db after each camera (not just at end) - dashboard: Flask app with templates/index.html - Single-camera full-screen live stream with auto-switch - DB history panel in sidebar - store_results.py: standalone script for existing reports
116 lines
3.4 KiB
Python
Executable File
116 lines
3.4 KiB
Python
Executable File
#!/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]
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import sqlite3
|
|
from pathlib import Path
|
|
|
|
from flask import Flask, Response, jsonify, render_template, send_file
|
|
|
|
DEFAULT_SHM_DIR = "/dev/shm"
|
|
DEFAULT_PORT = 8080
|
|
|
|
app = Flask(__name__)
|
|
app.config["shm_dir"] = DEFAULT_SHM_DIR
|
|
app.config["poll_ms"] = 500
|
|
app.config["run_date"] = ""
|
|
app.config["db_path"] = ""
|
|
|
|
|
|
def _discover_cameras():
|
|
shm = Path(app.config["shm_dir"])
|
|
cameras = []
|
|
if shm.is_dir():
|
|
for entry in sorted(shm.iterdir()):
|
|
if entry.is_dir() and entry.name.startswith("chicken_counter_"):
|
|
cameras.append(entry.name[len("chicken_counter_"):])
|
|
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"],
|
|
)
|
|
|
|
|
|
@app.route("/api/cameras")
|
|
def api_cameras():
|
|
return jsonify({"cameras": _discover_cameras()})
|
|
|
|
|
|
@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()))
|
|
|
|
|
|
@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 not frame_path.exists():
|
|
return jsonify({"error": "not found"}), 404
|
|
return send_file(frame_path, mimetype="image/jpeg", max_age=0, download_name=None)
|
|
|
|
|
|
@app.route("/api/db/history")
|
|
def db_history():
|
|
db = app.config["db_path"]
|
|
if not db or not Path(db).exists():
|
|
return jsonify([])
|
|
|
|
conn = sqlite3.connect(db)
|
|
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([
|
|
{"date": r[0], "location": r[1], "cams": r[2],
|
|
"total": r[3], "minutes": r[4]}
|
|
for r in rows
|
|
])
|
|
|
|
|
|
def main():
|
|
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")
|
|
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 ""
|
|
|
|
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 __name__ == "__main__":
|
|
main()
|