Compare commits
4 Commits
milestone-
...
98b43ce903
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
98b43ce903 | ||
|
|
1186c3bb35 | ||
|
|
16458d98e9 | ||
|
|
8a8bea1211 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -11,3 +11,4 @@ dataset_yolo/labels/*.cache
|
|||||||
dataset_yolo/labels/*_backup_before_remap_*/
|
dataset_yolo/labels/*_backup_before_remap_*/
|
||||||
runs/flywms_dataset_check/
|
runs/flywms_dataset_check/
|
||||||
runs/flywms_dataset_check_1epoch/
|
runs/flywms_dataset_check_1epoch/
|
||||||
|
navigate_snapshots*/
|
||||||
|
|||||||
70
aggiornamento-2026-05-16-09-03.md
Normal file
70
aggiornamento-2026-05-16-09-03.md
Normal 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.
|
||||||
179
flywms_navigation.ini
Normal file
179
flywms_navigation.ini
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
[navigation]
|
||||||
|
; OBBLIGATORIO: no.
|
||||||
|
; Ruolo: sorgente video usata per simulare la camera del drone.
|
||||||
|
; Se vuoto o "none", usa webcam 0.
|
||||||
|
; Default se non indicato: testhd.mp4
|
||||||
|
video = testhd.mp4
|
||||||
|
|
||||||
|
; OBBLIGATORIO: si.
|
||||||
|
; Ruolo: modello Ultralytics/YOLO moderno usato per rilevare gaylord ed etichette.
|
||||||
|
; Default se non indicato: C:\devel\flywms\runs\flywms_yolo11n_quick20\weights\best.pt
|
||||||
|
weights = C:\devel\flywms\runs\flywms_yolo11n_quick20\weights\best.pt
|
||||||
|
|
||||||
|
; OBBLIGATORIO: no.
|
||||||
|
; Ruolo: device usato da Ultralytics. Usa "cpu" ora; con GPU compatibile usare "0".
|
||||||
|
; Default se non indicato: cpu
|
||||||
|
ultralytics_device = cpu
|
||||||
|
|
||||||
|
; OBBLIGATORIO: no.
|
||||||
|
; Ruolo: dimensione input YOLO. 640 e' il valore usato nel training rapido.
|
||||||
|
; Default se non indicato: 640
|
||||||
|
input_size = 640
|
||||||
|
|
||||||
|
; OBBLIGATORIO: no.
|
||||||
|
; Ruolo: confidenza minima delle detection accettate dal detector.
|
||||||
|
; Default se non indicato: 0.25
|
||||||
|
min_confidence = 0.25
|
||||||
|
|
||||||
|
; OBBLIGATORIO: no.
|
||||||
|
; Ruolo: classe tracciata dalla navigazione. Le altre detection non entrano nel tracker.
|
||||||
|
; Default se non indicato: gaylord
|
||||||
|
target_class = gaylord
|
||||||
|
|
||||||
|
; OBBLIGATORIO: no.
|
||||||
|
; Ruolo: numero massimo di frame in cui una track puo' non essere vista prima di essere rimossa.
|
||||||
|
; Default se non indicato: 8
|
||||||
|
max_track_missed = 8
|
||||||
|
|
||||||
|
; OBBLIGATORIO: no.
|
||||||
|
; Ruolo: soglia minima dello score che associa una detection a una track esistente.
|
||||||
|
; Default se non indicato: 0.25
|
||||||
|
min_match_score = 0.25
|
||||||
|
|
||||||
|
; OBBLIGATORIO: no.
|
||||||
|
; Ruolo: distanza massima ammessa tra centri bbox, espressa come frazione della larghezza frame.
|
||||||
|
; Default se non indicato: 0.18
|
||||||
|
max_center_distance_ratio = 0.18
|
||||||
|
|
||||||
|
; OBBLIGATORIO: no.
|
||||||
|
; Ruolo: mezza ampiezza della fascia azzurra di avvicinamento al centro.
|
||||||
|
; Non fa scattare la foto: indica solo che la track e' candidata.
|
||||||
|
; Default se non indicato: 0.18
|
||||||
|
center_tolerance_ratio = 0.18
|
||||||
|
|
||||||
|
; OBBLIGATORIO: no.
|
||||||
|
; Ruolo: tolleranza stretta dalla linea verticale centrale per scattare la foto.
|
||||||
|
; La foto parte quando il centro bbox e' entro questa soglia.
|
||||||
|
; Default se non indicato: 0.035
|
||||||
|
snapshot_line_tolerance_ratio = 0.035
|
||||||
|
|
||||||
|
; OBBLIGATORIO: no.
|
||||||
|
; Ruolo: limite verticale superiore della fascia utile della scaffalatura.
|
||||||
|
; Default se non indicato: 0.15
|
||||||
|
usable_y_min_ratio = 0.15
|
||||||
|
|
||||||
|
; OBBLIGATORIO: no.
|
||||||
|
; Ruolo: limite verticale inferiore della fascia utile della scaffalatura.
|
||||||
|
; Default se non indicato: 0.85
|
||||||
|
usable_y_max_ratio = 0.85
|
||||||
|
|
||||||
|
; OBBLIGATORIO: no.
|
||||||
|
; Ruolo: numero minimo di detection confermate prima di considerare affidabile una track.
|
||||||
|
; Default se non indicato: 3
|
||||||
|
min_track_hits = 3
|
||||||
|
|
||||||
|
; OBBLIGATORIO: no.
|
||||||
|
; Ruolo: area minima del bbox gaylord rispetto all'intero frame.
|
||||||
|
; Serve a ignorare oggetti troppo lontani/piccoli.
|
||||||
|
; Default se non indicato: 0.02
|
||||||
|
min_gaylord_area_ratio = 0.02
|
||||||
|
|
||||||
|
; OBBLIGATORIO: no.
|
||||||
|
; Ruolo: margine da bordo immagine per considerare un bbox tagliato.
|
||||||
|
; 0 disabilita questo filtro, utile con il video manuale di test.
|
||||||
|
; Default se non indicato: 0.0
|
||||||
|
edge_margin_ratio = 0.0
|
||||||
|
|
||||||
|
; OBBLIGATORIO: no.
|
||||||
|
; Ruolo: padding aggiunto al bbox centrale prima di salvare il crop inviato all'OCR remoto.
|
||||||
|
; Default se non indicato: 0.03
|
||||||
|
ocr_payload_pad_ratio = 0.03
|
||||||
|
|
||||||
|
; OBBLIGATORIO: no.
|
||||||
|
; Ruolo: trend minimo dell'area bbox negli ultimi frame. Valori negativi tollerano leggera uscita.
|
||||||
|
; Default se non indicato: -0.35
|
||||||
|
min_area_trend = -0.35
|
||||||
|
|
||||||
|
; OBBLIGATORIO: no.
|
||||||
|
; Ruolo: numero di candidati da valutare prima dello snapshot.
|
||||||
|
; 1 significa: scatta subito quando il centro tocca la linea.
|
||||||
|
; Default se non indicato: 1
|
||||||
|
snapshot_window_frames = 1
|
||||||
|
|
||||||
|
; OBBLIGATORIO: no.
|
||||||
|
; Ruolo: directory dove salvare frame debug, crop OCR e snapshots.jsonl.
|
||||||
|
; Default se non indicato: navigate_snapshots
|
||||||
|
snapshot_output_dir = navigate_snapshots
|
||||||
|
|
||||||
|
; OBBLIGATORIO: no.
|
||||||
|
; Ruolo: tempo simulato con cui il drone attende OCR remoto + verifica WMS.
|
||||||
|
; Default se non indicato: 2.0
|
||||||
|
remote_ack_timeout_sec = 2.0
|
||||||
|
|
||||||
|
; OBBLIGATORIO: no.
|
||||||
|
; Ruolo: risposta remota simulata. Valori: always-ack, always-nack, alternate.
|
||||||
|
; Default se non indicato: always-ack
|
||||||
|
remote_ack_mode = always-ack
|
||||||
|
|
||||||
|
; OBBLIGATORIO: no.
|
||||||
|
; Ruolo: direzione simulata di ripartenza dopo ACK. Valori: destra, sinistra.
|
||||||
|
; Default se non indicato: destra
|
||||||
|
scan_direction = destra
|
||||||
|
|
||||||
|
; OBBLIGATORIO: no.
|
||||||
|
; Ruolo: larghezza massima delle finestre video di debug.
|
||||||
|
; Default se non indicato: 1280
|
||||||
|
preview_width = 1280
|
||||||
|
|
||||||
|
; OBBLIGATORIO: no.
|
||||||
|
; Ruolo: se true, il video di test viene riprodotto rispettando il framerate originale.
|
||||||
|
; 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
|
||||||
|
max_frames = 0
|
||||||
|
|
||||||
|
; OBBLIGATORIO: no.
|
||||||
|
; Ruolo: ogni quanti secondi stampare statistiche nel terminale.
|
||||||
|
; Default se non indicato: 2.0
|
||||||
|
stats_interval = 2.0
|
||||||
|
|
||||||
|
; OBBLIGATORIO: no.
|
||||||
|
; Ruolo: ogni quanti frame aggiornare il moto apparente stimato dalle track.
|
||||||
|
; Default se non indicato: 5
|
||||||
|
motion_report_interval = 5
|
||||||
|
|
||||||
|
; OBBLIGATORIO: no.
|
||||||
|
; Ruolo: movimento medio minimo in pixel per dichiarare destra/sinistra/su/giu.
|
||||||
|
; Default se non indicato: 1.5
|
||||||
|
motion_min_pixels = 1.5
|
||||||
|
|
||||||
|
; OBBLIGATORIO: no.
|
||||||
|
; Ruolo: se true, logga nel terminale lo stato delle track e i motivi di non scatto.
|
||||||
|
; Default se non indicato: true
|
||||||
|
debug_tracks = true
|
||||||
|
|
||||||
|
; OBBLIGATORIO: no.
|
||||||
|
; Ruolo: intensita' del flash visuale simulato al momento dello scatto, da 0 a 1.
|
||||||
|
; Default se non indicato: 0.70
|
||||||
|
flash_alpha = 0.70
|
||||||
|
|
||||||
|
; OBBLIGATORIO: no.
|
||||||
|
; Ruolo: se true, disabilita tutte le finestre video. Usarlo solo per test headless.
|
||||||
|
; Default se non indicato: false
|
||||||
|
no_display = false
|
||||||
1009
flywms_navigation.py
Normal file
1009
flywms_navigation.py
Normal file
File diff suppressed because it is too large
Load Diff
415
flywms_navigation_gui.py
Normal file
415
flywms_navigation_gui.py
Normal file
@@ -0,0 +1,415 @@
|
|||||||
|
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)
|
||||||
|
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 / 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,
|
||||||
|
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.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:
|
||||||
|
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
|
||||||
|
|
||||||
|
new_snapshots: list[nav.NavigationSnapshot] = []
|
||||||
|
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)
|
||||||
|
)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
elapsed = max(time.perf_counter() - self.start_time, 0.001)
|
||||||
|
fps_text = (
|
||||||
|
f"frame={self.frame_id} fps={self.frame_id / elapsed:.1f} "
|
||||||
|
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,
|
||||||
|
self.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=self.tracks,
|
||||||
|
detections_count=len(self.gaylords),
|
||||||
|
snapshots=new_snapshots,
|
||||||
|
yolo_ms=self.last_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())
|
||||||
116
handoff.md
Normal file
116
handoff.md
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
# FlyWMS handoff - GUI navigazione
|
||||||
|
|
||||||
|
Data: 2026-05-15
|
||||||
|
|
||||||
|
## Stato repository
|
||||||
|
|
||||||
|
- Branch: `master`
|
||||||
|
- Remote: `origin` su Gitea
|
||||||
|
- Ultimo lavoro: aggiunta shell DearPyGUI separata per la simulazione dimostrativa di `flywms_navigation.py`.
|
||||||
|
|
||||||
|
## Contesto recuperato
|
||||||
|
|
||||||
|
L'obiettivo discusso era trasformare `flywms_navigation.py` in una simulazione piu' ordinata e dimostrativa, senza contaminare la logica esistente. Il programma originale contiene gia':
|
||||||
|
|
||||||
|
- detector YOLO/Ultralytics;
|
||||||
|
- tracking geometrico leggero dei `gaylord`;
|
||||||
|
- decisione di snapshot quando una track e' centrata;
|
||||||
|
- salvataggio frame debug, payload OCR e `snapshots.jsonl`;
|
||||||
|
- simulazione comandi `STOP`, `SCATTA_FOTO`, `ASSOCIA_POSIZIONE`, `INVIA_ROI_REMOTA`, `ATTENDI_ACK`, `RIPARTI_*`;
|
||||||
|
- UI debug OpenCV integrata nel `main()`.
|
||||||
|
|
||||||
|
## Decisione architetturale
|
||||||
|
|
||||||
|
La GUI DearPyGUI e' stata tenuta in un file separato, `flywms_navigation_gui.py`.
|
||||||
|
|
||||||
|
Motivo: non toccare il ciclo originale e non rischiare regressioni nella logica di navigazione. La GUI importa `flywms_navigation.py` e riusa:
|
||||||
|
|
||||||
|
- `UltralyticsDetector`;
|
||||||
|
- `LightweightTracker`;
|
||||||
|
- `NavigationController`;
|
||||||
|
- `draw_navigation_debug`;
|
||||||
|
- configurazione da `flywms_navigation.ini`.
|
||||||
|
|
||||||
|
In pratica `flywms_navigation.py` resta il simulatore CLI/OpenCV funzionante; `flywms_navigation_gui.py` e' una shell dimostrativa sopra la stessa logica.
|
||||||
|
|
||||||
|
## Implementato
|
||||||
|
|
||||||
|
File nuovo:
|
||||||
|
|
||||||
|
- `flywms_navigation_gui.py`
|
||||||
|
|
||||||
|
Funzioni principali:
|
||||||
|
|
||||||
|
- `NavigationDemoEngine`: esegue un singolo step del ciclo video/YOLO/tracking/navigazione e restituisce uno stato frame-by-frame.
|
||||||
|
- `NavigationDemoGui`: gestisce finestra DearPyGUI, comandi Avvia/Pausa, Step, Reset, preview, metriche, comandi, payload OCR e track.
|
||||||
|
- conversione frame OpenCV BGR in texture RGBA DearPyGUI.
|
||||||
|
- gestione texture corretta con tag univoci quando cambia la dimensione immagine.
|
||||||
|
- resize visuale della preview principale a 960x540.
|
||||||
|
|
||||||
|
DearPyGUI e' stato installato nell'ambiente Python corrente con:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python -m pip install dearpygui
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verifiche eseguite
|
||||||
|
|
||||||
|
Compilazione:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python -m py_compile flywms_navigation_gui.py flywms_navigation.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Test motore GUI senza finestra persistente:
|
||||||
|
|
||||||
|
```text
|
||||||
|
state1 1 (720, 1280, 3)
|
||||||
|
state2 2 (720, 1280, 3)
|
||||||
|
```
|
||||||
|
|
||||||
|
Test aggiornamento UI/texture:
|
||||||
|
|
||||||
|
```text
|
||||||
|
ui step ok
|
||||||
|
960 540
|
||||||
|
```
|
||||||
|
|
||||||
|
Avvio manuale usato:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python flywms_navigation_gui.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Osservazioni performance
|
||||||
|
|
||||||
|
Con CPU, la GUI scende circa da 6 fps a 3.8 fps. Il dato e' credibile perche' la shell GUI aggiunge:
|
||||||
|
|
||||||
|
- disegno overlay OpenCV;
|
||||||
|
- conversione BGR -> RGBA;
|
||||||
|
- normalizzazione/copia texture `float32`;
|
||||||
|
- aggiornamento pannelli DearPyGUI nello stesso thread di YOLO.
|
||||||
|
|
||||||
|
Il carico CPU alto su tutti gli 8 core e' credibile: PyTorch/OpenCV/NumPy usano thread nativi anche se il loop Python e' singolo.
|
||||||
|
|
||||||
|
Quando sara' disponibile una GPU corretta, conviene rimisurare separando:
|
||||||
|
|
||||||
|
- fps YOLO/tracking;
|
||||||
|
- fps GUI;
|
||||||
|
- tempo medio YOLO;
|
||||||
|
- tempo medio conversione texture.
|
||||||
|
|
||||||
|
## Prossimi passi consigliati
|
||||||
|
|
||||||
|
1. Migliorare la GUI come demo:
|
||||||
|
- timeline eventi `STOP`, `SCATTA_FOTO`, `ACK/NACK`, `RIPARTI`;
|
||||||
|
- pannello soglie principali;
|
||||||
|
- evidenza piu' leggibile dello stato `entering/candidate/centered/snapshotted`;
|
||||||
|
- contatori snapshot e posizioni simulate.
|
||||||
|
|
||||||
|
2. Ottimizzare solo se serve:
|
||||||
|
- aggiornare texture a frequenza limitata;
|
||||||
|
- convertire per GUI un frame gia' ridotto;
|
||||||
|
- separare inferenza e GUI con stato latest-frame;
|
||||||
|
- aggiornare testo solo quando cambia.
|
||||||
|
|
||||||
|
3. Non modificare la logica di `flywms_navigation.py` finche' la GUI non ha stabilizzato i requisiti dimostrativi.
|
||||||
BIN
testhd2.mp4
LFS
BIN
testhd2.mp4
LFS
Binary file not shown.
Reference in New Issue
Block a user