import time from dataclasses import dataclass from pathlib import Path import cv2 import numpy as np import flywms_navigation as nav try: import dearpygui.dearpygui as dpg except ModuleNotFoundError as exc: dpg = None DEARPYGUI_IMPORT_ERROR = exc else: DEARPYGUI_IMPORT_ERROR = None TEXTURE_MAIN = "texture_main" TEXTURE_SNAPSHOT = "texture_snapshot" TEXTURE_REGISTRY = "texture_registry" IMAGE_MAIN = "image_main" IMAGE_SNAPSHOT = "image_snapshot" TEXT_STATUS = "text_status" TEXT_COMMANDS = "text_commands" TEXT_TRACKS = "text_tracks" TEXT_METRICS = "text_metrics" BUTTON_RUN = "button_run" @dataclass(frozen=True) class DemoFrameState: frame_id: int elapsed: float source_frame: np.ndarray display_frame: np.ndarray snapshot_frame: np.ndarray | None tracks: list[nav.Track] detections_count: int snapshots: list[nav.NavigationSnapshot] yolo_ms: float fps_text: str class NavigationDemoEngine: """One-step navigation simulator used by the DearPyGUI shell. The original flywms_navigation.py command-line program stays untouched. This class reuses its detector/tracker/navigation objects and exposes a frame-by-frame API suitable for a GUI event loop. """ def __init__(self, args): self.args = args nav.require_file(args.weights, "modello Ultralytics") self.detector = nav.UltralyticsDetector(args.weights, args.ultralytics_device) self.cap, self.source_name = nav.open_capture(args.video) if not self.cap.isOpened(): raise RuntimeError(f"impossibile aprire sorgente video: {self.source_name}") video_fps = self.cap.get(cv2.CAP_PROP_FPS) self.frame_delay = ( 1.0 / video_fps if args.realtime_playback and video_fps and video_fps > 1 else 0.0 ) self.tracker = nav.LightweightTracker( max_missed=args.max_track_missed, min_match_score=args.min_match_score, max_center_distance_ratio=args.max_center_distance_ratio, ) self.navigator = nav.NavigationController(args) self.frame_id = 0 self.start_time = time.perf_counter() self.last_loop_end = self.start_time self.yolo_total_ms = 0.0 self.yolo_cycles = 0 self.stop_reason = "" def close(self) -> None: self.cap.release() def step(self) -> DemoFrameState | None: if self.frame_delay > 0: now = time.perf_counter() sleep_for = self.frame_delay - (now - self.last_loop_end) if sleep_for > 0: time.sleep(sleep_for) self.last_loop_end = time.perf_counter() ok, frame = self.cap.read() if not ok: self.stop_reason = "Fine stream" return None self.frame_id += 1 timestamp = time.perf_counter() if self.args.max_frames > 0 and self.frame_id > self.args.max_frames: self.stop_reason = f"Raggiunto max_frames={self.args.max_frames}" return None detections, yolo_ms = self.detector.detect( frame, self.args.min_confidence, self.args.input_size, ) self.yolo_total_ms += yolo_ms self.yolo_cycles += 1 gaylords = [ det for det in detections if det.class_name.strip().lower() == self.args.target_class.strip().lower() ] tracks = self.tracker.update(gaylords, self.frame_id, frame.shape[1]) if ( self.args.motion_report_interval > 0 and self.frame_id % self.args.motion_report_interval == 0 ): self.navigator.set_motion_text( nav.estimate_motion_from_tracks(tracks, self.args.motion_min_pixels) ) new_snapshots: list[nav.NavigationSnapshot] = [] for track in tracks: if track.missed == 0: snapshot = self.navigator.process_track( track, frame, self.frame_id, timestamp, ) if snapshot is not None: new_snapshots.append(snapshot) for snapshot in new_snapshots: self.navigator.simulate_remote_response(snapshot) elapsed = max(time.perf_counter() - self.start_time, 0.001) fps_text = ( f"frame={self.frame_id} fps={self.frame_id / elapsed:.1f} " f"det={len(gaylords)} tracks={len(tracks)} " f"snap={self.navigator.snapshot_counter}" ) display = nav.draw_navigation_debug( frame, tracks, self.args, self.navigator.last_command_text, fps_text, ) return DemoFrameState( frame_id=self.frame_id, elapsed=elapsed, source_frame=frame, display_frame=display, snapshot_frame=self.navigator.last_ocr_payload_frame, tracks=tracks, detections_count=len(gaylords), snapshots=new_snapshots, yolo_ms=yolo_ms, fps_text=fps_text, ) class NavigationDemoGui: def __init__(self, args): self.args = args self.engine: NavigationDemoEngine | None = None self.running = False self.main_texture_size: tuple[int, int] | None = None self.snapshot_texture_size: tuple[int, int] | None = None self.main_texture_tag = TEXTURE_MAIN self.snapshot_texture_tag = TEXTURE_SNAPSHOT self.last_state: DemoFrameState | None = None def start(self) -> None: if self.engine is None: self.engine = NavigationDemoEngine(self.args) dpg.set_value(TEXT_STATUS, f"Sorgente: {self.engine.source_name}") self.running = True dpg.configure_item(BUTTON_RUN, label="Pausa") def pause(self) -> None: self.running = False dpg.configure_item(BUTTON_RUN, label="Avvia") def toggle_run(self) -> None: if self.running: self.pause() else: self.start() def reset(self) -> None: self.pause() if self.engine is not None: self.engine.close() self.engine = NavigationDemoEngine(self.args) self.last_state = None self.main_texture_size = None self.snapshot_texture_size = None dpg.set_value(TEXT_COMMANDS, "Nessun comando generato") dpg.set_value(TEXT_TRACKS, "Nessuna track") dpg.set_value(TEXT_METRICS, "Metriche non disponibili") dpg.set_value(TEXT_STATUS, f"Reset completato. Sorgente: {self.engine.source_name}") def step_once(self) -> None: if self.engine is None: self.engine = NavigationDemoEngine(self.args) state = self.engine.step() if state is None: self.pause() dpg.set_value(TEXT_STATUS, self.engine.stop_reason if self.engine else "Stop") return self.last_state = state self._update_ui(state) def tick(self) -> None: if self.running: self.step_once() def close(self) -> None: if self.engine is not None: self.engine.close() def _update_ui(self, state: DemoFrameState) -> None: self._set_image_texture( frame=state.display_frame, image_tag=IMAGE_MAIN, size_attr="main_texture_size", tag_attr="main_texture_tag", max_display_width=960, ) if state.snapshot_frame is not None: self._set_image_texture( frame=nav.resize_preview(state.snapshot_frame, 520), image_tag=IMAGE_SNAPSHOT, size_attr="snapshot_texture_size", tag_attr="snapshot_texture_tag", max_display_width=520, ) assert self.engine is not None avg_yolo = self.engine.yolo_total_ms / max(self.engine.yolo_cycles, 1) dpg.set_value( TEXT_METRICS, "\n".join([ state.fps_text, f"YOLO ultimo: {state.yolo_ms:.1f} ms", f"YOLO medio: {avg_yolo:.1f} ms", self.engine.navigator.motion_text, f"Snapshot salvati: {self.engine.navigator.snapshot_counter}", f"Output: {Path(self.args.snapshot_output_dir).resolve()}", ]), ) dpg.set_value( TEXT_COMMANDS, "\n".join(self.engine.navigator.last_command_lines) if self.engine.navigator.last_command_lines else "Nessun comando generato", ) dpg.set_value(TEXT_TRACKS, self._format_tracks(state.tracks)) if state.snapshots: dpg.set_value( TEXT_STATUS, f"Snapshot {state.snapshots[-1].snapshot_id:04d} acquisito " f"su {state.snapshots[-1].simulated_position}", ) def _set_image_texture( self, frame: np.ndarray, image_tag: str, size_attr: str, tag_attr: str, max_display_width: int, ) -> None: h, w = frame.shape[:2] rgba = bgr_to_rgba_float(frame) display_scale = min(1.0, max_display_width / float(w)) display_w = max(1, int(w * display_scale)) display_h = max(1, int(h * display_scale)) old_size = getattr(self, size_attr) if old_size != (w, h): old_texture_tag = getattr(self, tag_attr) new_texture_tag = dpg.generate_uuid() dpg.add_dynamic_texture( w, h, rgba, tag=new_texture_tag, parent=TEXTURE_REGISTRY, ) dpg.configure_item( image_tag, texture_tag=new_texture_tag, width=display_w, height=display_h, ) if dpg.does_item_exist(old_texture_tag): dpg.delete_item(old_texture_tag) setattr(self, tag_attr, new_texture_tag) setattr(self, size_attr, (w, h)) return dpg.configure_item(image_tag, width=display_w, height=display_h) dpg.set_value(getattr(self, tag_attr), rgba) @staticmethod def _format_tracks(tracks: list[nav.Track]) -> str: if not tracks: return "Nessuna track" lines = [] for track in tracks[:12]: cx, cy = nav.bbox_center(track.bbox) lines.append( f"#{track.id:03d} {track.state:<11} " f"conf={track.confidence:.2f} hits={track.hits:<3} " f"missed={track.missed:<2} center=({cx:.0f},{cy:.0f}) " f"trend={track.area_trend():+.2f} {track.last_candidate_reason}" ) return "\n".join(lines) def bgr_to_rgba_float(frame: np.ndarray) -> np.ndarray: rgba = cv2.cvtColor(frame, cv2.COLOR_BGR2RGBA) return np.asarray(rgba, dtype=np.float32).ravel() / 255.0 def build_ui(app: NavigationDemoGui) -> None: dpg.create_context() with dpg.font_registry(): default_font = dpg.add_font("C:/Windows/Fonts/segoeui.ttf", 17) mono_font = dpg.add_font("C:/Windows/Fonts/consola.ttf", 15) with dpg.texture_registry(show=False, tag=TEXTURE_REGISTRY): blank = np.zeros((32, 32, 4), dtype=np.float32).ravel() dpg.add_dynamic_texture(32, 32, blank, tag=TEXTURE_MAIN) dpg.add_dynamic_texture(32, 32, blank, tag=TEXTURE_SNAPSHOT) with dpg.window(tag="main_window", label="FlyWMS Navigation Demo"): with dpg.group(horizontal=True): dpg.add_button(label="Avvia", tag=BUTTON_RUN, callback=lambda: app.toggle_run()) dpg.add_button(label="Step", callback=lambda: app.step_once()) dpg.add_button(label="Reset", callback=lambda: app.reset()) dpg.add_text("Pronto", tag=TEXT_STATUS) dpg.add_separator() with dpg.group(horizontal=True): with dpg.child_window(width=980, height=-1, border=False): dpg.add_image(TEXTURE_MAIN, tag=IMAGE_MAIN) with dpg.child_window(width=560, height=-1, border=False): dpg.add_text("Metriche") dpg.add_text("Metriche non disponibili", tag=TEXT_METRICS) dpg.bind_item_font(TEXT_METRICS, mono_font) dpg.add_separator() dpg.add_text("Comandi") dpg.add_text("Nessun comando generato", tag=TEXT_COMMANDS) dpg.bind_item_font(TEXT_COMMANDS, mono_font) dpg.add_separator() dpg.add_text("Payload OCR") dpg.add_image(TEXTURE_SNAPSHOT, tag=IMAGE_SNAPSHOT) dpg.add_separator() dpg.add_text("Track") dpg.add_text("Nessuna track", tag=TEXT_TRACKS) dpg.bind_item_font(TEXT_TRACKS, mono_font) dpg.bind_font(default_font) dpg.create_viewport(title="FlyWMS Navigation Demo", width=1600, height=980) dpg.setup_dearpygui() dpg.show_viewport() dpg.set_primary_window("main_window", True) def main() -> int: if dpg is None: print( "DearPyGUI non e' installato. Installa il pacchetto con:\n" " python -m pip install dearpygui\n" f"Dettaglio import: {DEARPYGUI_IMPORT_ERROR}", flush=True, ) return 1 args = nav.parse_args() args.no_display = True app = NavigationDemoGui(args) build_ui(app) try: while dpg.is_dearpygui_running(): app.tick() dpg.render_dearpygui_frame() finally: app.close() dpg.destroy_context() return 0 if __name__ == "__main__": raise SystemExit(main())