diff --git a/aggiornamento-2026-05-16-09-03.md b/aggiornamento-2026-05-16-09-03.md new file mode 100644 index 0000000..893d9e8 --- /dev/null +++ b/aggiornamento-2026-05-16-09-03.md @@ -0,0 +1,70 @@ +# Aggiornamento 2026-05-16 09:03 + +## Decisione + +Separare il ritmo della preview dal ritmo di inferenza YOLO. + +Motivo: il video sorgente e' circa 30 fps, ma la pipeline completa non deve necessariamente elaborare ogni frame. Per la demo e per la futura pipeline reale e' piu' utile mantenere bassa la latenza, lasciando che la visualizzazione e YOLO abbiano frequenze configurabili. + +## Implementazione + +Aggiunti due parametri in `flywms_navigation.ini` e nel parser di `flywms_navigation.py`: + +- `preview_fps`: FPS massimo per lettura/preview realtime. `0` usa il framerate della sorgente. +- `yolo_fps`: FPS massimo per inferenza YOLO. `0` esegue YOLO su ogni frame di preview. + +Valori iniziali: + +```ini +preview_fps = 24.0 +yolo_fps = 15.0 +``` + +La stessa logica e' stata applicata anche alla shell DearPyGUI `flywms_navigation_gui.py`. + +## Nota su video e camera reale + +Con un file video, questi parametri simulano il ritmo desiderato della preview e limitano l'inferenza YOLO. In questa versione single-thread, se YOLO e' piu' lento del target, non si puo' arrivare al target. + +Con una camera reale, il codice prova anche a impostare `CAP_PROP_FPS` quando la sorgente e' webcam/camera. Questo e' best effort: molti driver ignorano il valore. Il metodo robusto resta scartare frame lato software e usare sempre il frame piu' recente disponibile. + +## Verifiche + +Compilazione: + +```powershell +python -m py_compile flywms_navigation.py flywms_navigation_gui.py +``` + +Lettura config: + +```text +24.0 15.0 +``` + +Run headless con `yolo_fps=15` su CPU: + +```text +fps=5.5 yolo_fps=5.5 +``` + +Interpretazione: su CPU il ciclo non raggiunge 15 fps, quindi YOLO gira comunque su ogni frame effettivamente processato. + +Run headless con `yolo_fps=2`: + +```text +fps=13.7 yolo_fps=2.0 +``` + +Interpretazione: il throttling YOLO funziona; la preview procede piu' veloce dell'inferenza. + +## Prossimo passo + +Quando arrivera' la GPU, rimisurare con: + +- `preview_fps = 24.0`; +- `yolo_fps = 15.0`; +- device GPU Ultralytics; +- GUI DearPyGUI attiva. + +Se il costo GUI diventera' dominante, separare in seguito thread inferenza e thread GUI con stato latest-frame. diff --git a/flywms_navigation.ini b/flywms_navigation.ini index 4a8d94f..a4f4a89 100644 --- a/flywms_navigation.ini +++ b/flywms_navigation.ini @@ -130,6 +130,19 @@ preview_width = 1280 ; Default se non indicato: true realtime_playback = true +; OBBLIGATORIO: no. +; Ruolo: FPS massimo per lettura/preview realtime. 0 usa il framerate della sorgente. +; Con video registrati puo' essere usato per simulare una preview piu' lenta, es. 24 fps. +; Con webcam/camere viene anche richiesto al driver, ma non tutti i driver rispettano il valore. +; Default se non indicato: 24.0 +preview_fps = 24.0 + +; OBBLIGATORIO: no. +; Ruolo: FPS massimo per inferenza YOLO. 0 esegue YOLO su ogni frame di preview. +; Nei frame intermedi la preview continua usando l'ultimo stato di tracking disponibile. +; Default se non indicato: 15.0 +yolo_fps = 15.0 + ; OBBLIGATORIO: no. ; Ruolo: massimo numero di frame da processare. 0 significa tutto il video. ; Default se non indicato: 0 diff --git a/flywms_navigation.py b/flywms_navigation.py index 5d5f238..e88ffc5 100644 --- a/flywms_navigation.py +++ b/flywms_navigation.py @@ -518,6 +518,10 @@ def parse_args(): ap.add_argument("--preview-width", type=int, default=defaults["preview_width"], help="Larghezza preview") ap.add_argument("--realtime-playback", action="store_true", default=defaults["realtime_playback"], help="Rispetta FPS video") + ap.add_argument("--preview-fps", type=float, default=defaults["preview_fps"], + help="FPS massimo per lettura/preview realtime. 0 = FPS sorgente") + ap.add_argument("--yolo-fps", type=float, default=defaults["yolo_fps"], + help="FPS massimo per inferenza YOLO. 0 = ogni frame di preview") ap.add_argument("--max-frames", type=int, default=defaults["max_frames"], help="Numero massimo frame; 0 = tutto") ap.add_argument("--stats-interval", type=float, default=defaults["stats_interval"], help="Intervallo log prestazioni") ap.add_argument("--motion-report-interval", type=int, default=defaults["motion_report_interval"], @@ -557,6 +561,8 @@ def load_navigation_config(path_str: str) -> dict[str, object]: "scan_direction": "destra", "preview_width": 1280, "realtime_playback": True, + "preview_fps": 24.0, + "yolo_fps": 15.0, "max_frames": 0, "stats_interval": 2.0, "motion_report_interval": 5, @@ -841,7 +847,11 @@ def main() -> int: return 1 video_fps = cap.get(cv2.CAP_PROP_FPS) - frame_delay = 1.0 / video_fps if args.realtime_playback and video_fps and video_fps > 1 else 0.0 + preview_fps = args.preview_fps if args.preview_fps and args.preview_fps > 0 else video_fps + if args.preview_fps and args.preview_fps > 0 and (args.video is None or str(args.video).isdigit()): + cap.set(cv2.CAP_PROP_FPS, float(args.preview_fps)) + frame_delay = 1.0 / preview_fps if args.realtime_playback and preview_fps and preview_fps > 1 else 0.0 + yolo_interval = 1.0 / args.yolo_fps if args.yolo_fps and args.yolo_fps > 0 else 0.0 tracker = LightweightTracker( max_missed=args.max_track_missed, min_match_score=args.min_match_score, @@ -860,6 +870,10 @@ def main() -> int: last_loop_end = start_time yolo_total_ms = 0.0 yolo_cycles = 0 + next_yolo_time = start_time + last_yolo_ms = 0.0 + gaylords: list[Detection] = [] + tracks: list[Track] = [] try: while True: @@ -880,25 +894,28 @@ def main() -> int: log(f"Raggiunto --max-frames={args.max_frames}") break - detections, yolo_ms = detector.detect(frame, args.min_confidence, args.input_size) - yolo_total_ms += yolo_ms - yolo_cycles += 1 - gaylords = [ - det for det in detections - if det.class_name.strip().lower() == args.target_class.strip().lower() - ] - - tracks = tracker.update(gaylords, frame_id, frame.shape[1]) - if args.motion_report_interval > 0 and frame_id % args.motion_report_interval == 0: - navigator.set_motion_text( - estimate_motion_from_tracks(tracks, args.motion_min_pixels) - ) new_snapshots: list[NavigationSnapshot] = [] - for track in tracks: - if track.missed == 0: - snapshot = navigator.process_track(track, frame, frame_id, timestamp) - if snapshot is not None: - new_snapshots.append(snapshot) + run_yolo = yolo_interval <= 0 or timestamp >= next_yolo_time + if run_yolo: + next_yolo_time = timestamp + yolo_interval + detections, last_yolo_ms = detector.detect(frame, args.min_confidence, args.input_size) + yolo_total_ms += last_yolo_ms + yolo_cycles += 1 + gaylords = [ + det for det in detections + if det.class_name.strip().lower() == args.target_class.strip().lower() + ] + + tracks = tracker.update(gaylords, frame_id, frame.shape[1]) + if args.motion_report_interval > 0 and yolo_cycles % args.motion_report_interval == 0: + navigator.set_motion_text( + estimate_motion_from_tracks(tracks, args.motion_min_pixels) + ) + for track in tracks: + if track.missed == 0: + snapshot = navigator.process_track(track, frame, frame_id, timestamp) + if snapshot is not None: + new_snapshots.append(snapshot) if args.no_display and new_snapshots: if args.remote_ack_timeout_sec > 0: time.sleep(args.remote_ack_timeout_sec) @@ -931,6 +948,7 @@ def main() -> int: elapsed = max(time.perf_counter() - start_time, 0.001) fps_text = ( f"frame={frame_id} fps={frame_id / elapsed:.1f} " + f"yolo_fps={yolo_cycles / elapsed:.1f} yolo={last_yolo_ms:.0f}ms " f"det={len(gaylords)} tracks={len(tracks)} snap={navigator.snapshot_counter}" ) display = draw_navigation_debug( diff --git a/flywms_navigation_gui.py b/flywms_navigation_gui.py index 871bace..3bc387c 100644 --- a/flywms_navigation_gui.py +++ b/flywms_navigation_gui.py @@ -59,11 +59,15 @@ class NavigationDemoEngine: raise RuntimeError(f"impossibile aprire sorgente video: {self.source_name}") video_fps = self.cap.get(cv2.CAP_PROP_FPS) + preview_fps = args.preview_fps if args.preview_fps and args.preview_fps > 0 else video_fps + if args.preview_fps and args.preview_fps > 0 and (args.video is None or str(args.video).isdigit()): + self.cap.set(cv2.CAP_PROP_FPS, float(args.preview_fps)) self.frame_delay = ( - 1.0 / video_fps - if args.realtime_playback and video_fps and video_fps > 1 + 1.0 / preview_fps + if args.realtime_playback and preview_fps and preview_fps > 1 else 0.0 ) + self.yolo_interval = 1.0 / args.yolo_fps if args.yolo_fps and args.yolo_fps > 0 else 0.0 self.tracker = nav.LightweightTracker( max_missed=args.max_track_missed, min_match_score=args.min_match_score, @@ -75,6 +79,10 @@ class NavigationDemoEngine: self.last_loop_end = self.start_time self.yolo_total_ms = 0.0 self.yolo_cycles = 0 + self.next_yolo_time = self.start_time + self.last_yolo_ms = 0.0 + self.gaylords: list[nav.Detection] = [] + self.tracks: list[nav.Track] = [] self.stop_reason = "" def close(self) -> None: @@ -99,39 +107,42 @@ class NavigationDemoEngine: 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, + run_yolo = self.yolo_interval <= 0 or timestamp >= self.next_yolo_time + if run_yolo: + self.next_yolo_time = timestamp + self.yolo_interval + detections, self.last_yolo_ms = self.detector.detect( + frame, + self.args.min_confidence, + self.args.input_size, + ) + self.yolo_total_ms += self.last_yolo_ms + self.yolo_cycles += 1 + + self.gaylords = [ + det for det in detections + if det.class_name.strip().lower() == self.args.target_class.strip().lower() + ] + self.tracks = self.tracker.update(self.gaylords, self.frame_id, frame.shape[1]) + + if ( + self.args.motion_report_interval > 0 + and self.yolo_cycles % self.args.motion_report_interval == 0 + ): + self.navigator.set_motion_text( + nav.estimate_motion_from_tracks(self.tracks, self.args.motion_min_pixels) ) - if snapshot is not None: - new_snapshots.append(snapshot) + + for track in self.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) @@ -139,12 +150,13 @@ class NavigationDemoEngine: 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"yolo_fps={self.yolo_cycles / elapsed:.1f} yolo={self.last_yolo_ms:.0f}ms " + f"det={len(self.gaylords)} tracks={len(self.tracks)} " f"snap={self.navigator.snapshot_counter}" ) display = nav.draw_navigation_debug( frame, - tracks, + self.tracks, self.args, self.navigator.last_command_text, fps_text, @@ -156,10 +168,10 @@ class NavigationDemoEngine: source_frame=frame, display_frame=display, snapshot_frame=self.navigator.last_ocr_payload_frame, - tracks=tracks, - detections_count=len(gaylords), + tracks=self.tracks, + detections_count=len(self.gaylords), snapshots=new_snapshots, - yolo_ms=yolo_ms, + yolo_ms=self.last_yolo_ms, fps_text=fps_text, )