Coverage for orchestr_ant_ion / pipeline / ui / dearpygui.py: 17%

213 statements  

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

1"""DearPyGui viewer for monitoring pipelines.""" 

2 

3from __future__ import annotations 

4 

5from collections import deque 

6from typing import TYPE_CHECKING, Any 

7 

8import cv2 

9import numpy as np 

10 

11from orchestr_ant_ion.pipeline.ui.viewmodel import format_labels 

12 

13 

14dpg: Any | None 

15_DPG_IMPORT_ERROR: ImportError | None = None 

16 

17try: 

18 import dearpygui.dearpygui as _dpg 

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

20 dpg = None 

21 _DPG_IMPORT_ERROR = exc 

22else: 

23 dpg = _dpg 

24 _DPG_IMPORT_ERROR = None 

25 

26if TYPE_CHECKING: 

27 from orchestr_ant_ion.pipeline.types import ( 

28 PerformanceMetrics, 

29 SystemStats, 

30 ) 

31 

32 

33class DearPyGuiViewer: 

34 """Minimal DearPyGui viewer for rendering frames as a texture.""" 

35 

36 def __init__(self, width: int, height: int, title: str = "YOLO Monitor") -> None: 

37 """Initialize the DearPyGui viewer UI.""" 

38 if dpg is None: 

39 message = "dearpygui is required for DearPyGuiViewer" 

40 raise ImportError(message) from _DPG_IMPORT_ERROR 

41 

42 self.dpg = dpg 

43 self.width = int(width) 

44 self.height = int(height) 

45 self._frame_size = (self.width, self.height) 

46 self._texture_registry_tag = "frame_texture_registry" 

47 self._texture_tag = "frame_texture" 

48 self._image_tag = "frame_image" 

49 

50 self._initialize_tags() 

51 self._initialize_plot_state() 

52 self.dpg.create_context() 

53 self._initialize_theme() 

54 self._create_viewport(title) 

55 self._create_texture_registry() 

56 self._create_layout() 

57 self.dpg.setup_dearpygui() 

58 self.dpg.show_viewport() 

59 

60 def _initialize_tags(self) -> None: 

61 self._perf_tags = { 

62 "title": "perf_title", 

63 "resolution": "perf_resolution", 

64 "capture": "perf_capture", 

65 "backend": "perf_backend", 

66 "pipeline": "perf_pipeline", 

67 "cpu_model": "perf_cpu_model", 

68 "ram_total": "perf_ram_total", 

69 "gpu_model": "perf_gpu_model", 

70 "vram_total": "perf_vram_total", 

71 "detections": "perf_detections", 

72 "camera_fps": "perf_camera_fps", 

73 "inference_ms": "perf_inference_ms", 

74 "budget": "perf_budget", 

75 "headroom": "perf_headroom", 

76 "sys_cpu": "perf_sys_cpu", 

77 "sys_ram": "perf_sys_ram", 

78 "gpu": "perf_gpu", 

79 "vram": "perf_vram", 

80 "power": "perf_power", 

81 "energy": "perf_energy", 

82 "proc": "perf_proc", 

83 } 

84 

85 self._det_tags = { 

86 "detections": "det_count", 

87 "class": "det_class", 

88 } 

89 

90 self._log_tag = "log_output" 

91 self._plot_tags = { 

92 "perf_plot": "perf_plot", 

93 "perf_camera_fps": "series_camera_fps", 

94 "perf_inference_ms": "series_inference_ms", 

95 "perf_x": "perf_x_axis", 

96 "perf_y": "perf_y_axis", 

97 "sys_plot": "sys_plot", 

98 "sys_cpu": "series_sys_cpu", 

99 "sys_ram": "series_sys_ram", 

100 "sys_gpu": "series_sys_gpu", 

101 "sys_x": "sys_x_axis", 

102 "sys_y": "sys_y_axis", 

103 } 

104 

105 def _initialize_plot_state(self) -> None: 

106 self._plot_history_len = 120 

107 self._plot_index = 0 

108 self._plot_data = { 

109 "x": deque(maxlen=self._plot_history_len), 

110 "camera_fps": deque(maxlen=self._plot_history_len), 

111 "inference_ms": deque(maxlen=self._plot_history_len), 

112 "sys_cpu": deque(maxlen=self._plot_history_len), 

113 "sys_ram": deque(maxlen=self._plot_history_len), 

114 "sys_gpu": deque(maxlen=self._plot_history_len), 

115 } 

116 

117 def _initialize_theme(self) -> None: 

