forked from zakaria/chicken-counting-sukawarna-det
Try to make it work in Jetson Orin Nano
This commit is contained in:
1 parent
a54a070ca9
commit
7aa5d5eb61
53 files changed
+773
-23
No files matched your search
+220
@@ -0,0 +1,220 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[codz]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py.cover
|
||||
*.lcov
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
# Pipfile.lock
|
||||
|
||||
# UV
|
||||
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# uv.lock
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
# poetry.lock
|
||||
# poetry.toml
|
||||
|
||||
# pdm
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
|
||||
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
|
||||
# pdm.lock
|
||||
# pdm.toml
|
||||
.pdm-python
|
||||
.pdm-build/
|
||||
|
||||
# pixi
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
|
||||
# pixi.lock
|
||||
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
|
||||
# in the .venv directory. It is recommended not to include this directory in version control.
|
||||
.pixi/*
|
||||
!.pixi/config.toml
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule*
|
||||
celerybeat.pid
|
||||
|
||||
# Redis
|
||||
*.rdb
|
||||
*.aof
|
||||
*.pid
|
||||
|
||||
# RabbitMQ
|
||||
mnesia/
|
||||
rabbitmq/
|
||||
rabbitmq-data/
|
||||
|
||||
# ActiveMQ
|
||||
activemq-data/
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.envrc
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
# .idea/
|
||||
|
||||
# Abstra
|
||||
# Abstra is an AI-powered process automation framework.
|
||||
# Ignore directories containing user credentials, local state, and settings.
|
||||
# Learn more at https://abstra.io/docs
|
||||
.abstra/
|
||||
|
||||
# Visual Studio Code
|
||||
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
||||
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
||||
# you could uncomment the following to ignore the entire vscode folder
|
||||
# .vscode/
|
||||
# Temporary file for partial code execution
|
||||
tempCodeRunnerFile.py
|
||||
|
||||
# Ruff stuff:
|
||||
.ruff_cache/
|
||||
|
||||
# PyPI configuration file
|
||||
.pypirc
|
||||
|
||||
# Marimo
|
||||
marimo/_static/
|
||||
marimo/_lsp/
|
||||
__marimo__/
|
||||
|
||||
# Streamlit
|
||||
.streamlit/secrets.toml
|
||||
Binary file not shown.
Binary file not shown.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
+7
-3
@@ -1,5 +1,5 @@
|
||||
batch:
|
||||
root_dir: /home/nvidia-admin/VIDEOS/cycle7/kandang-atas
|
||||
root_dir: /media/jetson/DATA/.Codes/VIDEOS/cycle7/kandang-atas
|
||||
camera_glob: "kandang_*_camera_{num}_*.mp4"
|
||||
output_subdir: output
|
||||
compress_max_mb: 200
|
||||
@@ -8,7 +8,7 @@ batch:
|
||||
|
||||
defaults:
|
||||
detection:
|
||||
model_path: /home/nvidia-admin/LABS/try-weight-estimator/try-chicken-sukawarna/chicken-detection-model-v26n-300e-best-2026-05-02-NEW.pt
|
||||
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
|
||||
@@ -23,7 +23,7 @@ defaults:
|
||||
buffer_below_px: 250
|
||||
show_in_overlay: true
|
||||
tracker:
|
||||
tracker_config_path: /home/nvidia-admin/LABS/try-weight-estimator/try-chicken-sukawarna/configs/trackers/botsort_chicken.yaml
|
||||
tracker_config_path: /media/jetson/DATA/.Codes/chicken-counting-sukawarna-det/configs/trackers/botsort_chicken.yaml
|
||||
persist: true
|
||||
track_buffer: 75
|
||||
gate:
|
||||
@@ -66,6 +66,10 @@ defaults:
|
||||
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
|
||||
|
||||
Regular → Executable
File mode changed.
Executable
+261
@@ -0,0 +1,261 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Standalone live dashboard for chicken-counter pipeline.
|
||||
|
||||
Serve from project root:
|
||||
PYTHONPATH=src python3 dashboard.py [--port 8080]
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
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
|
||||
|
||||
DEFAULT_SHM_DIR = "/dev/shm"
|
||||
DEFAULT_PORT = 8080
|
||||
|
||||
DASHBOARD_HTML = r"""<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<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() {{
|
||||
var ul = document.getElementById("cam-list");
|
||||
ul.innerHTML = cameras.map(function(c) {{
|
||||
return '<li class="' + (c === activeCam ? "active" : "") + '" onclick="selectCam(\'' + c + '\')">' +
|
||||
c + '<span class="cam-badge">▶</span></li>';
|
||||
}}).join("");
|
||||
}}
|
||||
|
||||
function selectCam(id) {{
|
||||
activeCam = id;
|
||||
renderCamList();
|
||||
load();
|
||||
}}
|
||||
|
||||
function load() {{
|
||||
if (!activeCam) return;
|
||||
var t = Date.now();
|
||||
img.src = "/shm/" + activeCam + "/frame.jpg?t=" + t;
|
||||
fetch("/shm/" + activeCam + "/stats.json?t=" + t).then(function(r) {{
|
||||
if (!r.ok) {{ setOffline(); return; }}
|
||||
return r.json();
|
||||
}}).then(function(s) {{
|
||||
if (!s) return;
|
||||
lastUpdate = Date.now();
|
||||
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;
|
||||
document.getElementById("stat-frame").textContent = s.frame_index;
|
||||
document.getElementById("stat-speed").textContent = s.smoothed_speed;
|
||||
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() {{
|
||||
var ago = Math.round((Date.now() - lastUpdate) / 1000);
|
||||
document.getElementById("refresh-counter").textContent = ago + "s";
|
||||
}}
|
||||
|
||||
img.onerror = function() {{ img.style.display = "none"; noFrame.style.display = "block"; noFrame.textContent = "Waiting for frame..."; }};
|
||||
img.onload = function() {{ img.style.display = "block"; noFrame.style.display = "none"; }};
|
||||
|
||||
setInterval(function() {{ load(); }}, POLL_MS);
|
||||
setInterval(loadCameras, 3000);
|
||||
setInterval(updateRefresh, 1000);
|
||||
loadCameras();
|
||||
</script>
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
|
||||
class DashboardHandler(SimpleHTTPRequestHandler):
|
||||
shm_dir = DEFAULT_SHM_DIR
|
||||
poll_ms = 500
|
||||
|
||||
def log_message(self, format, *args):
|
||||
pass
|
||||
|
||||
def do_GET(self):
|
||||
try:
|
||||
self._handle_request()
|
||||
except (BrokenPipeError, ConnectionResetError):
|
||||
pass
|
||||
|
||||
def _handle_request(self):
|
||||
parsed = urlparse(self.path)
|
||||
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)
|
||||
self._send_html(html)
|
||||
return
|
||||
|
||||
if path == "/api/cameras":
|
||||
cameras = self._discover_cameras()
|
||||
self._send_json({"cameras": cameras})
|
||||
return
|
||||
|
||||
if path.startswith("/shm/"):
|
||||
rel = path[len("/shm/"):]
|
||||
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
|
||||
|
||||
self.send_error(404)
|
||||
|
||||
def _discover_cameras(self):
|
||||
shm = Path(self.shm_dir)
|
||||
cameras = []
|
||||
if shm.is_dir():
|
||||
for entry in sorted(shm.iterdir()):
|
||||
if entry.is_dir() and entry.name.startswith("chicken_counter_"):
|
||||
cam_id = entry.name[len("chicken_counter_"):]
|
||||
cameras.append(cam_id)
|
||||
return cameras
|
||||
|
||||
def _send_html(self, html: str):
|
||||
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)
|
||||
|
||||
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 main():
|
||||
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("--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)")
|
||||
args = parser.parse_args()
|
||||
|
||||
DashboardHandler.shm_dir = args.shm_dir
|
||||
DashboardHandler.poll_ms = args.poll_ms
|
||||
|
||||
server = ThreadingHTTPServer(("0.0.0.0", args.port), DashboardHandler)
|
||||
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")
|
||||
try:
|
||||
server.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
print("\n[dashboard] stopped")
|
||||
server.server_close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
+16
-4
@@ -14,7 +14,7 @@ from chicken_counter.tracking import DetectionTracker
|
||||
from chicken_counter.types import CameraBatchResult
|
||||
|
||||
|
||||
def run_daily_batch(settings: BatchSettings, date: str | None = None) -> 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()
|
||||
day_dir = Path(settings.batch.root_dir) / run_date
|
||||
output_dir = day_dir / settings.batch.output_subdir
|
||||
@@ -23,6 +23,8 @@ def run_daily_batch(settings: BatchSettings, date: str | None = None) -> Path:
|
||||
print(f"[batch] starting daily run for {run_date}")
|
||||
print(f"[batch] input folder: {day_dir}")
|
||||
print(f"[batch] output folder: {output_dir}")
|
||||
if no_video:
|
||||
print("[batch] --no-video: skipping video output, overlay, and compression")
|
||||
|
||||
discovery = discover_camera_videos(day_dir, settings)
|
||||
camera_order = sorted(settings.cameras.items(), key=lambda item: item[1].camera_num)
|
||||
@@ -31,11 +33,12 @@ def run_daily_batch(settings: BatchSettings, date: str | None = None) -> Path:
|
||||
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=output_dir / f"{first_camera_id}_vis.mp4",
|
||||
output_path=init_output_path,
|
||||
checkpoint_dir=output_dir / "checkpoints" / first_camera_id,
|
||||
)
|
||||
shared_tracker = DetectionTracker(init_config)
|
||||
@@ -58,7 +61,7 @@ def run_daily_batch(settings: BatchSettings, date: str | None = None) -> Path:
|
||||
continue
|
||||
|
||||
source_path = discovery.found[camera_id]
|
||||
vis_path = output_dir / f"{camera_id}_vis.mp4"
|
||||
vis_path = output_dir / f"{camera_id}_vis.mp4" if not no_video else None
|
||||
checkpoint_dir = output_dir / "checkpoints" / camera_id
|
||||
|
||||
print(f"[batch] processing {camera_id} from {source_path.name}")
|
||||
@@ -69,7 +72,8 @@ def run_daily_batch(settings: BatchSettings, date: str | None = None) -> Path:
|
||||
output_path=vis_path,
|
||||
checkpoint_dir=checkpoint_dir,
|
||||
)
|
||||
pipeline_result = run_pipeline(camera_config, tracker=shared_tracker)
|
||||
camera_config.performance.verbose = verbose
|
||||
pipeline_result = run_pipeline(camera_config, tracker=shared_tracker, show_progress=show_progress)
|
||||
camera_results.append(
|
||||
CameraBatchResult(
|
||||
camera_id=camera_id,
|
||||
@@ -82,6 +86,14 @@ def run_daily_batch(settings: BatchSettings, date: str | None = None) -> Path:
|
||||
)
|
||||
persist_batch_reports(run_date, camera_results, output_dir)
|
||||
|
||||
if no_video:
|
||||
report = build_batch_report(run_date, camera_results, output_dir=output_dir)
|
||||
print(
|
||||
f"[batch] complete for {run_date}: total_entered_sum={report.total_entered_sum} "
|
||||
f"report={report_path}"
|
||||
)
|
||||
return report_path
|
||||
|
||||
print("[batch] all cameras complete; starting compression")
|
||||
for item in camera_results:
|
||||
if item.skipped or item.pipeline is None:
|
||||
|
||||
Regular → Executable
File mode changed.
Regular → Executable
+10
-3
@@ -16,6 +16,8 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
run_parser = subparsers.add_parser("run", help="Run a single camera pipeline.")
|
||||
run_parser.add_argument("--config", required=True, help="Path to camera config YAML/JSON.")
|
||||
run_parser.add_argument("--camera-id", help="Camera ID when using a multi-camera config file.")
|
||||
run_parser.add_argument("--verbose", action="store_true", help="Enable debug-level logging.")
|
||||
run_parser.add_argument("--progress-bar", action="store_true", help="Show a terminal progress bar.")
|
||||
|
||||
batch_parser = subparsers.add_parser("batch", help="Run the daily Cycle7 multi-camera batch.")
|
||||
batch_parser.add_argument("--config", required=True, help="Path to batch config YAML/JSON.")
|
||||
@@ -23,6 +25,9 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
"--date",
|
||||
help="Processing date folder in YYYY-MM-DD format. Defaults to today.",
|
||||
)
|
||||
batch_parser.add_argument("--verbose", action="store_true", help="Enable debug-level logging.")
|
||||
batch_parser.add_argument("--no-video", action="store_true", help="Skip video output and compression for speed.")
|
||||
batch_parser.add_argument("--progress-bar", action="store_true", help="Show a terminal progress bar.")
|
||||
|
||||
parser.add_argument("--config", help=argparse.SUPPRESS)
|
||||
parser.add_argument("--camera-id", help=argparse.SUPPRESS)
|
||||
@@ -35,12 +40,13 @@ def main() -> None:
|
||||
|
||||
if args.command == "batch":
|
||||
settings = load_batch_config(args.config)
|
||||
run_daily_batch(settings, date=args.date)
|
||||
run_daily_batch(settings, date=args.date, verbose=args.verbose, no_video=args.no_video, show_progress=args.progress_bar)
|
||||
return
|
||||
|
||||
if args.command == "run":
|
||||
config = load_camera_config(args.config, args.camera_id)
|
||||
result = run_pipeline(config)
|
||||
config.performance.verbose = args.verbose
|
||||
result = run_pipeline(config, show_progress=args.progress_bar)
|
||||
print(
|
||||
f"[done] camera={result.camera_id} total_entered={result.total_entered_count} "
|
||||
f"frames={result.frames_processed} reason={result.stopped_reason}"
|
||||
@@ -49,7 +55,8 @@ def main() -> None:
|
||||
|
||||
if args.config:
|
||||
config = load_camera_config(args.config, args.camera_id)
|
||||
result = run_pipeline(config)
|
||||
config.performance.verbose = args.verbose
|
||||
result = run_pipeline(config, show_progress=args.progress_bar)
|
||||
print(
|
||||
f"[done] camera={result.camera_id} total_entered={result.total_entered_count} "
|
||||
f"frames={result.frames_processed} reason={result.stopped_reason}"
|
||||
|
||||
Regular → Executable
File mode changed.
Regular → Executable
+13
-3
@@ -168,6 +168,14 @@ class PerformanceConfig:
|
||||
half: bool = False
|
||||
overlay_buffer_reuse: bool = True
|
||||
inference_stride: int = 1
|
||||
verbose: bool = False
|
||||
|
||||
|
||||
@dataclass
|
||||
class StreamConfig:
|
||||
enabled: bool = False
|
||||
shm_dir: str = "/dev/shm"
|
||||
interval_frames: int = 5
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -193,6 +201,7 @@ class CameraConfig:
|
||||
performance: PerformanceConfig
|
||||
feedback: FeedbackConfig
|
||||
detection_zone: DetectionZoneConfig = field(default_factory=DetectionZoneConfig)
|
||||
stream: StreamConfig = field(default_factory=StreamConfig)
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -269,6 +278,7 @@ def _build_camera_config(raw: dict[str, Any]) -> CameraConfig:
|
||||
performance=PerformanceConfig(**raw.get("performance", {})),
|
||||
feedback=FeedbackConfig(**raw.get("feedback", {})),
|
||||
detection_zone=DetectionZoneConfig(**raw.get("detection_zone", {})),
|
||||
stream=StreamConfig(**raw.get("stream", {})),
|
||||
)
|
||||
|
||||
|
||||
@@ -338,7 +348,7 @@ def build_camera_config_from_batch(
|
||||
camera_id: str,
|
||||
*,
|
||||
source: str | Path,
|
||||
output_path: str | Path,
|
||||
output_path: str | Path | None,
|
||||
checkpoint_dir: str | Path,
|
||||
) -> CameraConfig:
|
||||
if camera_id not in settings.cameras:
|
||||
@@ -378,13 +388,13 @@ def build_camera_config_from_batch(
|
||||
raw["overlay"]["count_anchor"] = list(preset.count_anchor)
|
||||
|
||||
raw.setdefault("display", {})
|
||||
raw["display"]["output_path"] = str(output_path)
|
||||
raw["display"]["output_path"] = str(output_path) if output_path is not None else None
|
||||
raw["display"]["show_window"] = False
|
||||
|
||||
raw.setdefault("feedback", {})
|
||||
raw["feedback"]["enabled"] = True
|
||||
raw["feedback"]["every_n_frames"] = settings.batch.checkpoint_every_n_frames
|
||||
raw["feedback"]["save_images"] = True
|
||||
raw["feedback"]["save_images"] = output_path is not None
|
||||
raw["feedback"]["image_output_dir"] = str(checkpoint_dir)
|
||||
raw["feedback"]["log_to_terminal"] = True
|
||||
|
||||
|
||||
Regular → Executable
+12
@@ -20,6 +20,8 @@ class CountingZone:
|
||||
track_buffer: int,
|
||||
min_box_area_px: int = 0,
|
||||
validate_while_inside: bool = True,
|
||||
*,
|
||||
verbose: bool = False,
|
||||
) -> None:
|
||||
self.roi = roi
|
||||
self.gate = gate
|
||||
@@ -28,6 +30,7 @@ class CountingZone:
|
||||
self.min_box_area_px = min_box_area_px
|
||||
self.min_overlap_ratio = roi.min_overlap_ratio
|
||||
self.validate_while_inside = validate_while_inside
|
||||
self.verbose = verbose
|
||||
self.inside_box_count = 0
|
||||
self.total_entered_count = 0
|
||||
self.histories: dict[int, deque[tuple[int, int]]] = defaultdict(lambda: deque(maxlen=trail_length))
|
||||
@@ -86,6 +89,15 @@ class CountingZone:
|
||||
sequence_number=self.sequence_numbers_by_track_id[track.track_id],
|
||||
)
|
||||
)
|
||||
if self.verbose:
|
||||
x1, y1, x2, y2 = track.bbox_xyxy
|
||||
bbox_area = max(0, x2 - x1) * max(0, y2 - y1)
|
||||
overlap = self._bbox_overlap_ratio(track)
|
||||
print(
|
||||
f"[count] track={track.track_id} seq=#{self.total_entered_count} "
|
||||
f"frame={frame_index} area={bbox_area} overlap={overlap:.2f} "
|
||||
f"conf={track.confidence:.2f} centroid={track.centroid}"
|
||||
)
|
||||
|
||||
self.inside_box_count = len(inside_ids)
|
||||
self.current_inside_ids = inside_ids
|
||||
|
||||
Regular → Executable
+22
-1
@@ -10,12 +10,14 @@ from chicken_counter.types import MotionState, TrackObservation
|
||||
|
||||
|
||||
class BackwardMotionDetector:
|
||||
def __init__(self, config: MotionConfig, roi: RoiConfig) -> None:
|
||||
def __init__(self, config: MotionConfig, roi: RoiConfig, *, verbose: bool = False) -> None:
|
||||
self.config = config
|
||||
self.roi = roi
|
||||
self.previous_gray: np.ndarray | None = None
|
||||
self.state = MotionState()
|
||||
self._roi_bounds = self._compute_roi_bounds()
|
||||
self.verbose = verbose
|
||||
self._update_count = 0
|
||||
|
||||
def _compute_roi_bounds(self) -> tuple[int, int, int, int]:
|
||||
x_values = [point[0] for point in self.roi.points]
|
||||
@@ -94,6 +96,25 @@ class BackwardMotionDetector:
|
||||
self.state.backward_active = False
|
||||
|
||||
if self.state.consecutive_reverse_frames >= self.config.debounce_frames:
|
||||
was_active = self.state.backward_active
|
||||
self.state.backward_active = True
|
||||
if self.verbose and not was_active:
|
||||
print(
|
||||
f"[motion #{self._update_count}] BACKWARD TRIGGERED! "
|
||||
f"smoothed_speed={self.state.smoothed_speed:.1f} "
|
||||
f"consecutive={self.state.consecutive_reverse_frames}"
|
||||
)
|
||||
|
||||
if self.verbose:
|
||||
self._update_count += 1
|
||||
features_found = len(valid_prev) if points is not None and self.previous_gray is not None else 0
|
||||
print(
|
||||
f"[motion #{self._update_count}] "
|
||||
f"features={features_found} "
|
||||
f"median_speed={median_axis_speed:.1f} "
|
||||
f"smoothed_speed={self.state.smoothed_speed:.1f} "
|
||||
f"consecutive_rev={self.state.consecutive_reverse_frames} "
|
||||
f"backward={self.state.backward_active}"
|
||||
)
|
||||
|
||||
return self.state
|
||||
Regular → Executable
File mode changed.
Regular → Executable
+189
-9
@@ -2,6 +2,9 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import shutil
|
||||
import sys
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
@@ -19,6 +22,62 @@ from chicken_counter.types import FrameResult, PipelineResult, TrackObservation
|
||||
from chicken_counter.video_writer import make_video_writer
|
||||
|
||||
|
||||
class _ProgressBar:
|
||||
def __init__(self, total: int | None, width: int = 30) -> None:
|
||||
self._total = total
|
||||
self._width = width
|
||||
self._last_render = 0.0
|
||||
self._last_line_len = 0
|
||||
self._checkpoint_msg = ""
|
||||
|
||||
def _build_checkpoint_suffix(self) -> str:
|
||||
if not self._checkpoint_msg:
|
||||
return ""
|
||||
msg = self._checkpoint_msg
|
||||
self._checkpoint_msg = ""
|
||||
return f" [{msg}]"
|
||||
|
||||
def render(self, frame_index: int, elapsed: float, fps: float, inside: int, total_entered: int, backward: bool) -> None:
|
||||
now = time.monotonic()
|
||||
if now - self._last_render < 0.2 and frame_index > 1 and not self._checkpoint_msg:
|
||||
return
|
||||
self._last_render = now
|
||||
|
||||
elapsed_str = _format_duration(elapsed)
|
||||
checkpoint_suffix = self._build_checkpoint_suffix()
|
||||
if self._total:
|
||||
pct = min(100, frame_index * 100 // self._total)
|
||||
filled = self._width * pct // 100
|
||||
bar = "[" + "=" * filled + ">" + " " * (self._width - filled) + "]"
|
||||
eta_seconds = (self._total - frame_index) / fps if fps > 0 else 0.0
|
||||
eta_str = _format_duration(eta_seconds)
|
||||
status = "backward" if backward else "running"
|
||||
line = (
|
||||
f"\r{bar} {pct:3d}% {frame_index}/{self._total} "
|
||||
f"{elapsed_str} eta={eta_str} {fps:.1f}fps "
|
||||
f"count={inside}/{total_entered} {status}{checkpoint_suffix}"
|
||||
)
|
||||
else:
|
||||
status = "backward" if backward else "running"
|
||||
line = (
|
||||
f"\rframe={frame_index} {elapsed_str} {fps:.1f}fps "
|
||||
f"count={inside}/{total_entered} {status}{checkpoint_suffix}"
|
||||
)
|
||||
|
||||
pad = max(0, self._last_line_len - len(line))
|
||||
self._last_line_len = len(line)
|
||||
sys.stderr.write(line + " " * pad)
|
||||
sys.stderr.flush()
|
||||
|
||||
def emit(self, message: str) -> None:
|
||||
self._checkpoint_msg = message
|
||||
self._last_render = 0.0
|
||||
|
||||
def finish(self) -> None:
|
||||
sys.stderr.write("\n")
|
||||
sys.stderr.flush()
|
||||
|
||||
|
||||
@dataclass
|
||||
class PipelineArtifacts:
|
||||
capture: cv2.VideoCapture
|
||||
@@ -48,8 +107,9 @@ def build_pipeline(
|
||||
track_buffer=config.tracker.track_buffer,
|
||||
min_box_area_px=config.detection.min_box_area_px,
|
||||
validate_while_inside=config.detection.validate_while_inside,
|
||||
verbose=config.performance.verbose,
|
||||
)
|
||||
motion_detector = BackwardMotionDetector(config.motion, config.roi)
|
||||
motion_detector = BackwardMotionDetector(config.motion, config.roi, verbose=config.performance.verbose)
|
||||
|
||||
width = int(capture.get(cv2.CAP_PROP_FRAME_WIDTH))
|
||||
height = int(capture.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
||||
@@ -80,6 +140,12 @@ def build_pipeline(
|
||||
codec_preference=config.display.codec_preference,
|
||||
)
|
||||
|
||||
if config.stream.enabled:
|
||||
cam_dir = Path(config.stream.shm_dir) / f"chicken_counter_{config.camera_id}"
|
||||
if cam_dir.exists():
|
||||
shutil.rmtree(str(cam_dir))
|
||||
print(f"[stream] cleaned {cam_dir}")
|
||||
|
||||
return PipelineArtifacts(
|
||||
capture=capture,
|
||||
tracker=tracker,
|
||||
@@ -97,6 +163,8 @@ def build_pipeline(
|
||||
def run_pipeline(
|
||||
config: CameraConfig,
|
||||
tracker: DetectionTracker | None = None,
|
||||
*,
|
||||
show_progress: bool = False,
|
||||
) -> PipelineResult:
|
||||
if tracker is not None:
|
||||
tracker.config = config
|
||||
@@ -114,26 +182,51 @@ def run_pipeline(
|
||||
last_tracks: list[TrackObservation] = []
|
||||
stopped_reason = "eof"
|
||||
user_quit = False
|
||||
verbose = config.performance.verbose
|
||||
cumulative_timings: dict[str, float] = {"read": 0.0, "infer": 0.0, "motion": 0.0, "count": 0.0, "overlay": 0.0, "write": 0.0}
|
||||
timed_frames = 0
|
||||
verbose_interval = max(1, inference_stride * 30)
|
||||
progress = _ProgressBar(artifacts.total_source_frames) if show_progress else None
|
||||
|
||||
try:
|
||||
while True:
|
||||
if verbose:
|
||||
t0 = time.monotonic()
|
||||
|
||||
ok, frame = artifacts.capture.read()
|
||||
if not ok:
|
||||
break
|
||||
|
||||
frame_index += 1
|
||||
|
||||
if verbose:
|
||||
t_read = time.monotonic()
|
||||
|
||||
if frame_index % inference_stride == 0 or not last_tracks:
|
||||
last_tracks = artifacts.tracker.infer(
|
||||
frame,
|
||||
crop_rect=artifacts.detection_zone_rect,
|
||||
)
|
||||
|
||||
if verbose and frame_index % inference_stride == 0:
|
||||
t_infer = time.monotonic()
|
||||
|
||||
tracks = last_tracks
|
||||
motion_state = artifacts.motion_detector.update(frame, tracks, frame_index)
|
||||
|
||||
if verbose:
|
||||
t_motion = time.monotonic()
|
||||
|
||||
count_events = artifacts.counting_zone.update(
|
||||
tracks,
|
||||
frame_index,
|
||||
counting_paused=motion_state.backward_active,
|
||||
)
|
||||
|
||||
if verbose:
|
||||
t_count = time.monotonic()
|
||||
|
||||
needs_overlay = config.display.show_window or artifacts.writer is not None or config.stream.enabled
|
||||
annotated = draw_overlay(
|
||||
frame,
|
||||
config,
|
||||
@@ -142,7 +235,10 @@ def run_pipeline(
|
||||
motion_state,
|
||||
frame_index=frame_index,
|
||||
buffer=artifacts.overlay_buffer,
|
||||
)
|
||||
) if needs_overlay else frame
|
||||
|
||||
if verbose:
|
||||
t_overlay = time.monotonic()
|
||||
|
||||
result = FrameResult(
|
||||
frame_index=frame_index,
|
||||
@@ -152,11 +248,57 @@ def run_pipeline(
|
||||
motion_state=motion_state,
|
||||
count_events=count_events,
|
||||
)
|
||||
_consume_result(config, artifacts, annotated, result)
|
||||
_consume_result(config, artifacts, annotated, result, progress)
|
||||
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)
|
||||
|
||||
if verbose:
|
||||
t_write = time.monotonic()
|
||||
if frame_index % inference_stride == 0:
|
||||
cumulative_timings["read"] += (t_read - t0) * 1000
|
||||
cumulative_timings["infer"] += (t_infer - t_read) * 1000
|
||||
cumulative_timings["motion"] += (t_motion - t_infer) * 1000
|
||||
cumulative_timings["count"] += (t_count - t_motion) * 1000
|
||||
cumulative_timings["overlay"] += (t_overlay - t_count) * 1000
|
||||
cumulative_timings["write"] += (t_write - t_overlay) * 1000
|
||||
timed_frames += 1
|
||||
|
||||
if frame_index % verbose_interval == 0 and timed_frames > 0:
|
||||
n = timed_frames
|
||||
print(
|
||||
f"[debug ~{verbose_interval}f avg ms] "
|
||||
f"read={cumulative_timings['read']/n:.1f} "
|
||||
f"infer={cumulative_timings['infer']/n:.1f} "
|
||||
f"motion={cumulative_timings['motion']/n:.1f} "
|
||||
f"count={cumulative_timings['count']/n:.1f} "
|
||||
f"overlay={cumulative_timings['overlay']/n:.1f} "
|
||||
f"write={cumulative_timings['write']/n:.1f} "
|
||||
f"tracks={len(tracks)} "
|
||||
f"inside={artifacts.counting_zone.inside_box_count} "
|
||||
f"total={artifacts.counting_zone.total_entered_count} "
|
||||
f"motion_speed={motion_state.smoothed_speed:.1f} "
|
||||
f"backward={motion_state.backward_active}"
|
||||
)
|
||||
cumulative_timings = {k: 0.0 for k in cumulative_timings}
|
||||
timed_frames = 0
|
||||
|
||||
if progress is not None:
|
||||
elapsed = time.monotonic() - artifacts.run_start_time
|
||||
fps = frame_index / elapsed if elapsed > 0 else 0.0
|
||||
progress.render(
|
||||
frame_index, elapsed, fps,
|
||||
artifacts.counting_zone.inside_box_count,
|
||||
artifacts.counting_zone.total_entered_count,
|
||||
motion_state.backward_active,
|
||||
)
|
||||
|
||||
if motion_state.backward_active:
|
||||
stopped_reason = "backward"
|
||||
if progress is not None:
|
||||
progress.emit(f"[stop] backward detection confirmed at frame={frame_index}; ending pipeline")
|
||||
else:
|
||||
print(f"[stop] backward detection confirmed at frame={frame_index}; ending pipeline")
|
||||
break
|
||||
|
||||
@@ -183,6 +325,9 @@ def run_pipeline(
|
||||
if user_quit:
|
||||
stopped_reason = "user_quit"
|
||||
|
||||
if progress is not None:
|
||||
progress.finish()
|
||||
|
||||
return PipelineResult(
|
||||
camera_id=config.camera_id,
|
||||
total_entered_count=artifacts.counting_zone.total_entered_count,
|
||||
@@ -199,6 +344,7 @@ def _consume_result(
|
||||
artifacts: PipelineArtifacts,
|
||||
annotated,
|
||||
result: FrameResult,
|
||||
progress: _ProgressBar | None = None,
|
||||
) -> None:
|
||||
if config.display.show_window:
|
||||
cv2.imshow(config.display.window_name, annotated)
|
||||
@@ -206,13 +352,42 @@ def _consume_result(
|
||||
artifacts.writer.write(annotated)
|
||||
|
||||
for event in result.count_events:
|
||||
print(
|
||||
if config.performance.verbose:
|
||||
msg = (
|
||||
f"[frame {event.frame_index}] counted track={event.track_id} "
|
||||
f"inside_box={result.inside_box_count} total_entered={event.total_entered_after_event}"
|
||||
)
|
||||
if progress is not None:
|
||||
progress.emit(msg)
|
||||
else:
|
||||
print(msg)
|
||||
|
||||
if _should_emit_feedback(config, result.frame_index):
|
||||
_emit_periodic_feedback(config, artifacts, annotated, 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:
|
||||
cam_dir = Path(shm_dir) / f"chicken_counter_{camera_id}"
|
||||
cam_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
jpg_path = cam_dir / "frame.jpg"
|
||||
tmp_path = cam_dir / ".frame_tmp.jpg"
|
||||
cv2.imwrite(str(tmp_path), frame, [cv2.IMWRITE_JPEG_QUALITY, 75])
|
||||
tmp_path.replace(jpg_path)
|
||||
|
||||
stats = {
|
||||
"frame_index": result.frame_index,
|
||||
"inside_box_count": result.inside_box_count,
|
||||
"total_entered_count": result.total_entered_count,
|
||||
"track_count": len(result.tracks),
|
||||
"backward_active": result.motion_state.backward_active,
|
||||
"smoothed_speed": round(result.motion_state.smoothed_speed, 1),
|
||||
"count_events": len(result.count_events),
|
||||
}
|
||||
stats_path = cam_dir / "stats.json"
|
||||
stats_tmp = cam_dir / ".stats_tmp.json"
|
||||
stats_tmp.write_text(json.dumps(stats), encoding="utf-8")
|
||||
stats_tmp.replace(stats_path)
|
||||
|
||||
|
||||
def _should_emit_feedback(config: CameraConfig, frame_index: int) -> bool:
|
||||
@@ -238,15 +413,16 @@ def _emit_periodic_feedback(
|
||||
artifacts: PipelineArtifacts,
|
||||
annotated,
|
||||
result: FrameResult,
|
||||
progress: _ProgressBar | None = None,
|
||||
) -> None:
|
||||
if config.feedback.log_to_terminal:
|
||||
elapsed = time.monotonic() - artifacts.run_start_time
|
||||
fps = result.frame_index / elapsed if elapsed > 0 else 0.0
|
||||
status = "backward_stop" if result.motion_state.backward_active else "running"
|
||||
|
||||
progress = f"frame={result.frame_index}"
|
||||
progress_text = f"frame={result.frame_index}"
|
||||
if artifacts.total_source_frames:
|
||||
progress = f"frame={result.frame_index}/{artifacts.total_source_frames}"
|
||||
progress_text = f"frame={result.frame_index}/{artifacts.total_source_frames}"
|
||||
|
||||
eta_text = ""
|
||||
if artifacts.total_source_frames and fps > 0:
|
||||
@@ -254,13 +430,17 @@ def _emit_periodic_feedback(
|
||||
eta_seconds = remaining_frames / fps
|
||||
eta_text = f" eta={_format_duration(eta_seconds)}"
|
||||
|
||||
print(
|
||||
f"[checkpoint] {progress} elapsed={_format_duration(elapsed)} "
|
||||
msg = (
|
||||
f"[checkpoint] {progress_text} elapsed={_format_duration(elapsed)} "
|
||||
f"fps={fps:.1f} inside_box={result.inside_box_count} "
|
||||
f"total_entered={result.total_entered_count} "
|
||||
f"backward_active={result.motion_state.backward_active} "
|
||||
f"status={status}{eta_text}"
|
||||
)
|
||||
if progress is not None:
|
||||
progress.emit(msg)
|
||||
else:
|
||||
print(f"\r\033[K{msg}")
|
||||
|
||||
if config.feedback.save_images:
|
||||
output_dir = Path(config.feedback.image_output_dir)
|
||||
|
||||
Regular → Executable
File mode changed.
Regular → Executable
+23
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
@@ -18,6 +19,8 @@ class DetectionTracker:
|
||||
self.model_kind = model_path.suffix.lower().lstrip(".") or "unknown"
|
||||
self.model = YOLO(config.detection.model_path)
|
||||
self.tracker_config_path = str(Path(config.tracker.tracker_config_path))
|
||||
self.verbose = config.performance.verbose
|
||||
self._infer_count = 0
|
||||
print(
|
||||
f"[model] loaded {self.model_kind} from {model_path} "
|
||||
f"(imgsz={config.detection.imgsz}, device={config.detection.device})"
|
||||
@@ -57,9 +60,18 @@ class DetectionTracker:
|
||||
if self.model_kind != "engine" and self.config.performance.half:
|
||||
track_kwargs["half"] = True
|
||||
|
||||
if self.verbose:
|
||||
t_start = time.monotonic()
|
||||
|
||||
results = self.model.track(**track_kwargs)
|
||||
|
||||
if self.verbose:
|
||||
t_track = time.monotonic()
|
||||
self._infer_count += 1
|
||||
|
||||
if not results:
|
||||
if self.verbose:
|
||||
print(f"[tracker #{self._infer_count}] no detections (infer={t_track - t_start:.1f}ms)")
|
||||
return []
|
||||
|
||||
result = results[0]
|
||||
@@ -115,6 +127,17 @@ class DetectionTracker:
|
||||
)
|
||||
)
|
||||
|
||||
if self.verbose:
|
||||
unique_ids = sorted(set(t.track_id for t in tracks))
|
||||
confs = [t.confidence for t in tracks] if tracks else [0]
|
||||
print(
|
||||
f"[tracker #{self._infer_count}] "
|
||||
f"det={len(tracks)} unique={len(unique_ids)} "
|
||||
f"conf=[{min(confs):.2f}..{max(confs):.2f}] "
|
||||
f"ids={unique_ids[:10]}{'+' if len(unique_ids) > 10 else ''} "
|
||||
f"infer={t_track - t_start:.1f}ms"
|
||||
)
|
||||
|
||||
return tracks
|
||||
|
||||
@staticmethod
|
||||
|
||||
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Reference in new issue
Block a user