Try to make it work in Jetson Orin Nano

This commit is contained in:
proitlab committed 2026-07-21 16:02:51 +07:00
1 parent a54a070ca9
commit 7aa5d5eb61
53 files changed
+778 -28

No files matched your search

View File
File mode changed.
View File
File mode changed.
View File
File mode changed.
View File
File mode changed.
View File
File mode changed.
View File
File mode changed.
Regular → Executable
View File
File mode changed.
View File
File mode changed.
View File
File mode changed.
View File
File mode changed.
View File
File mode changed.
View File
File mode changed.
View File
File mode changed.
View File
File mode changed.
View File
File mode changed.
View File
File mode changed.
View File
File mode changed.
View File
File mode changed.
View File
File mode changed.
View File
File mode changed.
View File
File mode changed.
View File
File mode changed.
View File
File mode changed.
View File
File mode changed.
View File
File mode changed.
View File
File mode changed.
View File
File mode changed.
+16 -4
View File
@@ -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
View File
File mode changed.
Regular → Executable
+10 -3
View File
@@ -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
View File
File mode changed.
Regular → Executable
+13 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
File mode changed.
Regular → Executable
+194 -14
View File
@@ -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,12 +248,58 @@ 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"
print(f"[stop] backward detection confirmed at frame={frame_index}; ending pipeline")
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
if config.display.max_frames and frame_index >= config.display.max_frames:
@@ -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(
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 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
View File
File mode changed.
Regular → Executable
+23
View File
@@ -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
View File
File mode changed.
View File
File mode changed.