Coverage for orchestr_ant_ion / yolo / monitor.py: 0%

354 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-03-19 08:36 +0000

1"""Main entry point for the YOLO monitoring pipeline.""" 

2 

3from __future__ import annotations 

4 

5import os 

6import platform 

7import threading 

8import time 

9from collections import deque 

10from dataclasses import dataclass 

11from typing import TYPE_CHECKING, Any, Protocol, cast 

12 

13import cv2 

14import numpy as np 

15import onnxruntime as ort 

16import psutil 

17from loguru import logger 

18 

19from orchestr_ant_ion.pipeline.capture import CameraCapture 

20from orchestr_ant_ion.pipeline.capture.gstreamer import find_gstreamer_launch 

21from orchestr_ant_ion.pipeline.constants import ( 

22 DEBUG_LOG_INTERVAL_SECONDS, 

23 DEFAULT_CPU_TDP_WATTS, 

24 ENERGY_WH_INITIAL, 

25 LOG_INTERVAL_SECONDS, 

26 POWER_WATTS_INITIAL, 

27 RESOURCE_LOG_INTERVAL_SECONDS, 

28 STATS_UPDATE_INTERVAL_FRAMES, 

29) 

30from orchestr_ant_ion.pipeline.logging import ( 

31 attach_log_buffer, 

32 configure_logging, 

33 create_log_buffer, 

34) 

35from orchestr_ant_ion.pipeline.metrics.performance import PerformanceTracker 

36from orchestr_ant_ion.pipeline.monitoring.power import ( 

37 PowerMonitor, 

38 get_cpu_freq_ratio, 

39) 

40from orchestr_ant_ion.pipeline.monitoring.system import SystemMonitor 

41from orchestr_ant_ion.pipeline.tracking.centroid import SimpleCentroidTracker 

42from orchestr_ant_ion.pipeline.types import ( 

43 CameraConfig, 

44 CaptureBackend, 

45 PerformanceMetrics, 

46 SystemStats, 

47) 

48from orchestr_ant_ion.pipeline.ui.dearpygui import DearPyGuiViewer 

49from orchestr_ant_ion.yolo.cli import parse_args 

50from orchestr_ant_ion.yolo.core.postprocess import DecodeConfig, postprocess 

51from orchestr_ant_ion.yolo.core.preprocess import infer_input_size, preprocess 

52from orchestr_ant_ion.yolo.ui.draw import draw_detections 

53 

54 

55WxPythonViewer: Any = None 

56_WX_VIEWER_IMPORT_ERROR: ImportError | None = None 

57try: 

58 from orchestr_ant_ion.pipeline.ui.wx import WxPythonViewer as _WxPythonViewer 

59except ImportError as exc: # pragma: no cover - optional dependency 

60 _WX_VIEWER_IMPORT_ERROR = exc 

61else: 

62 WxPythonViewer = _WxPythonViewer 

63 _WX_VIEWER_IMPORT_ERROR = None 

64 

65if TYPE_CHECKING: 

66 import argparse 

67 from collections.abc import Callable 

68 

69 from orchestr_ant_ion.pipeline.types import ( 

70 Track, 

71 ) 

72 

73 

74get_cpu_info: Callable[[], dict] | None = None 

75try: 

76 from cpuinfo import get_cpu_info as _real_get_cpu_info 

77 

78 get_cpu_info = _real_get_cpu_info 

79except ImportError: # pragma: no cover - optional dependency 

80 get_cpu_info = None 

81 

82 

83class ViewerProtocol(Protocol): 

84 """Runtime viewer contract used by the monitor.""" 

85 

86 def is_open(self) -> bool: # noqa: D102 

87 ... 

88 

89 def render( 

90 self, 

91 frame: np.ndarray, 

92 perf_metrics: PerformanceMetrics | None = None, 

93 sys_stats: SystemStats | None = None, 

94 proc_stats: dict | None = None, 

95 camera_info: dict | None = None, 

96 detections_count: int | None = None, 

97 classification: dict | None = None, 

98 log_lines: list[str] | None = None, 

99 hardware_info: dict | None = None, 

100 power_info: dict | None = None, 

101 ) -> None: 