118 self._colors = { 

119 "bg": (17, 24, 39, 255), 

120 "panel": (31, 41, 55, 255), 

121 "panel_alt": (24, 32, 45, 255), 

122 "border": (55, 65, 81, 255), 

123 "text": (229, 231, 235, 255), 

124 "muted": (156, 163, 175, 255), 

125 "accent": (34, 211, 238, 255), 

126 "accent_soft": (14, 116, 144, 255), 

127 } 

128 self._theme = self._create_theme() 

129 self.dpg.bind_theme(self._theme) 

130 

131 def _create_viewport(self, title: str) -> None: 

132 self.dpg.create_viewport( 

133 title=title, 

134 width=self.width + 360, 

135 height=self.height + 120, 

136 ) 

137 

138 def _create_texture_registry(self) -> None: 

139 with self.dpg.texture_registry(show=False, tag=self._texture_registry_tag): 

140 self._create_texture(self.width, self.height) 

141 

142 def _create_layout(self) -> None: 

143 self._create_video_window() 

144 self._create_perf_window() 

145 self._create_detection_window() 

146 self._create_log_window() 

147 

148 def _create_video_window(self) -> None: 

149 with self.dpg.window( 

150 label="Video", 

151 tag="video_window", 

152 pos=(10, 10), 

153 width=self.width + 20, 

154 height=self.height + 20, 

155 no_move=True, 

156 no_resize=True, 

157 ): 

158 self.dpg.add_image( 

159 self._texture_tag, 

160 tag=self._image_tag, 

161 width=self.width, 

162 height=self.height, 

163 ) 

164 

165 def _create_perf_window(self) -> None: 

166 with self.dpg.window( 

167 label="System & Performance", 

168 tag="perf_window", 

169 pos=(self.width + 30, 10), 

170 width=320, 

171 height=400, 

172 ): 

173 self.dpg.add_text( 

174 "YOLO Monitor", 

175 tag=self._perf_tags["title"], 

176 color=self._colors["accent"], 

177 ) 

178 with self.dpg.child_window(border=True, autosize_x=True): 

179 self.dpg.add_text("Overview", color=self._colors["muted"]) 

180 self.dpg.add_separator() 

181 self.dpg.add_text("", tag=self._perf_tags["resolution"]) 

182 self.dpg.add_text("", tag=self._perf_tags["capture"]) 

183 self.dpg.add_text("", tag=self._perf_tags["backend"]) 

184 self.dpg.add_text("", tag=self._perf_tags["pipeline"]) 

185 self.dpg.add_text("", tag=self._perf_tags["cpu_model"]) 

186 self.dpg.add_text("", tag=self._perf_tags["ram_total"]) 

187 self.dpg.add_text("", tag=self._perf_tags["gpu_model"]) 

188 self.dpg.add_text("", tag=self._perf_tags["vram_total"]) 

189 self.dpg.add_text("", tag=self._perf_tags["detections"]) 

190 with self.dpg.child_window(border=True, autosize_x=True): 

191 self.dpg.add_text("Performance", color=self._colors["muted"]) 

192 self.dpg.add_separator() 

193 self.dpg.add_text("", tag=self._perf_tags["camera_fps"]) 

194 self.dpg.add_text("", tag=self._perf_tags["inference_ms"]) 

195 self.dpg.add_text("", tag=self._perf_tags["budget"]) 

196 self.dpg.add_text("", tag=self._perf_tags["headroom"]) 

197 with self.dpg.child_window(border=True, autosize_x=True): 

198 self.dpg.add_text("System", color=self._colors["muted"]) 

199 self.dpg.add_separator() 

200 self.dpg.add_text("", tag=self._perf_tags["sys_cpu"]) 

201 self.dpg.add_text("", tag=self._perf_tags["sys_ram"]) 

202 self.dpg.add_text("", tag=self._perf_tags["gpu"]) 

203 self.dpg.add_text("", tag=self._perf_tags["vram"]) 

204 self.dpg.add_text("", tag=self._perf_tags["power"]) 

205 self.dpg.add_text("", tag=self._perf_tags["energy"]) 

206 with self.dpg.child_window(border=True, autosize_x=True): 

207 self.dpg.add_text("Process", color=self._colors["muted"]) 

208 self.dpg.add_separator() 

209 self.dpg.add_text("", tag=self._perf_tags["proc"]) 

210 with self.dpg.child_window(border=True, autosize_x=True, height=420): 

211 self.dpg.add_text("Trends", color=self._colors["muted"]) 

212 self.dpg.add_separator() 

213 self._create_perf_plot() 

214 self._create_system_plot() 

