Coverage for orchestrant/yolo/monitor.py: 28%
373 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-19 17:06 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-19 17:06 +0000
1"""Main entry point for the YOLO monitoring pipeline."""
3from __future__ import annotations
5import json
6import os
7import platform
8import sys
9import threading
10import time
11from collections import deque
12from dataclasses import dataclass
13from pathlib import Path
14from typing import TYPE_CHECKING, Any, Protocol, cast
16import cv2
17import numpy as np
18import onnxruntime as ort
19import psutil
20from loguru import logger
22from orchestrant.paths import default_log_dir, missing_model_message, model_search_paths
23from orchestrant.pipeline.capture import CameraCapture
24from orchestrant.pipeline.capture.gstreamer import find_gstreamer_launch
25from orchestrant.pipeline.constants import (
26 DEBUG_LOG_INTERVAL_SECONDS,
27 DEFAULT_CPU_TDP_WATTS,
28 ENERGY_WH_INITIAL,
29 LOG_INTERVAL_SECONDS,
30 POWER_WATTS_INITIAL,
31 RESOURCE_LOG_INTERVAL_SECONDS,
32 STATS_UPDATE_INTERVAL_FRAMES,
33)
34from orchestrant.pipeline.logging import (
35 attach_log_buffer,
36 configure_logging,
37 create_log_buffer,
38 log_buffer_lines,
39)
40from orchestrant.pipeline.metrics.performance import PerformanceTracker
41from orchestrant.pipeline.monitoring.power import (
42 PowerMonitor,
43 get_cpu_freq_ratio,
44)
45from orchestrant.pipeline.monitoring.system import SystemMonitor
46from orchestrant.pipeline.tracking.centroid import SimpleCentroidTracker
47from orchestrant.pipeline.types import (
48 CameraConfig,
49 CaptureBackend,
50 PerformanceMetrics,
51 SystemStats,
52)
53from orchestrant.pipeline.ui.flet_viewer import FLET_AVAILABLE, FletViewer
54from orchestrant.yolo.cli import parse_args
55from orchestrant.yolo.core.postprocess import DecodeConfig, postprocess
56from orchestrant.yolo.core.preprocess import infer_input_size, preprocess, wants_stretch
57from orchestrant.yolo.ui.draw import draw_detections
60if TYPE_CHECKING:
61 import argparse
62 from collections.abc import Callable
64 from orchestrant.pipeline.types import (
65 Track,
66 )
69get_cpu_info: Callable[[], dict] | None = None
70try:
71 from cpuinfo import get_cpu_info as _real_get_cpu_info
73 get_cpu_info = _real_get_cpu_info
74except ImportError: # pragma: no cover - optional dependency
75 get_cpu_info = None
78class ViewerProtocol(Protocol):
79 """Runtime viewer contract used by the monitor."""
81 def is_open(self) -> bool: # noqa: D102
82 ...
84 def render(
85 self,
86 frame: np.ndarray,
87 *,
88 perf_metrics: PerformanceMetrics | None = None,
89 sys_stats: SystemStats | None = None,
90 proc_stats: dict | None = None,
91 camera_info: dict | None = None,
92 detections_count: int | None = None,
93 classification: dict | None = None,
94 log_lines: list[str] | None = None,
95 hardware_info: dict | None = None,
96 power_info: dict | None = None,
97 ) -> None:
98 """Render a frame and associated telemetry in the active viewer."""
99 ...
101 def close(self) -> None: # noqa: D102
102 ...
105class ViewerLoopProtocol(ViewerProtocol, Protocol):
106 """Viewer contract for backends with their own event loop."""
108 def run(self) -> None: # noqa: D102
109 ...
112def _extract_camera_dimensions(
113 camera_info: dict[str, object],
114) -> tuple[int, int]:
115 """Extract width and height from camera info dict."""
116 width_raw = camera_info.get("width", 0)
117 height_raw = camera_info.get("height", 0)
118 width = int(width_raw) if isinstance(width_raw, int | float | str) else 0
119 height = int(height_raw) if isinstance(height_raw, int | float | str) else 0
120 return width, height
123def _get_cpu_model() -> str:
124 model = ""
125 if get_cpu_info is not None:
126 try:
127 info = get_cpu_info()
128 model = info.get("brand_raw") or info.get("brand") or ""
129 except Exception:
130 model = ""
131 if not model:
132 model = platform.processor()
133 if not model and platform.system() == "Windows":
134 model = os.environ.get("PROCESSOR_IDENTIFIER", "")
135 return model or "Unknown"
138@dataclass
139class MonitorContext:
140 """Static context for running the monitoring loop."""
142 args: argparse.Namespace
143 camera: CameraCapture
144 camera_info: dict[str, object]
145 sys_monitor: SystemMonitor
146 session: ort.InferenceSession
147 input_name: str
148 input_size: tuple[int, int]
149 stretch: bool
150 perf_tracker: PerformanceTracker
151 tracker: SimpleCentroidTracker
152 tracks: dict[int, Track]
153 cpu_history: deque[float]
154 log_buffer: deque[str]
155 power_monitor: PowerMonitor
156 hardware_info: dict[str, object]
157 viewer: ViewerProtocol | None
158 cpu_tdp_watts: float
161@dataclass
162class RuntimeState:
163 """Mutable runtime state for the monitoring loop."""
165 frame_count: int
166 sys_stats: SystemStats
167 proc_stats: dict[str, float | int]
168 perf_metrics: PerformanceMetrics
169 power_last_time: float
170 power_info: dict[str, float]
171 energy_wh: float
172 output_debug_logged: bool
173 last_log_time: float
174 last_debug_log_time: float
175 last_resource_log_time: float
178def _init_runtime_state() -> RuntimeState:
179 now = time.perf_counter()
180 return RuntimeState(
181 frame_count=0,
182 sys_stats=SystemStats(),
183 proc_stats={"cpu_percent": 0.0, "memory_mb": 0.0, "threads": 0},
184 perf_metrics=PerformanceMetrics(),
185 power_last_time=now,
186 power_info={
187 "system_power_watts": POWER_WATTS_INITIAL,
188 "cpu_power_watts": POWER_WATTS_INITIAL,
189 "gpu_power_watts": POWER_WATTS_INITIAL,
190 "energy_wh": ENERGY_WH_INITIAL,
191 },
192 energy_wh=ENERGY_WH_INITIAL,
193 output_debug_logged=False,
194 last_log_time=now,
195 last_debug_log_time=now,
196 last_resource_log_time=now,
197 )
200def _update_tracks_if_enabled(
201 ctx: MonitorContext,
202 frame: np.ndarray,
203 detections: list[dict[str, object]],
204 tracks: dict[int, Track],
205) -> dict[int, Track]:
206 if not ctx.args.map:
207 return tracks
208 fh, fw = frame.shape[:2]
209 person_centroids: list[tuple[float, float]] = []
210 for det in detections:
211 if det.get("class_id") != 0:
212 continue
213 bbox_obj = det.get("bbox")
214 if not isinstance(bbox_obj, list | tuple) or len(bbox_obj) < 4:
215 continue
216 x1_obj, _y1_obj, x2_obj, y2_obj = bbox_obj[:4]
217 if not isinstance(x1_obj, int | float):
218 continue
219 if not isinstance(x2_obj, int | float):
220 continue
221 if not isinstance(y2_obj, int | float):
222 continue
223 cx = (float(x1_obj) + float(x2_obj)) / 2.0
224 cy = float(y2_obj)
225 person_centroids.append((float(cx) / max(1, fw), float(cy) / max(1, fh)))
226 return ctx.tracker.update(person_centroids, now_ts=time.perf_counter())
229def _update_stats_if_needed(
230 ctx: MonitorContext,
231 state: RuntimeState,
232) -> None:
233 if state.frame_count % STATS_UPDATE_INTERVAL_FRAMES != 0:
234 return
235 state.sys_stats = ctx.sys_monitor.get_stats()
236 state.proc_stats = ctx.sys_monitor.get_process_stats()
237 state.perf_metrics = ctx.perf_tracker.get_metrics()
238 now_power = time.perf_counter()
239 dt = max(0.0, now_power - state.power_last_time)
240 state.power_last_time = now_power
241 state.power_info = ctx.power_monitor.update(
242 sys_gpu_power=float(getattr(state.sys_stats, "gpu_power_watts", 0.0)),
243 cpu_util_percent=state.sys_stats.cpu_percent,
244 cpu_tdp_watts=ctx.cpu_tdp_watts,
245 freq_ratio=get_cpu_freq_ratio(),
246 dt_seconds=dt,
247 )
248 state.energy_wh = state.power_info["energy_wh"]
251def _update_cpu_history(
252 ctx: MonitorContext,
253 state: RuntimeState,
254) -> None:
255 if not ctx.args.cpu_plot:
256 return
257 state_value = float(np.clip(state.proc_stats.get("cpu_percent", 0.0), 0.0, 100.0))
258 ctx.cpu_history.append(state_value)
261def _draw_frame_if_needed(
262 ctx: MonitorContext,
263 state: RuntimeState,
264 frame: np.ndarray,
265 detections: list[dict[str, object]],
266 classification: dict[str, object] | None,
267) -> np.ndarray:
268 if ctx.args.no_display:
269 return frame
270 return draw_detections(
271 frame,
272 detections,
273 state.perf_metrics,
274 state.sys_stats,
275 state.proc_stats,
276 ctx.camera_info,
277 cpu_history=ctx.cpu_history if ctx.args.cpu_plot else None,
278 classification=classification,
279 tracks=ctx.tracks if ctx.args.map else None,
280 map_size=ctx.args.map_size,
281 debug_boxes=ctx.args.debug_boxes,
282 show_stats_panel=ctx.args.ui != "flet",
283 show_detection_panel=ctx.args.ui != "flet",
284 )
287def _log_periodic_metrics(
288 ctx: MonitorContext,
289 state: RuntimeState,
290 detections: list[dict[str, object]],
291 classification: dict[str, object] | None,
292) -> None:
293 current_time = time.perf_counter()
294 if current_time - state.last_log_time >= LOG_INTERVAL_SECONDS:
295 metrics = ctx.perf_tracker.get_metrics()
296 logger.info(
297 "Camera: {:.1f} FPS | Inference: {:.1f}ms | Budget: {:.0f}% | Detections: {}",
298 metrics.camera_fps,
299 metrics.inference_ms,
300 metrics.frame_budget_percent,
301 len(detections),
302 )
303 state.last_log_time = current_time
305 if (
306 ctx.args.debug_detections
307 and current_time - state.last_debug_log_time >= DEBUG_LOG_INTERVAL_SECONDS
308 ):
309 sample = detections[:3]
310 logger.info("Sample detections: {}", sample)
311 if classification is not None:
312 logger.info("Classification: {}", classification)
313 state.last_debug_log_time = current_time
315 if current_time - state.last_resource_log_time >= RESOURCE_LOG_INTERVAL_SECONDS:
316 metrics = ctx.perf_tracker.get_metrics()
317 logger.info(
318 "System CPU: {:.1f}% | RAM: {:.1f}/{:.1f}GB",
319 state.sys_stats.cpu_percent,
320 state.sys_stats.ram_used_gb,
321 state.sys_stats.ram_total_gb,
322 )
324 if state.sys_stats.gpu_name != "N/A":
325 logger.info(
326 "GPU: {:.0f}% | VRAM: {:.1f}/{:.1f}GB | Temp: {:.0f}°C",
327 state.sys_stats.gpu_percent,
328 state.sys_stats.gpu_memory_used_gb,
329 state.sys_stats.gpu_memory_total_gb,
330 state.sys_stats.gpu_temp_celsius,
331 )
333 logger.info(
334 "Process: CPU {:.1f}% | {:.0f}MB RAM",
335 state.proc_stats["cpu_percent"],
336 state.proc_stats["memory_mb"],
337 )
339 if state.power_info["system_power_watts"] > 0.0:
340 logger.info(
341 "Power (est): {:.0f}W | CPU {:.0f}W | GPU {:.0f}W | Energy {:.3f}Wh",
342 state.power_info["system_power_watts"],
343 state.power_info["cpu_power_watts"],
344 state.power_info["gpu_power_watts"],
345 state.power_info["energy_wh"],
346 )
347 elif state.power_info["gpu_power_watts"] > 0.0:
348 logger.info(
349 "Power: GPU {:.0f}W | Energy {:.3f}Wh",
350 state.power_info["gpu_power_watts"],
351 state.power_info["energy_wh"],
352 )
354 headroom = 100 - metrics.frame_budget_percent
355 potential = (
356 int(metrics.inference_capacity_fps / metrics.camera_fps)
357 if metrics.camera_fps > 0
358 else 0
359 )
360 logger.info(
361 "Performance: {:.0f}% budget | {:.0f}% headroom | ~{} streams",
362 metrics.frame_budget_percent,
363 headroom,
364 potential,
365 )
366 logger.info("-" * 60)
367 state.last_resource_log_time = current_time
370def _render_output(
371 ctx: MonitorContext,
372 state: RuntimeState,
373 frame: np.ndarray,
374 detections: list[dict[str, object]],
375 classification: dict[str, object] | None,
376) -> bool:
377 if ctx.args.no_display:
378 return True
379 if ctx.viewer is not None:
380 if not ctx.viewer.is_open():
381 logger.info("Quit requested by user")
382 return False
383 ctx.viewer.render(
384 frame,
385 perf_metrics=state.perf_metrics,
386 sys_stats=state.sys_stats,
387 proc_stats=state.proc_stats,
388 camera_info=ctx.camera_info,
389 detections_count=len(detections),
390 classification=classification,
391 log_lines=log_buffer_lines(ctx.log_buffer),
392 hardware_info=ctx.hardware_info,
393 power_info=state.power_info,
394 )
395 return True
397 cv2.imshow("Object Detection", frame)
398 if cv2.waitKey(1) & 0xFF == ord("q"):
399 logger.info("Quit requested by user")
400 return False
401 return True
404def _run_detection_loop(ctx: MonitorContext) -> int:
405 state = _init_runtime_state()
406 logger.info("-" * 60)
407 logger.info("Starting detection loop. Press 'q' to quit.")
408 logger.info("-" * 60)
410 try:
411 while True:
412 ret, frame = ctx.camera.read()
413 if not ret or frame is None:
414 logger.warning("Failed to grab frame")
415 continue
417 ctx.perf_tracker.tick_camera()
418 blob, scale, pad_x, pad_y = preprocess(
419 frame, input_size=ctx.input_size, stretch=ctx.stretch
420 )
422 inference_start = time.perf_counter()
423 outputs = ctx.session.run(None, {ctx.input_name: blob})
424 inference_ms = (time.perf_counter() - inference_start) * 1000
425 ctx.perf_tracker.add_inference_time(inference_ms)
427 detections, classification = postprocess(
428 [np.asarray(output) for output in outputs],
429 DecodeConfig(
430 scale=scale,
431 pad_x=pad_x,
432 pad_y=pad_y,
433 input_size=ctx.input_size,
434 conf_threshold=ctx.args.conf,
435 debug_boxes=ctx.args.debug_boxes,
436 ),
437 debug_output=ctx.args.debug_output and not state.output_debug_logged,
438 )
439 if ctx.args.debug_output and not state.output_debug_logged:
440 state.output_debug_logged = True
442 ctx.tracks = _update_tracks_if_enabled(
443 ctx,
444 frame,
445 detections,
446 ctx.tracks,
447 )
449 state.frame_count += 1
450 _update_stats_if_needed(ctx, state)
451 _update_cpu_history(ctx, state)
453 frame = _draw_frame_if_needed(ctx, state, frame, detections, classification)
454 _log_periodic_metrics(ctx, state, detections, classification)
456 if not _render_output(ctx, state, frame, detections, classification):
457 break
459 except KeyboardInterrupt:
460 logger.info("Interrupted by user")
461 except Exception as exc:
462 logger.exception("Error during detection: {}", exc)
463 finally:
464 logger.info("=" * 60)
465 logger.info("Session Summary")
467 final_metrics = ctx.perf_tracker.get_metrics()
468 logger.info("Capture backend: {}", ctx.camera_info["backend"])
469 logger.info("Total frames: {}", state.frame_count)
470 logger.info("Avg throughput: {:.1f} FPS", final_metrics.actual_throughput_fps)
471 logger.info("Avg inference: {:.1f}ms", final_metrics.inference_ms)
472 logger.info("Avg budget used: {:.1f}%", final_metrics.frame_budget_percent)
474 ctx.camera.release()
475 if ctx.viewer is not None:
476 ctx.viewer.close()
477 cv2.destroyAllWindows()
478 ctx.power_monitor.shutdown()
479 ctx.sys_monitor.shutdown()
480 logger.success("Cleanup complete. Goodbye!")
482 return 0
485class MonitorInitError(RuntimeError):
486 """Raised when monitor initialization fails."""
489def _init_viewer(
490 args: argparse.Namespace,
491 camera_info: dict[str, object],
492 camera: CameraCapture,
493 sys_monitor: SystemMonitor,
494) -> ViewerProtocol | None:
495 if args.ui != "flet" or args.no_display:
496 return None
498 width, height = _extract_camera_dimensions(camera_info)
499 try:
500 viewer = FletViewer(
501 width,
502 height,
503 "YOLO Monitor",
504 web=args.web,
505 host=args.host,
506 port=args.port,
507 )
508 except Exception as exc:
509 logger.error("Failed to initialize the Flet viewer: {}", exc)
510 camera.release()
511 sys_monitor.shutdown()
512 message = "Flet viewer initialization failed"
513 raise MonitorInitError(message) from exc
514 else:
515 logger.success("Flet viewer initialized")
516 return viewer
519def resolve_ui(args: argparse.Namespace) -> str:
520 """`auto` is Flet where installed, else OpenCV's window; --web always means Flet."""
521 if args.ui == "auto":
522 return "flet" if FLET_AVAILABLE or args.web else "opencv"
523 return args.ui
526def _log_platform_info(initial_stats: SystemStats) -> None:
527 logger.info("System RAM: {:.1f} GB", initial_stats.ram_total_gb)
528 logger.info("CPU model: {}", _get_cpu_model())
529 try:
530 cpu_freq = psutil.cpu_freq()
531 if cpu_freq and cpu_freq.max:
532 logger.info(
533 "CPU freq: {:.0f} MHz (max {:.0f} MHz)",
534 cpu_freq.current or 0.0,
535 cpu_freq.max,
536 )
537 except Exception as exc:
538 logger.debug("Unable to read CPU frequency: {}", exc)
539 logger.info(
540 "CPU cores: {} physical, {} logical",
541 psutil.cpu_count(logical=False),
542 psutil.cpu_count(),
543 )
544 if initial_stats.gpu_name != "N/A":
545 logger.info(
546 "GPU: {} ({:.1f} GB VRAM)",
547 initial_stats.gpu_name,
548 initial_stats.gpu_memory_total_gb,
549 )
552def _build_context(args: argparse.Namespace, log_buffer: deque[str]) -> MonitorContext:
553 logger.info("=" * 60)
554 logger.info("Object Detection with System Monitoring")
555 logger.info("=" * 60)
557 logger.info("Platform: {} {}", platform.system(), platform.release())
558 logger.info("Python: {}", platform.python_version())
559 logger.info("OpenCV: {}", cv2.__version__)
561 gst_path, gst_status = find_gstreamer_launch()
562 if gst_path:
563 logger.info("GStreamer: {}", gst_status)
564 else:
565 logger.warning("GStreamer: {}", gst_status)
567 if args.backend == "gstreamer" and gst_path is None:
568 logger.error("GStreamer backend requested but not found!")
569 logger.error(
570 "Install GStreamer from: https://gstreamer.freedesktop.org/download/"
571 )
572 message = "GStreamer backend requested but not found"
573 raise MonitorInitError(message)
575 camera_config = CameraConfig(
576 device_index=args.camera,
577 width=args.width,
578 height=args.height,
579 fps=args.fps,
580 backend=CaptureBackend(args.backend),
581 )
583 logger.info("Requested: {}x{} @ {} FPS", args.width, args.height, args.fps)
584 logger.info("Backend: {}", args.backend)
585 logger.info("UI: {}", args.ui)
587 sys_monitor = SystemMonitor(gpu_device_id=args.gpu)
588 initial_stats = sys_monitor.get_stats()
589 _log_platform_info(initial_stats)
591 logger.info("Loading model: {}", args.model)
592 try:
593 session = ort.InferenceSession(args.model, providers=select_providers(args.gpu))
594 except Exception as exc:
595 logger.error("Failed to load model: {}", exc)
596 sys_monitor.shutdown()
597 message = "Failed to load model"
598 raise MonitorInitError(message) from exc
600 active_provider = session.get_providers()[0]
601 logger.success("Model loaded using: {}", active_provider)
603 model_input = session.get_inputs()[0]
604 logger.debug("Model input: {}, shape: {}", model_input.name, model_input.shape)
605 input_size = infer_input_size(model_input.shape)
607 logger.info("-" * 60)
608 camera = CameraCapture(camera_config)
609 if not camera.open():
610 logger.error("Failed to open camera!")
611 sys_monitor.shutdown()
612 message = "Failed to open camera"
613 raise MonitorInitError(message)
615 camera_info = camera.get_info()
616 logger.info("Active backend: {}", camera_info["backend"])
617 logger.info("Capture pipeline: {}", camera_info.get("pipeline", ""))
619 hardware_info = {
620 "cpu_model": _get_cpu_model(),
621 "ram_total_gb": initial_stats.ram_total_gb,
622 "gpu_model": initial_stats.gpu_name,
623 "vram_total_gb": initial_stats.gpu_memory_total_gb,
624 }
626 perf_tracker = PerformanceTracker(avg_frames=30)
627 tracker = SimpleCentroidTracker()
628 tracks: dict[int, Track] = {}
629 cpu_history: deque[float] = deque(maxlen=max(2, int(args.cpu_history)))
631 cpu_tdp_watts = float(
632 os.getenv("KATAGLYPHIS_CPU_TDP_WATTS", str(DEFAULT_CPU_TDP_WATTS))
633 or DEFAULT_CPU_TDP_WATTS
634 )
635 logger.info("CPU power baseline (TDP): {:.0f} W", cpu_tdp_watts)
637 power_monitor = PowerMonitor()
638 viewer = _init_viewer(args, camera_info, camera, sys_monitor)
640 return MonitorContext(
641 args=args,
642 camera=camera,
643 camera_info=camera_info,
644 sys_monitor=sys_monitor,
645 session=session,
646 input_name=model_input.name,
647 input_size=input_size,
648 stretch=wants_stretch(session),
649 perf_tracker=perf_tracker,
650 tracker=tracker,
651 tracks=tracks,
652 cpu_history=cpu_history,
653 log_buffer=log_buffer,
654 power_monitor=power_monitor,
655 hardware_info=hardware_info,
656 viewer=viewer,
657 cpu_tdp_watts=cpu_tdp_watts,
658 )
661def select_providers(gpu: int) -> list[Any]:
662 """CUDA, then DirectML, then CPU, as far as this ONNX Runtime build offers them."""
663 available = set(ort.get_available_providers())
664 providers: list[Any] = []
665 if "CUDAExecutionProvider" in available:
666 providers.append(
667 (
668 "CUDAExecutionProvider",
669 {"device_id": gpu, "arena_extend_strategy": "kNextPowerOfTwo"},
670 )
671 )
672 if "DmlExecutionProvider" in available:
673 providers.append(("DmlExecutionProvider", {"device_id": gpu}))
674 providers.append("CPUExecutionProvider")
675 return providers
678def run_self_test(args: argparse.Namespace) -> int:
679 """One inference on a blank frame, printed as JSON: the install check CI runs with no camera or display."""
680 model = Path(args.model)
681 if not model.is_file():
682 searched = ", ".join(str(path) for path in model_search_paths())
683 failure = {
684 "ok": False,
685 "error": missing_model_message(model),
686 "searched": searched,
687 }
688 sys.stdout.write(json.dumps(failure) + "\n")
689 return 1
690 session = ort.InferenceSession(str(model), providers=select_providers(args.gpu))
691 meta = session.get_inputs()[0]
692 input_size = infer_input_size(meta.shape)
693 stretch = wants_stretch(session)
694 frame = np.zeros((args.height, args.width, 3), dtype=np.uint8)
695 blob, scale, pad_x, pad_y = preprocess(
696 frame, input_size=input_size, stretch=stretch
697 )
698 started = time.perf_counter()
699 outputs = session.run(None, {meta.name: blob})
700 inference_ms = (time.perf_counter() - started) * 1000
701 detections, _ = postprocess(
702 [np.asarray(output) for output in outputs],
703 DecodeConfig(
704 scale=scale,
705 pad_x=pad_x,
706 pad_y=pad_y,
707 input_size=input_size,
708 conf_threshold=args.conf,
709 debug_boxes=False,
710 ),
711 )
712 report = {
713 "ok": True,
714 "python": platform.python_version(),
715 "onnxruntime": ort.__version__,
716 "onnxruntime_module": str(Path(ort.__file__).parent),
717 "opencv": cv2.__version__,
718 "providers": session.get_providers(),
719 "model": str(model.resolve()),
720 "input_size": list(input_size),
721 "preprocess": "stretch" if stretch else "letterbox",
722 "outputs": [list(np.asarray(output).shape) for output in outputs],
723 "detections_on_blank_frame": len(detections),
724 "inference_ms": round(inference_ms, 2),
725 "log_dir": str(default_log_dir()),
726 }
727 sys.stdout.write(json.dumps(report, indent=2) + "\n")
728 return 0
731def run_yolo_monitor(argv: list[str] | None = None) -> int:
732 """Entry point for running the YOLO monitor."""
733 args = parse_args(argv)
734 if args.self_test:
735 return run_self_test(args)
736 configure_logging(args.log_level, log_dir=str(default_log_dir()))
737 args.ui = resolve_ui(args)
738 if args.web and args.ui != "flet":
739 logger.error("--web serves the Flet viewer; it cannot go with --ui {}", args.ui)
740 return 1
741 if not Path(args.model).is_file():
742 logger.error("{}", missing_model_message(args.model))
743 return 1
744 log_buffer = create_log_buffer(max_lines=200)
745 attach_log_buffer(log_buffer, level="INFO")
746 try:
747 ctx = _build_context(args, log_buffer)
748 except MonitorInitError:
749 return 1
751 if ctx.viewer is None:
752 return _run_detection_loop(ctx)
754 # The Flet viewer owns the main thread until its window closes; detection runs beside it.
755 exit_code = 0
756 viewer = cast("ViewerLoopProtocol", ctx.viewer)
758 def _run_loop() -> None:
759 nonlocal exit_code
760 exit_code = _run_detection_loop(ctx)
762 worker = threading.Thread(target=_run_loop, name="yolo-detection", daemon=True)
763 worker.start()
764 viewer.run()
765 worker.join()
766 return exit_code
769if __name__ == "__main__":
770 raise SystemExit(run_yolo_monitor())