102 """Render a frame and associated telemetry in the active viewer.""" 

103 ... 

104 

105 def close(self) -> None: # noqa: D102 

106 ... 

107 

108 

109class ViewerLoopProtocol(ViewerProtocol, Protocol): 

110 """Viewer contract for backends with their own event loop.""" 

111 

112 def run(self) -> None: # noqa: D102 

113 ... 

114 

115 

116def _extract_camera_dimensions( 

117 camera_info: dict[str, object], 

118) -> tuple[int, int]: 

119 """Extract width and height from camera info dict.""" 

120 width_raw = camera_info.get("width", 0) 

121 height_raw = camera_info.get("height", 0) 

122 width = int(width_raw) if isinstance(width_raw, int | float | str) else 0 

123 height = int(height_raw) if isinstance(height_raw, int | float | str) else 0 

124 return width, height 

125 

126 

127def _get_cpu_model() -> str: 

128 model = "" 

129 if get_cpu_info is not None: 

130 try: 

131 info = get_cpu_info() 

132 model = info.get("brand_raw") or info.get("brand") or "" 

133 except Exception: 

134 model = "" 

135 if not model: 

136 model = platform.processor() 

137 if not model and platform.system() == "Windows": 

138 model = os.environ.get("PROCESSOR_IDENTIFIER", "") 

139 return model or "Unknown" 

140 

141 

142@dataclass 

143class MonitorContext: 

144 """Static context for running the monitoring loop.""" 

145 

146 args: argparse.Namespace 

147 camera: CameraCapture 

148 camera_info: dict[str, object] 

149 sys_monitor: SystemMonitor 

150 session: ort.InferenceSession 

151 input_name: str 

152 input_size: tuple[int, int] 

153 perf_tracker: PerformanceTracker 

154 tracker: SimpleCentroidTracker 

155 tracks: dict[int, Track] 

156 cpu_history: deque[float] 

157 log_buffer: deque[str] 

158 power_monitor: PowerMonitor 

159 hardware_info: dict[str, object] 

160 viewer: ViewerProtocol | None 

161 cpu_tdp_watts: float 

162 

163 

164@dataclass 

165class RuntimeState: 

166 """Mutable runtime state for the monitoring loop.""" 

167 

168 frame_count: int 

169 sys_stats: SystemStats 

170 proc_stats: dict[str, float | int] 

171 perf_metrics: PerformanceMetrics 

172 power_last_time: float 

173 power_info: dict[str, float] 

174 energy_wh: float 

175 output_debug_logged: bool 

176 last_log_time: float 

177 last_debug_log_time: float 

178 last_resource_log_time: float 

179 

180 

181def _init_runtime_state() -> RuntimeState: 

182 now = time.perf_counter() 

183 return RuntimeState( 

184 frame_count=0, 

185 sys_stats=SystemStats(), 

186 proc_stats={"cpu_percent": 0.0, "memory_mb": 0.0, "threads": 0}, 

187 perf_metrics=PerformanceMetrics(), 

188 power_last_time=now, 

189 power_info={ 

190 "system_power_watts": POWER_WATTS_INITIAL, 

191 "cpu_power_watts": POWER_WATTS_INITIAL, 

192 "gpu_power_watts": POWER_WATTS_INITIAL, 

193 "energy_wh": ENERGY_WH_INITIAL, 

194 }, 

195 energy_wh=ENERGY_WH_INITIAL, 

196 output_debug_logged=False, 

197 last_log_time=now, 

198 last_debug_log_time=now, 

199 last_resource_log_time=now, 

200 ) 

201 

202 

203def _update_tracks_if_enabled( 

204 ctx: MonitorContext, 

205 frame: np.ndarray, 

206 detections: list[dict[str, object]], 

207 tracks: dict[int, Track], 

208) -> dict[int, Track]: 

209 if not ctx.args.map: 

210 return tracks 

211 fh, fw = frame.shape[:2] 

212 person_centroids: list[tuple[float, float]] = [] 

213 for det in detections: 