215 

216 def _create_perf_plot(self) -> None: 

217 with self.dpg.plot( 

218 label="Performance", 

219 height=180, 

220 width=-1, 

221 tag=self._plot_tags["perf_plot"], 

222 ): 

223 self.dpg.add_plot_legend() 

224 self.dpg.add_plot_axis( 

225 self.dpg.mvXAxis, 

226 label="Frames", 

227 tag=self._plot_tags["perf_x"], 

228 ) 

229 y_axis = self.dpg.add_plot_axis( 

230 self.dpg.mvYAxis, 

231 label="Value", 

232 tag=self._plot_tags["perf_y"], 

233 ) 

234 self.dpg.add_line_series( 

235 [], 

236 [], 

237 label="Camera FPS", 

238 parent=y_axis, 

239 tag=self._plot_tags["perf_camera_fps"], 

240 ) 

241 self.dpg.add_line_series( 

242 [], 

243 [], 

244 label="Inference ms", 

245 parent=y_axis, 

246 tag=self._plot_tags["perf_inference_ms"], 

247 ) 

248 

249 def _create_system_plot(self) -> None: 

250 with self.dpg.plot( 

251 label="System", 

252 height=180, 

253 width=-1, 

254 tag=self._plot_tags["sys_plot"], 

255 ): 

256 self.dpg.add_plot_legend() 

257 self.dpg.add_plot_axis( 

258 self.dpg.mvXAxis, 

259 label="Frames", 

260 tag=self._plot_tags["sys_x"], 

261 ) 

262 y_axis = self.dpg.add_plot_axis( 

263 self.dpg.mvYAxis, 

264 label="Percent", 

265 tag=self._plot_tags["sys_y"], 

266 ) 

267 self.dpg.add_line_series( 

268 [], 

269 [], 

270 label="CPU %", 

271 parent=y_axis, 

272 tag=self._plot_tags["sys_cpu"], 

273 ) 

274 self.dpg.add_line_series( 

275 [], 

276 [], 

277 label="RAM %", 

278 parent=y_axis, 

279 tag=self._plot_tags["sys_ram"], 

280 ) 

281 self.dpg.add_line_series( 

282 [], 

283 [], 

284 label="GPU %", 

285 parent=y_axis, 

286 tag=self._plot_tags["sys_gpu"], 

287 ) 

288 

289 def _create_detection_window(self) -> None: 

290 with self.dpg.window( 

291 label="Detections", 

292 tag="det_window", 

293 pos=(self.width + 30, 420), 

294 width=320, 

295 height=180, 

296 ): 

297 self.dpg.add_text("Current", color=self._colors["muted"]) 

298 self.dpg.add_separator() 

299 self.dpg.add_text("", tag=self._det_tags["detections"]) 

300 self.dpg.add_text("", tag=self._det_tags["class"]) 

301 

302 def _create_log_window(self) -> None: 

303 with self.dpg.window( 

304 label="Logs", 

305 tag="log_window", 

306 pos=(self.width + 30, 610), 

307 width=320, 

308 height=260, 

309 ): 

310 self.dpg.add_text("Recent logs", color=self._colors["muted"]) 

311 self.dpg.add_separator() 

312 self.dpg.add_input_text( 

313 tag=self._log_tag, 

314 multiline=True, 

315 readonly=True, 

316 width=-1, 

317 height=-1, 

318 ) 

319 

320 def _create_texture(self, width: int, height: int) -> None: 

321 """Create or recreate the DearPyGui texture.""" 

322 if self.dpg.does_item_exist(self._texture_tag): 

323 self.dpg.delete_item(self._texture_tag) 

324 frame_data = np.zeros((height, width, 3), dtype=np.float32).ravel() 

325 self.dpg.add_raw_texture( 

326 width, 

327 height, 

328 frame_data, 

329 format=self.dpg.mvFormat_Float_rgb, 

330 tag=self._texture_tag, 

331 parent=self._texture_registry_tag, 

332 ) 

333 

334 def _create_theme(self) -> int: 

335 """Create and return a consistent dark theme.""" 

336 with self.dpg.theme() as theme, self.dpg.theme_component(self.dpg.mvAll): 

337 self.dpg.add_theme_color(self.dpg.mvThemeCol_WindowBg, self._colors["bg"]) 

338 self.dpg.add_theme_color(self.dpg.mvThemeCol_ChildBg, self._colors["panel"]) 

339 self.dpg.add_theme_color(self.dpg.mvThemeCol_Border, self._colors["border"]) 

