Try to make it work in Jetson Orin Nano
This commit is contained in:
1 parent
a54a070ca9
commit
7aa5d5eb61
53 files changed
+778
-28
No files matched your search
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
+194
-14
@@ -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
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.
Reference in new issue
Block a user