forked from zakaria/chicken-counting-sukawarna-det
Dashboard: auto-focus running camera, show date + all-camera totals
- Auto-detect active camera by polling frame_index - Show All Cameras Total accumulation in sidebar - Show per-camera totals in camera list - Add run_date to stats.json and display in dashboard - Pipeline: thread run_date through build/run/batch
This commit is contained in:
1 parent
a54d0e6b2c
commit
6dbaa517dc
3 files changed
+71
-10
No files matched your search
+61
-6
@@ -62,6 +62,8 @@ body{font-family:system-ui,monospace;background:#0f0f14;color:#e0e0e0;overflow:h
|
||||
<div id="app">
|
||||
<div id="sidebar">
|
||||
<h1>🐔 Chicken Counter</h1>
|
||||
<div class="stat"><label>All Cameras Total</label><div class="value good" id="stat-all-total">--</div></div>
|
||||
<div class="stat"><label>Active Camera</label><div class="value" id="stat-cam-name">--</div></div>
|
||||
<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>
|
||||
@@ -73,7 +75,8 @@ body{font-family:system-ui,monospace;background:#0f0f14;color:#e0e0e0;overflow:h
|
||||
<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
|
||||
<span><span id="stat-date">%%DATE%%</span>
|
||||
<span class="refresh" id="refresh-counter"></span> ago
|
||||
<button class="refresh-btn" onclick="load()">↻ Refresh</button></span>
|
||||
</div>
|
||||
<div id="frame-container">
|
||||
@@ -88,6 +91,7 @@ var SHM = "%%SHM_DIR%%";
|
||||
var cameras = [];
|
||||
var activeCam = null;
|
||||
var lastUpdate = 0;
|
||||
var perCam = {{}};
|
||||
var img = document.getElementById("frame-img");
|
||||
var noFrame = document.getElementById("no-frame");
|
||||
|
||||
@@ -95,16 +99,52 @@ 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"; }}
|
||||
if (!cameras.length) {{ noFrame.textContent = "No cameras found in " + SHM; img.style.display = "none"; updateSidebar(); return; }}
|
||||
detectActive();
|
||||
if (!activeCam) selectCam(cameras[0]);
|
||||
}});
|
||||
}}
|
||||
|
||||
function detectActive() {{
|
||||
var pending = cameras.length;
|
||||
cameras.forEach(function(cam) {{
|
||||
fetch("/shm/" + cam + "/stats.json?t=" + Date.now()).then(function(r) {{
|
||||
if (!r.ok) return;
|
||||
return r.json();
|
||||
}}).then(function(s) {{
|
||||
pending--;
|
||||
if (!s) return;
|
||||
var prev = (perCam[cam] && perCam[cam].frame) || 0;
|
||||
perCam[cam] = {{
|
||||
total: s.total_entered_count,
|
||||
inside: s.inside_box_count,
|
||||
frame: s.frame_index,
|
||||
tracks: s.track_count
|
||||
}};
|
||||
if (s.frame_index > prev && activeCam !== cam) selectCam(cam);
|
||||
}}).finally(function() {{
|
||||
if (pending === 0) updateSidebar();
|
||||
}});
|
||||
}});
|
||||
}}
|
||||
|
||||
function updateSidebar() {{
|
||||
var allTotal = 0;
|
||||
cameras.forEach(function(cam) {{
|
||||
if (perCam[cam]) allTotal += perCam[cam].total || 0;
|
||||
}});
|
||||
document.getElementById("stat-all-total").textContent = allTotal || "--";
|
||||
document.getElementById("stat-cam-name").textContent = activeCam || "--";
|
||||
}}
|
||||
|
||||
function renderCamList() {{
|
||||
var ul = document.getElementById("cam-list");
|
||||
ul.innerHTML = cameras.map(function(c) {{
|
||||
var info = perCam[c] || {{}};
|
||||
var total = info.total || 0;
|
||||
return '<li class="' + (c === activeCam ? "active" : "") + '" onclick="selectCam(\'' + c + '\')">' +
|
||||
c + '<span class="cam-badge">▶</span></li>';
|
||||
c + ' <span style="font-size:10px;color:#888">(' + total + ')</span>' +
|
||||
'<span class="cam-badge">▶</span></li>';
|
||||
}}).join("");
|
||||
}}
|
||||
|
||||
@@ -124,6 +164,15 @@ function load() {{
|
||||
}}).then(function(s) {{
|
||||
if (!s) return;
|
||||
lastUpdate = Date.now();
|
||||
perCam[activeCam] = {{
|
||||
total: s.total_entered_count,
|
||||
inside: s.inside_box_count,
|
||||
frame: s.frame_index,
|
||||
tracks: s.track_count
|
||||
}};
|
||||
if (s.run_date) {{
|
||||
document.getElementById("stat-date").textContent = s.run_date;
|
||||
}}
|
||||
document.getElementById("stat-total").textContent = s.total_entered_count;
|
||||
document.getElementById("stat-inside").textContent = s.inside_box_count;
|
||||
document.getElementById("stat-tracks").textContent = s.track_count;
|
||||
@@ -134,6 +183,8 @@ function load() {{
|
||||
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;
|
||||
updateSidebar();
|
||||
renderCamList();
|
||||
}});
|
||||
}}
|
||||
|
||||
@@ -162,6 +213,7 @@ loadCameras();
|
||||
class DashboardHandler(SimpleHTTPRequestHandler):
|
||||
shm_dir = DEFAULT_SHM_DIR
|
||||
poll_ms = 500
|
||||
run_date = ""
|
||||
|
||||
def log_message(self, format, *args):
|
||||
pass
|
||||
@@ -177,7 +229,7 @@ class DashboardHandler(SimpleHTTPRequestHandler):
|
||||
path = unquote(parsed.path)
|
||||
|
||||
if path == "/" or path == "/index.html":
|
||||
html = DASHBOARD_HTML.replace("%%POLL_MS%%", str(self.poll_ms)).replace("%%SHM_DIR%%", self.shm_dir)
|
||||
html = DASHBOARD_HTML.replace("%%POLL_MS%%", str(self.poll_ms)).replace("%%SHM_DIR%%", self.shm_dir).replace("%%DATE%%", self.run_date or "today")
|
||||
self._send_html(html)
|
||||
return
|
||||
|
||||
@@ -242,14 +294,17 @@ def main():
|
||||
parser.add_argument("--port", type=int, default=DEFAULT_PORT, help=f"HTTP port (default: {DEFAULT_PORT})")
|
||||
parser.add_argument("--shm-dir", default=DEFAULT_SHM_DIR, help=f"Shared memory directory (default: {DEFAULT_SHM_DIR})")
|
||||
parser.add_argument("--poll-ms", type=int, default=500, help="Image poll interval in ms (default: 500)")
|
||||
parser.add_argument("--date", default="", help="Processing date to display (e.g. 2026-06-10)")
|
||||
args = parser.parse_args()
|
||||
|
||||
DashboardHandler.shm_dir = args.shm_dir
|
||||
DashboardHandler.poll_ms = args.poll_ms
|
||||
DashboardHandler.run_date = args.date
|
||||
|
||||
server = ThreadingHTTPServer(("0.0.0.0", args.port), DashboardHandler)
|
||||
date_info = f" date={args.date}" if args.date else ""
|
||||
print(f"[dashboard] serving at http://0.0.0.0:{args.port}")
|
||||
print(f"[dashboard] shm_dir={args.shm_dir} poll={args.poll_ms}ms")
|
||||
print(f"[dashboard] shm_dir={args.shm_dir}{date_info} poll={args.poll_ms}ms")
|
||||
try:
|
||||
server.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
|
||||
@@ -58,7 +58,7 @@ def run_daily_batch(settings: BatchSettings, date: str | None = None, *, verbose
|
||||
checkpoint_dir=checkpoint_dir,
|
||||
)
|
||||
camera_config.performance.verbose = verbose
|
||||
pipeline_result = run_pipeline(camera_config, show_progress=show_progress)
|
||||
pipeline_result = run_pipeline(camera_config, show_progress=show_progress, run_date=run_date)
|
||||
camera_results.append(
|
||||
CameraBatchResult(
|
||||
camera_id=camera_id,
|
||||
|
||||
@@ -90,11 +90,14 @@ class PipelineArtifacts:
|
||||
total_source_frames: int | None
|
||||
owns_tracker: bool
|
||||
detection_zone_rect: tuple[int, int, int, int] | None = None
|
||||
run_date: str = ""
|
||||
|
||||
|
||||
def build_pipeline(
|
||||
config: CameraConfig,
|
||||
tracker: DetectionTracker | None = None,
|
||||
*,
|
||||
run_date: str = "",
|
||||
) -> PipelineArtifacts:
|
||||
capture = open_capture(config.source)
|
||||
owns_tracker = tracker is None
|
||||
@@ -157,6 +160,7 @@ def build_pipeline(
|
||||
total_source_frames=total_source_frames,
|
||||
owns_tracker=owns_tracker,
|
||||
detection_zone_rect=detection_zone_rect,
|
||||
run_date=run_date,
|
||||
)
|
||||
|
||||
|
||||
@@ -165,12 +169,13 @@ def run_pipeline(
|
||||
tracker: DetectionTracker | None = None,
|
||||
*,
|
||||
show_progress: bool = False,
|
||||
run_date: str = "",
|
||||
) -> PipelineResult:
|
||||
if tracker is not None:
|
||||
tracker.config = config
|
||||
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)
|
||||
print(
|
||||
f"[perf] inference_stride={inference_stride} "
|
||||
@@ -252,7 +257,7 @@ def run_pipeline(
|
||||
last_annotated = annotated
|
||||
|
||||
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:
|
||||
t_write = time.monotonic()
|
||||
@@ -366,7 +371,7 @@ def _consume_result(
|
||||
_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.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,
|
||||
"smoothed_speed": round(result.motion_state.smoothed_speed, 1),
|
||||
"count_events": len(result.count_events),
|
||||
"run_date": run_date,
|
||||
}
|
||||
stats_path = cam_dir / "stats.json"
|
||||
stats_tmp = cam_dir / ".stats_tmp.json"
|
||||
|
||||
Reference in new issue
Block a user