214 if det.get("class_id") != 0: 

215 continue 

216 bbox_obj = det.get("bbox") 

217 if not isinstance(bbox_obj, list | tuple) or len(bbox_obj) < 4: 

218 continue 

219 x1_obj, _y1_obj, x2_obj, y2_obj = bbox_obj[:4] 

220 if not isinstance(x1_obj, int | float): 

221 continue 

222 if not isinstance(x2_obj, int | float): 

223 continue 

224 if not isinstance(y2_obj, int | float): 

225 continue 

226 cx = (float(x1_obj) + float(x2_obj)) / 2.0 

227 cy = float(y2_obj) 

228 person_centroids.append((float(cx) / max(1, fw), float(cy) / max(1, fh))) 

229 return ctx.tracker.update(person_centroids, now_ts=time.perf_counter()) 

230 

231 

232def _update_stats_if_needed( 

233 ctx: MonitorContext, 

234 state: RuntimeState, 

235) -> None: 

236 if state.frame_count % STATS_UPDATE_INTERVAL_FRAMES != 0: 

237 return 

238 state.sys_stats = ctx.sys_monitor.get_stats() 

239 state.proc_stats = ctx.sys_monitor.get_process_stats() 

240 state.perf_metrics = ctx.perf_tracker.get_metrics() 

241 now_power = time.perf_counter() 

242 dt = max(0.0, now_power - state.power_last_time) 

243 state.power_last_time = now_power 

244 state.power_info = ctx.power_monitor.update( 

245 sys_gpu_power=float(getattr(state.sys_stats, "gpu_power_watts", 0.0)), 

246 cpu_util_percent=state.sys_stats.cpu_percent, 

247 cpu_tdp_watts=ctx.cpu_tdp_watts, 

248 freq_ratio=get_cpu_freq_ratio(), 

249 dt_seconds=dt, 

250 ) 

251 state.energy_wh = state.power_info["energy_wh"] 

252 

253 

254def _update_cpu_history( 

255 ctx: MonitorContext, 

256 state: RuntimeState, 

257) -> None: 

258 if not ctx.args.cpu_plot: 

259 return 

260 state_value = float(np.clip(state.proc_stats.get("cpu_percent", 0.0), 0.0, 100.0)) 

261 ctx.cpu_history.append(state_value) 

262 

263 

264def _draw_frame_if_needed( 

265 ctx: MonitorContext, 

266 state: RuntimeState, 

267 frame: np.ndarray, 

268 detections: list[dict[str, object]], 

269 classification: dict[str, object] | None, 

270) -> np.ndarray: 

271 if ctx.args.no_display: 

272 return frame 

273 return draw_detections( 

274 frame, 

275 detections, 

276 state.perf_metrics, 

277 state.sys_stats, 

278 state.proc_stats, 

279 ctx.camera_info, 

280 cpu_history=ctx.cpu_history if ctx.args.cpu_plot else None, 

281 classification=classification, 

282 tracks=ctx.tracks if ctx.args.map else None, 

283 map_size=ctx.args.map_size, 

284 debug_boxes=ctx.args.debug_boxes, 

285 show_stats_panel=ctx.args.ui not in {"dearpygui", "wxpython"}, 

286 show_detection_panel=ctx.args.ui not in {"dearpygui", "wxpython"}, 

287 ) 

288 

289 

290def _log_periodic_metrics( 

291 ctx: MonitorContext, 

292 state: RuntimeState, 

293 detections: list[dict[str, object]], 

294 classification: dict[str, object] | None, 

295) -> None: 

296 current_time = time.perf_counter() 

297 if current_time - state.last_log_time >= LOG_INTERVAL_SECONDS: 

298 metrics = ctx.perf_tracker.get_metrics() 

299 logger.info( 

300 "Camera: {:.1f} FPS | Inference: {:.1f}ms | Budget: {:.0f}% | Detections: {}", 

301 metrics.camera_fps, 

302 metrics.inference_ms, 

303 metrics.frame_budget_percent, 

304 len(detections), 

305 ) 

306 state.last_log_time = current_time 

307 

