configura fps preview e yolo

This commit is contained in:
administrator
2026-05-16 09:10:48 +02:00
parent 16458d98e9
commit 1186c3bb35
4 changed files with 170 additions and 57 deletions

View File

@@ -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.

View File

@@ -130,6 +130,19 @@ preview_width = 1280
; Default se non indicato: true ; Default se non indicato: true
realtime_playback = 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. ; OBBLIGATORIO: no.
; Ruolo: massimo numero di frame da processare. 0 significa tutto il video. ; Ruolo: massimo numero di frame da processare. 0 significa tutto il video.
; Default se non indicato: 0 ; Default se non indicato: 0

View File

@@ -518,6 +518,10 @@ def parse_args():
ap.add_argument("--preview-width", type=int, default=defaults["preview_width"], help="Larghezza preview") 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("--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("--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("--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"], 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", "scan_direction": "destra",
"preview_width": 1280, "preview_width": 1280,
"realtime_playback": True, "realtime_playback": True,
"preview_fps": 24.0,
"yolo_fps": 15.0,
"max_frames": 0, "max_frames": 0,
"stats_interval": 2.0, "stats_interval": 2.0,
"motion_report_interval": 5, "motion_report_interval": 5,
@@ -841,7 +847,11 @@ def main() -> int:
return 1 return 1
video_fps = cap.get(cv2.CAP_PROP_FPS) 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( tracker = LightweightTracker(
max_missed=args.max_track_missed, max_missed=args.max_track_missed,
min_match_score=args.min_match_score, min_match_score=args.min_match_score,
@@ -860,6 +870,10 @@ def main() -> int:
last_loop_end = start_time last_loop_end = start_time
yolo_total_ms = 0.0 yolo_total_ms = 0.0
yolo_cycles = 0 yolo_cycles = 0
next_yolo_time = start_time
last_yolo_ms = 0.0
gaylords: list[Detection] = []
tracks: list[Track] = []
try: try:
while True: while True:
@@ -880,25 +894,28 @@ def main() -> int:
log(f"Raggiunto --max-frames={args.max_frames}") log(f"Raggiunto --max-frames={args.max_frames}")
break 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] = [] new_snapshots: list[NavigationSnapshot] = []
for track in tracks: run_yolo = yolo_interval <= 0 or timestamp >= next_yolo_time
if track.missed == 0: if run_yolo:
snapshot = navigator.process_track(track, frame, frame_id, timestamp) next_yolo_time = timestamp + yolo_interval
if snapshot is not None: detections, last_yolo_ms = detector.detect(frame, args.min_confidence, args.input_size)
new_snapshots.append(snapshot) 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.no_display and new_snapshots:
if args.remote_ack_timeout_sec > 0: if args.remote_ack_timeout_sec > 0:
time.sleep(args.remote_ack_timeout_sec) time.sleep(args.remote_ack_timeout_sec)
@@ -931,6 +948,7 @@ def main() -> int:
elapsed = max(time.perf_counter() - start_time, 0.001) elapsed = max(time.perf_counter() - start_time, 0.001)
fps_text = ( fps_text = (
f"frame={frame_id} fps={frame_id / elapsed:.1f} " 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}" f"det={len(gaylords)} tracks={len(tracks)} snap={navigator.snapshot_counter}"
) )
display = draw_navigation_debug( display = draw_navigation_debug(

View File

@@ -59,11 +59,15 @@ class NavigationDemoEngine:
raise RuntimeError(f"impossibile aprire sorgente video: {self.source_name}") raise RuntimeError(f"impossibile aprire sorgente video: {self.source_name}")
video_fps = self.cap.get(cv2.CAP_PROP_FPS) 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 = ( self.frame_delay = (
1.0 / video_fps 1.0 / preview_fps
if args.realtime_playback and video_fps and video_fps > 1 if args.realtime_playback and preview_fps and preview_fps > 1
else 0.0 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( self.tracker = nav.LightweightTracker(
max_missed=args.max_track_missed, max_missed=args.max_track_missed,
min_match_score=args.min_match_score, min_match_score=args.min_match_score,
@@ -75,6 +79,10 @@ class NavigationDemoEngine:
self.last_loop_end = self.start_time self.last_loop_end = self.start_time
self.yolo_total_ms = 0.0 self.yolo_total_ms = 0.0
self.yolo_cycles = 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 = "" self.stop_reason = ""
def close(self) -> None: def close(self) -> None:
@@ -99,39 +107,42 @@ class NavigationDemoEngine:
self.stop_reason = f"Raggiunto max_frames={self.args.max_frames}" self.stop_reason = f"Raggiunto max_frames={self.args.max_frames}"
return None 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] = [] new_snapshots: list[nav.NavigationSnapshot] = []
for track in tracks: run_yolo = self.yolo_interval <= 0 or timestamp >= self.next_yolo_time
if track.missed == 0: if run_yolo:
snapshot = self.navigator.process_track( self.next_yolo_time = timestamp + self.yolo_interval
track, detections, self.last_yolo_ms = self.detector.detect(
frame, frame,
self.frame_id, self.args.min_confidence,
timestamp, 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: for snapshot in new_snapshots:
self.navigator.simulate_remote_response(snapshot) self.navigator.simulate_remote_response(snapshot)
@@ -139,12 +150,13 @@ class NavigationDemoEngine:
elapsed = max(time.perf_counter() - self.start_time, 0.001) elapsed = max(time.perf_counter() - self.start_time, 0.001)
fps_text = ( fps_text = (
f"frame={self.frame_id} fps={self.frame_id / elapsed:.1f} " 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}" f"snap={self.navigator.snapshot_counter}"
) )
display = nav.draw_navigation_debug( display = nav.draw_navigation_debug(
frame, frame,
tracks, self.tracks,
self.args, self.args,
self.navigator.last_command_text, self.navigator.last_command_text,
fps_text, fps_text,
@@ -156,10 +168,10 @@ class NavigationDemoEngine:
source_frame=frame, source_frame=frame,
display_frame=display, display_frame=display,
snapshot_frame=self.navigator.last_ocr_payload_frame, snapshot_frame=self.navigator.last_ocr_payload_frame,
tracks=tracks, tracks=self.tracks,
detections_count=len(gaylords), detections_count=len(self.gaylords),
snapshots=new_snapshots, snapshots=new_snapshots,
yolo_ms=yolo_ms, yolo_ms=self.last_yolo_ms,
fps_text=fps_text, fps_text=fps_text,
) )