340 self.dpg.add_theme_color(self.dpg.mvThemeCol_Text, self._colors["text"]) 

341 self.dpg.add_theme_color( 

342 self.dpg.mvThemeCol_FrameBg, self._colors["panel_alt"] 

343 ) 

344 self.dpg.add_theme_color( 

345 self.dpg.mvThemeCol_FrameBgHovered, self._colors["panel"] 

346 ) 

347 self.dpg.add_theme_color( 

348 self.dpg.mvThemeCol_FrameBgActive, self._colors["panel"] 

349 ) 

350 self.dpg.add_theme_color(self.dpg.mvThemeCol_TitleBg, self._colors["panel"]) 

351 self.dpg.add_theme_color( 

352 self.dpg.mvThemeCol_TitleBgActive, self._colors["panel"] 

353 ) 

354 self.dpg.add_theme_color( 

355 self.dpg.mvThemeCol_Header, self._colors["panel_alt"] 

356 ) 

357 self.dpg.add_theme_color( 

358 self.dpg.mvThemeCol_HeaderHovered, self._colors["panel"] 

359 ) 

360 self.dpg.add_theme_color( 

361 self.dpg.mvThemeCol_HeaderActive, self._colors["panel"] 

362 ) 

363 self.dpg.add_theme_style(self.dpg.mvStyleVar_WindowRounding, 6) 

364 self.dpg.add_theme_style(self.dpg.mvStyleVar_ChildRounding, 6) 

365 self.dpg.add_theme_style(self.dpg.mvStyleVar_FrameRounding, 6) 

366 self.dpg.add_theme_style(self.dpg.mvStyleVar_WindowPadding, 10, 10) 

367 self.dpg.add_theme_style(self.dpg.mvStyleVar_ItemSpacing, 8, 6) 

368 self.dpg.add_theme_style(self.dpg.mvStyleVar_FramePadding, 8, 6) 

369 return theme 

370 

371 def is_open(self) -> bool: 

372 """Return True if the DearPyGui window is still open.""" 

373 return self.dpg.is_dearpygui_running() 

374 

375 def _update_frame_texture(self, frame: np.ndarray) -> None: 

376 frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) 

377 frame_rgb = frame_rgb.astype(np.float32) / 255.0 

378 h, w = frame_rgb.shape[:2] 

379 if (w, h) != self._frame_size: 

380 self._frame_size = (w, h) 

381 self._create_texture(w, h) 

382 self.dpg.configure_item(self._image_tag, width=w, height=h) 

383 self.dpg.configure_item( 

384 "video_window", 

385 width=w + 20, 

386 height=h + 20, 

387 ) 

388 

389 self.dpg.set_value(self._texture_tag, frame_rgb.ravel()) 

390 

391 def _update_perf_panel( 

392 self, 

393 *, 

394 perf_metrics: PerformanceMetrics | None, 

395 sys_stats: SystemStats | None, 

396 proc_stats: dict | None, 

397 camera_info: dict | None, 

398 detections_count: int | None, 

399 hardware_info: dict | None, 

400 power_info: dict | None, 

401 classification: dict | None, 

402 frame_size: tuple[int, int], 

403 ) -> None: 

404 labels = format_labels( 

405 perf_metrics=perf_metrics, 

406 sys_stats=sys_stats, 

407 proc_stats=proc_stats, 

408 camera_info=camera_info, 

409 detections_count=detections_count, 

410 hardware_info=hardware_info, 

411 power_info=power_info, 

412 classification=classification, 

413 frame_size=frame_size, 

414 ) 

415 

416 # Map ViewLabels fields to perf tags (most share the same key name) 

417 tag_map = { 

418 "resolution": "resolution", 

419 "capture": "capture", 

420 "backend": "backend", 

421 "pipeline": "pipeline", 

422 "cpu_model": "cpu_model", 

423 "ram_total": "ram_total", 

424 "gpu_model": "gpu_model", 

425 "vram_total": "vram_total", 

426 "detections": "detections", 

427 "camera_fps": "camera_fps", 

428 "inference_ms": "inference_ms", 

429 "budget": "budget", 

430 "headroom": "headroom", 

431 "sys_cpu": "sys_cpu", 

432 "sys_ram": "sys_ram", 

433 "gpu": "gpu", 

434 "vram": "vram", 

435 "power": "power", 

436 "energy": "energy", 

437 "proc": "proc", 

438 } 

439 for label_field, tag_key in tag_map.items(): 

440 if tag_key in self._perf_tags: 

441 self.dpg.set_value( 

442 self._perf_tags[tag_key], getattr(labels, label_field) 

443 ) 

