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
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-19 08:36 +0000
1"""DearPyGui viewer for monitoring pipelines."""
3from __future__ import annotations
5from collections import deque
6from typing import TYPE_CHECKING, Any
8import cv2
9import numpy as np
11from orchestr_ant_ion.pipeline.ui.viewmodel import format_labels
14dpg: Any | None
15_DPG_IMPORT_ERROR: ImportError | None = None
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
26if TYPE_CHECKING:
27 from orchestr_ant_ion.pipeline.types import (
28 PerformanceMetrics,
29 SystemStats,
30 )
33class DearPyGuiViewer:
34 """Minimal DearPyGui viewer for rendering frames as a texture."""
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
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"
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()
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 }
85 self._det_tags = {
86 "detections": "det_count",
87 "class": "det_class",
88 }
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 }
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 }
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)
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 )
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)
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()
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 )
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()
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 )
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 )
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"])
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 )
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 )
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
371 def is_open(self) -> bool:
372 """Return True if the DearPyGui window is still open."""
373 return self.dpg.is_dearpygui_running()
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 )
389 self.dpg.set_value(self._texture_tag, frame_rgb.ravel())
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 )
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 )
445 if perf_metrics is not None and sys_stats is not None:
446 self._update_plots(perf_metrics, sys_stats)
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)
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"])
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 )
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))
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])
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)
549 self.dpg.render_dearpygui_frame()
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()