308 if ( 

309 ctx.args.debug_detections 

310 and current_time - state.last_debug_log_time >= DEBUG_LOG_INTERVAL_SECONDS 

311 ): 

312 sample = detections[:3] 

313 logger.info("Sample detections: {}", sample) 

314 if classification is not None: 

315 logger.info("Classification: {}", classification) 

316 state.last_debug_log_time = current_time 

317 

318 if current_time - state.last_resource_log_time >= RESOURCE_LOG_INTERVAL_SECONDS: 

319 metrics = ctx.perf_tracker.get_metrics() 

320 logger.info( 

321 "System CPU: {:.1f}% | RAM: {:.1f}/{:.1f}GB", 

322 state.sys_stats.cpu_percent, 

323 state.sys_stats.ram_used_gb, 

324 state.sys_stats.ram_total_gb, 

325 ) 

326 

327 if state.sys_stats.gpu_name != "N/A": 

328 logger.info( 

329 "GPU: {:.0f}% | VRAM: {:.1f}/{:.1f}GB | Temp: {:.0f}°C", 

330 state.sys_stats.gpu_percent, 

331 state.sys_stats.gpu_memory_used_gb, 

332 state.sys_stats.gpu_memory_total_gb, 

333 state.sys_stats.gpu_temp_celsius, 

334 ) 

335 

336 logger.info( 

337 "Process: CPU {:.1f}% | {:.0f}MB RAM", 

338 state.proc_stats["cpu_percent"], 

339 state.proc_stats["memory_mb"], 

340 ) 

341 

342 if state.power_info["system_power_watts"] > 0.0: 

343 logger.info( 

344 "Power (est): {:.0f}W | CPU {:.0f}W | GPU {:.0f}W | Energy {:.3f}Wh", 

345 state.power_info["system_power_watts"], 

346 state.power_info["cpu_power_watts"], 

347 state.power_info["gpu_power_watts"], 

348 state.power_info["energy_wh"], 

349 ) 

350 elif state.power_info["gpu_power_watts"] > 0.0: 

351 logger.info( 

352 "Power: GPU {:.0f}W | Energy {:.3f}Wh", 

353 state.power_info["gpu_power_watts"], 

354 state.power_info["energy_wh"], 

355 ) 

356 

357 headroom = 100 - metrics.frame_budget_percent 

358 potential = ( 

359 int(metrics.inference_capacity_fps / metrics.camera_fps) 

360 if metrics.camera_fps > 0 

361 else 0 

362 ) 

363 logger.info( 

364 "Performance: {:.0f}% budget | {:.0f}% headroom | ~{} streams", 

365 metrics.frame_budget_percent, 

366 headroom, 

367 potential, 

368 ) 

369 logger.info("-" * 60) 

370 state.last_resource_log_time = current_time 

371 

372 

373def _render_output( 

374 ctx: MonitorContext, 

375 state: RuntimeState, 

376 frame: np.ndarray, 

377 detections: list[dict[str, object]], 

378 classification: dict[str, object] | None, 

379) -> bool: 

380 if ctx.args.no_display: 

381 return True 

382 if ctx.viewer is not None: 

383 if not ctx.viewer.is_open(): 

384 logger.info("Quit requested by user") 

385 return False 

386 ctx.viewer.render( 

387 frame, 

388 perf_metrics=state.perf_metrics, 

389 sys_stats=state.sys_stats, 

390 proc_stats=state.proc_stats, 

391 camera_info=ctx.camera_info, 

392 detections_count=len(detections), 

393 classification=classification, 

394 log_lines=list(ctx.log_buffer), 

395 hardware_info=ctx.hardware_info, 

396 power_info=state.power_info, 

397 ) 

398 return True 

399 

400 cv2.imshow("YOLOv10 Detection", frame) 

401 if cv2.waitKey(1) & 0xFF == ord("q"): 

402 logger.info("Quit requested by user") 

403 return False 

404 return True 

405 

406 

407def _run_detection_loop(ctx: MonitorContext) -> int: 

408 state = _init_runtime_state() 

409 logger.info("-" * 60) 