444 

445 if perf_metrics is not None and sys_stats is not None: 

446 self._update_plots(perf_metrics, sys_stats) 

447 

448 def _update_plots( 

449 self, 

450 perf_metrics: PerformanceMetrics, 

451 sys_stats: SystemStats, 

452 ) -> None: 

453 self._plot_index += 1 

454 self._plot_data["x"].append(self._plot_index) 

455 self._plot_data["camera_fps"].append(float(perf_metrics.camera_fps)) 

456 self._plot_data["inference_ms"].append(float(perf_metrics.inference_ms)) 

457 self._plot_data["sys_cpu"].append(float(sys_stats.cpu_percent)) 

458 self._plot_data["sys_ram"].append(float(sys_stats.ram_percent)) 

459 if sys_stats.gpu_name != "N/A": 

460 self._plot_data["sys_gpu"].append(float(sys_stats.gpu_percent)) 

461 else: 

462 self._plot_data["sys_gpu"].append(0.0) 

463 

464 x_values = list(self._plot_data["x"]) 

465 self.dpg.set_value( 

466 self._plot_tags["perf_camera_fps"], 

467 [x_values, list(self._plot_data["camera_fps"])], 

468 ) 

469 self.dpg.set_value( 

470 self._plot_tags["perf_inference_ms"], 

471 [x_values, list(self._plot_data["inference_ms"])], 

472 ) 

473 self.dpg.set_value( 

474 self._plot_tags["sys_cpu"], 

475 [x_values, list(self._plot_data["sys_cpu"])], 

476 ) 

477 self.dpg.set_value( 

478 self._plot_tags["sys_ram"], 

479 [x_values, list(self._plot_data["sys_ram"])], 

480 ) 

481 self.dpg.set_value( 

482 self._plot_tags["sys_gpu"], 

483 [x_values, list(self._plot_data["sys_gpu"])], 

484 ) 

485 self.dpg.fit_axis_data(self._plot_tags["perf_x"]) 

486 self.dpg.fit_axis_data(self._plot_tags["perf_y"]) 

487 self.dpg.fit_axis_data(self._plot_tags["sys_x"]) 

488 self.dpg.fit_axis_data(self._plot_tags["sys_y"]) 

489 

490 def _update_detection_panel( 

491 self, 

492 *, 

493 detections_count: int | None, 

494 classification: dict | None, 

495 ) -> None: 

496 if detections_count is not None: 

497 self.dpg.set_value( 

498 self._det_tags["detections"], 

499 f"Detections: {detections_count}", 

500 ) 

501 if classification is not None: 

502 class_label = classification.get("label", "unknown") 

503 class_score = classification.get("score", 0.0) 

504 self.dpg.set_value( 

505 self._det_tags["class"], 

506 f"Class: {class_label} ({class_score:.2f})", 

507 ) 

508 

509 def _update_log_panel(self, *, log_lines: list[str] | None) -> None: 

510 if log_lines is not None: 

511 self.dpg.set_value(self._log_tag, "\n".join(log_lines)) 

512 

513 def render( 

514 self, 

515 frame: np.ndarray, 

516 perf_metrics: PerformanceMetrics | None = None, 

517 sys_stats: SystemStats | None = None, 

518 proc_stats: dict | None = None, 

519 camera_info: dict | None = None, 

520 detections_count: int | None = None, 

521 classification: dict | None = None, 

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

523 hardware_info: dict | None = None, 

524 power_info: dict | None = None, 

525 ) -> None: 

526 """Render a frame and update UI panels.""" 

527 if not self.dpg.is_dearpygui_running(): 

528 return 

529 self._update_frame_texture(frame) 

530 frame_size = (self._frame_size[0], self._frame_size[1]) 

531 

532 self._update_perf_panel( 

533 perf_metrics=perf_metrics, 

534 sys_stats=sys_stats, 

535 proc_stats=proc_stats, 

536 camera_info=camera_info, 

537 detections_count=detections_count, 

538 hardware_info=hardware_info, 

539 power_info=power_info, 

540 classification=classification, 

541 frame_size=frame_size, 

542 ) 

543 self._update_detection_panel( 

544 detections_count=detections_count, 

545 classification=classification, 

546 ) 

547 self._update_log_panel(log_lines=log_lines) 

548 

549 self.dpg.render_dearpygui_frame() 

550 

551 def close(self) -> None: 

552 """Close the viewer and destroy the DearPyGui context.""" 

553 if self.dpg.is_dearpygui_running(): 

554 self.dpg.destroy_context()