10 Commits
Author SHA1 Message Date
proitlab 3b524d4a65 Today Commit 2026-07-23 20:42:14 +07:00
proitlab 6cddf2c9d3 Fix dashboard freeze: waitress, reduce frame refresh, atomic reads
- Switch Flask dev server -> waitress (8 threads, production WSGI)
- Only refresh frame.jpg when frame_index changes, not every second
- Safety try/except on send_file for edge-case file conflicts
- Fix test_run.sh: export PYTHONPATH, use venv python
- Update service file to use venv python
2026-07-22 16:14:03 +07:00
proitlab b9f4ba2ef8 Database 2026-07-22 15:59:35 +07:00
proitlab 94c4dd81a7 consolidated models and db 2026-07-22 15:59:06 +07:00
proitlab 8cb5b01c00 Fix test_run.sh: alias->CMD (aliases don't work in scripts) 2026-07-22 15:51:35 +07:00
proitlab 3eeb20ca56 Create test run 2026-07-22 15:46:46 +07:00
proitlab f9fc6c0571 Add optimized batch config, remove large model files from tracking
- Add configs/cycle7_batch_optimized.yaml with tuned thresholds
- Update BatchConfig: add location and db_path fields
- Remove .pt/.onnx/.engine files from tracking (too large for git)
- Clean untracked test artifacts
2026-07-22 15:26:33 +07:00
proitlab e7c26d3f86 Auto-init DB on dashboard start if file missing
_init_db() creates the schema at startup so /api/db/*
endpoints work immediately without needing a batch run first.
2026-07-22 15:03:21 +07:00
proitlab dc447264b7 Reduce dashboard CPU: cache DB, suppress logs, add systemd service
- Suppress Flask request logs (werkzeug ERROR only)
- Cache SQLite connection with WAL + 8MB cache
- Increase poll_ms 500->1000, history refresh 30s->60s
- Show 'waiting for pipeline...' when /dev/shm empty
- Add chicken-dashboard.service for systemd auto-start
2026-07-22 14:56:10 +07:00
proitlab 9512fc7f4d Clean all /dev/shm counters on batch start
Wipe chicken_counter_* directories on batch start so dashboard
starts clean and populates as each camera runs.
2026-07-22 14:42:05 +07:00
16 changed files with 435 additions and 143 deletions

No files matched your search

+15
View File
@@ -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
+3 -1
View File
@@ -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
+126
View File
@@ -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
View File
@@ -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.
View File
Whitespace-only changes.
+8
View File
@@ -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")
+2
View File
@@ -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
View File
@@ -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 &#x2022; 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>&#x1f4ca; History</h3>" + rows.map(function(r) {
// per-day totals
fetch("/api/db/history").then(r => r.json()).then(rows => {
var html = "<h3>&#x1f4ca; 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">&#x1f4f9; ' + 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
View File
@@ -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
View File
@@ -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