410 logger.info("Starting detection loop. Press 'q' to quit.") 

411 logger.info("-" * 60) 

412 

413 try: 

414 while True: 

415 ret, frame = ctx.camera.read() 

416 if not ret or frame is None: 

417 logger.warning("Failed to grab frame") 

418 continue 

419 

420 ctx.perf_tracker.tick_camera() 

421 blob, scale, pad_x, pad_y = preprocess(frame, input_size=ctx.input_size) 

422 

423 inference_start = time.perf_counter() 

424 outputs = ctx.session.run(None, {ctx.input_name: blob}) 

425 inference_ms = (time.perf_counter() - inference_start) * 1000 

426 ctx.perf_tracker.add_inference_time(inference_ms) 

427 

428 detections, classification = postprocess( 

429 [np.asarray(output) for output in outputs], 

430 DecodeConfig( 

431 scale=scale, 

432 pad_x=pad_x, 

433 pad_y=pad_y, 

434 input_size=ctx.input_size, 

435 conf_threshold=ctx.args.conf, 

436 debug_boxes=ctx.args.debug_boxes, 

437 ), 

438 debug_output=ctx.args.debug_output and not state.output_debug_logged, 

439 ) 

440 if ctx.args.debug_output and not state.output_debug_logged: 

441 state.output_debug_logged = True 

442 

443 ctx.tracks = _update_tracks_if_enabled( 

444 ctx, 

445 frame, 

446 detections, 

447 ctx.tracks, 

448 ) 

449 

450 state.frame_count += 1 

451 _update_stats_if_needed(ctx, state) 

452 _update_cpu_history(ctx, state) 

453 

454 frame = _draw_frame_if_needed(ctx, state, frame, detections, classification) 

455 _log_periodic_metrics(ctx, state, detections, classification) 

456 

457 if not _render_output(ctx, state, frame, detections, classification): 

458 break 

459 

460 except KeyboardInterrupt: 

461 logger.info("Interrupted by user") 

462 except Exception as exc: 

463 logger.exception("Error during detection: {}", exc) 

464 finally: 

465 logger.info("=" * 60) 

466 logger.info("Session Summary") 

467 

468 final_metrics = ctx.perf_tracker.get_metrics() 

469 logger.info("Capture backend: {}", ctx.camera_info["backend"]) 

470 logger.info("Total frames: {}", state.frame_count) 

471 logger.info("Avg throughput: {:.1f} FPS", final_metrics.actual_throughput_fps) 

472 logger.info("Avg inference: {:.1f}ms", final_metrics.inference_ms) 

473 logger.info("Avg budget used: {:.1f}%", final_metrics.frame_budget_percent) 

474 

475 ctx.camera.release() 

476 if ctx.viewer is not None: 

477 ctx.viewer.close() 

478 cv2.destroyAllWindows() 

479 ctx.power_monitor.shutdown() 

480 ctx.sys_monitor.shutdown() 

481 logger.success("Cleanup complete. Goodbye!") 

482 

483 return 0 

484 

485 

486class MonitorInitError(RuntimeError): 

487 """Raised when monitor initialization fails.""" 

488 

489 

490def _init_viewer( 

491 args: argparse.Namespace, 

492 camera_info: dict[str, object], 

493 camera: CameraCapture, 

494 sys_monitor: SystemMonitor, 

495) -> ViewerProtocol | None: 

496 if args.ui not in {"dearpygui", "wxpython"} or args.no_display: 

497 return None 

498 

499 width, height = _extract_camera_dimensions(camera_info) 

500 

501 if args.ui == "dearpygui": 

502 try: 

503 viewer = DearPyGuiViewer( 

504 width=width, 

505 height=height, 

506 title="YOLO Monitor", 

507 ) 

508 except Exception as exc: 

509 logger.error("Failed to initialize DearPyGui viewer: {}", exc) 

510 camera.release() 

511 sys_monitor.shutdown() 

512 message = "DearPyGui viewer initialization failed" 

513 raise MonitorInitError(message) from exc 

514 else: 

515 logger.success("DearPyGui viewer initialized") 

516 return viewer 

517 

518 if WxPythonViewer is None: 

519 logger.error("wxPython viewer requested but not available") 

520 if _WX_VIEWER_IMPORT_ERROR is not None: 

521 logger.error("wxPython import error: {}", _WX_VIEWER_IMPORT_ERROR) 

522 camera.release() 

523 sys_monitor.shutdown() 

524 message = "wxPython viewer not available" 

525 raise MonitorInitError(message) 

526 

527 try: 

528 viewer = WxPythonViewer( 

529 width=width, 

530 height=height, 

531 title="YOLO Monitor", 

532 ) 

533 except Exception as exc: 

534 logger.error("Failed to initialize wxPython viewer: {}", exc) 

535 camera.release() 

536 sys_monitor.shutdown() 

537 message = "wxPython viewer initialization failed" 

538 raise MonitorInitError(message) from exc 

539 else: 

540 logger.success("wxPython viewer initialized") 

541 return viewer 

542 

543 

544def _log_platform_info(initial_stats: SystemStats) -> None: 

545 logger.info("System RAM: {:.1f} GB", initial_stats.ram_total_gb) 

546 logger.info("CPU model: {}", _get_cpu_model()) 

547 try: 

548 cpu_freq = psutil.cpu_freq() 

549 if cpu_freq and cpu_freq.max: 

550 logger.info( 

551 "CPU freq: {:.0f} MHz (max {:.0f} MHz)", 

552 cpu_freq.current or 0.0, 

553 cpu_freq.max, 

554 ) 

555 except Exception as exc: 

556 logger.debug("Unable to read CPU frequency: {}", exc) 

557 logger.info( 

558 "CPU cores: {} physical, {} logical", 

559 psutil.cpu_count(logical=False), 

560 psutil.cpu_count(), 

561 ) 

562 if initial_stats.gpu_name != "N/A": 

563 logger.info( 

564 "GPU: {} ({:.1f} GB VRAM)", 

565 initial_stats.gpu_name, 

566 initial_stats.gpu_memory_total_gb, 

567 ) 

568 

569 

570def _build_context(args: argparse.Namespace, log_buffer: deque[str]) -> MonitorContext: 

571 logger.info("=" * 60) 

572 logger.info("YOLOv10 Object Detection with System Monitoring") 

573 logger.info("=" * 60) 

574 

575 logger.info("Platform: {} {}", platform.system(), platform.release()) 

576 logger.info("Python: {}", platform.python_version()) 

577 logger.info("OpenCV: {}", cv2.__version__) 

578 

579 gst_path, gst_status = find_gstreamer_launch() 

580 if gst_path: 

581 logger.info("GStreamer: {}", gst_status) 

582 else: 

583 logger.warning("GStreamer: {}", gst_status) 

584 

585 if args.backend == "gstreamer" and gst_path is None: 

586 logger.error("GStreamer backend requested but not found!") 

587 logger.error( 

588 "Install GStreamer from: https://gstreamer.freedesktop.org/download/" 

589 ) 

590 message = "GStreamer backend requested but not found" 

591 raise MonitorInitError(message) 

592 

593 camera_config = CameraConfig( 

594 device_index=args.camera, 

595 width=args.width, 

596 height=args.height, 

597 fps=args.fps, 

598 backend=CaptureBackend(args.backend), 

599 ) 

600 

601 logger.info("Requested: {}x{} @ {} FPS", args.width, args.height, args.fps) 

602 logger.info("Backend: {}", args.backend) 

603 logger.info("UI: {}", args.ui) 

604 

605 sys_monitor = SystemMonitor(gpu_device_id=args.gpu) 

606 initial_stats = sys_monitor.get_stats() 

607 _log_platform_info(initial_stats) 

608 

609 providers = [ 

610 ( 

611 "CUDAExecutionProvider", 

612 {"device_id": args.gpu, "arena_extend_strategy": "kNextPowerOfTwo"}, 

613 ), 

614 "CPUExecutionProvider", 

615 ] 

616 

617 logger.info("Loading model: {}", args.model) 

618 try: 

619 session = ort.InferenceSession(args.model, providers=providers) 

620 except Exception as exc: 

621 logger.error("Failed to load model: {}", exc) 

622 sys_monitor.shutdown() 

623 message = "Failed to load model" 

624 raise MonitorInitError(message) from exc 

625 

626 active_provider = session.get_providers()[0] 

627 logger.success("Model loaded using: {}", active_provider) 

628 

629 input_name = session.get_inputs()[0].name 

630 input_shape = session.get_inputs()[0].shape 

631 logger.debug("Model input: {}, shape: {}", input_name, input_shape) 

632 input_size = infer_input_size(input_shape) 

633 

634 logger.info("-" * 60) 

635 camera = CameraCapture(camera_config) 

636 if not camera.open(): 

637 logger.error("Failed to open camera!") 

638 sys_monitor.shutdown() 

639 message = "Failed to open camera" 

640 raise MonitorInitError(message) 

641 

642 camera_info = camera.get_info() 

643 logger.info("Active backend: {}", camera_info["backend"]) 

644 logger.info("Capture pipeline: {}", camera_info.get("pipeline", "")) 

645 

646 hardware_info = { 

647 "cpu_model": _get_cpu_model(), 

648 "ram_total_gb": initial_stats.ram_total_gb, 

649 "gpu_model": initial_stats.gpu_name, 

650 "vram_total_gb": initial_stats.gpu_memory_total_gb, 

651 } 

652 

653 perf_tracker = PerformanceTracker(avg_frames=30) 

654 tracker = SimpleCentroidTracker() 

655 tracks: dict[int, Track] = {} 

656 cpu_history: deque[float] = deque(maxlen=max(2, int(args.cpu_history))) 

657 

658 cpu_tdp_watts = float( 

659 os.getenv("KATAGLYPHIS_CPU_TDP_WATTS", str(DEFAULT_CPU_TDP_WATTS)) 

660 or DEFAULT_CPU_TDP_WATTS 

661 ) 

662 logger.info("CPU power baseline (TDP): {:.0f} W", cpu_tdp_watts) 

663 

664 power_monitor = PowerMonitor() 

665 viewer = _init_viewer(args, camera_info, camera, sys_monitor) 

666 

667 return MonitorContext( 

668 args=args, 

669 camera=camera, 

670 camera_info=camera_info, 

671 sys_monitor=sys_monitor, 

672 session=session, 

673 input_name=input_name, 

674 input_size=input_size, 

675 perf_tracker=perf_tracker, 

676 tracker=tracker, 

677 tracks=tracks, 

678 cpu_history=cpu_history, 

679 log_buffer=log_buffer, 

680 power_monitor=power_monitor, 

681 hardware_info=hardware_info, 

682 viewer=viewer, 

683 cpu_tdp_watts=cpu_tdp_watts, 

684 ) 

685 

686 

687def run_yolo_monitor(argv: list[str] | None = None) -> int: 

688 """Entry point for running the YOLOv10 monitor.""" 

689 args = parse_args(argv) 

690 configure_logging(args.log_level) 

691 log_buffer = create_log_buffer(max_lines=200) 

692 attach_log_buffer(log_buffer, level="INFO") 

693 try: 

694 ctx = _build_context(args, log_buffer) 

695 except MonitorInitError: 

696 return 1 

697 

698 use_wx = args.ui == "wxpython" and not args.no_display and ctx.viewer is not None 

699 if use_wx: 

700 exit_code = 0 

701 wx_viewer = cast("ViewerLoopProtocol", ctx.viewer) 

702 

703 def _run_loop() -> None: 

704 nonlocal exit_code 

705 exit_code = _run_detection_loop(ctx) 

706 

707 worker = threading.Thread( 

708 target=_run_loop, 

709 name="yolo-detection", 

710 daemon=True, 

711 ) 

712 worker.start() 

713 wx_viewer.run() 

714 worker.join() 

715 return exit_code 

716 

717 return _run_detection_loop(ctx) 

718 

719 

720if __name__ == "__main__": 

721 raise SystemExit(run_yolo_monitor())