pipeline in linea single thread

This commit is contained in:
administrator
2026-05-19 08:52:44 +02:00
parent 98b43ce903
commit f728524ee6
43 changed files with 5245 additions and 154 deletions

1
.gitignore vendored
View File

@@ -12,3 +12,4 @@ dataset_yolo/labels/*_backup_before_remap_*/
runs/flywms_dataset_check/
runs/flywms_dataset_check_1epoch/
navigate_snapshots*/
wms_received*/

View File

@@ -0,0 +1,99 @@
# Aggiornamento 2026-05-16 10:14
## Obiettivo
Cambiare il payload destinato all'OCR: non piu' solo il crop del gaylord, ma il crop ravvicinato dell'etichetta associata al gaylord centrato.
Il gaylord resta l'ancora semantica: serve per sapere a quale oggetto appartiene l'etichetta. L'etichetta diventa il target ottico da centrare e fotografare.
## Decisioni
- L'etichetta accettata deve avere classe `etichetta`.
- Il bbox etichetta deve essere contenuto nel bbox del gaylord centrato.
- Detection etichetta fuori da un gaylord vengono ignorate.
- Se il gaylord e' centrato ma non c'e' etichetta contenuta, viene loggato `ETICHETTA_NON_TROVATA` e si attende un frame successivo.
- Il crop etichetta usa padding configurabile.
- Il sistema mantiene sia snapshot/crop gaylord sia crop etichetta.
- Il movimento drone verso l'etichetta e il ritorno al gaylord sono simulati in pixel.
## Parametri aggiunti
In `flywms_navigation.ini`:
```ini
label_class = etichetta
label_payload_pad_ratio = 0.20
label_move_sec = 2.0
label_stabilization_sec = 1.0
label_return_sec = 2.0
```
## Comportamento OpenCV
Quando il gaylord centrato ha un'etichetta associata:
1. si ferma la scansione;
2. viene disegnata una freccia nera bidirezionale dal centro del gaylord al centro dell'etichetta;
3. vengono mostrati i comandi di centraggio etichetta;
4. viene simulato il movimento verso l'etichetta;
5. viene simulata la stabilizzazione;
6. viene salvato il crop dell'etichetta;
7. viene simulato il ritorno al centro gaylord;
8. viene ripresa la scansione.
La finestra `flywms etichetta` mostra il crop dell'etichetta. La finestra `flywms snapshot` continua a mostrare il crop/debug del gaylord.
## Metadati
Ogni record JSONL ora include:
- `gaylord_bbox`;
- `label_bbox`;
- `movement_vector_px`;
- `debug_frame_path`;
- `ocr_payload_path`;
- `label_payload_path`.
## Verifiche
Compilazione:
```powershell
python -m py_compile flywms_navigation.py flywms_navigation_gui.py
```
Run headless ordinario su 80 frame:
```text
det=2 labels=1 tracks=2 snapshots=0
```
Nessuno snapshot nei primi 80 frame perche' il gaylord non era ancora sulla linea stretta di scatto.
Run headless forzato con tolleranza larga:
```powershell
python flywms_navigation.py --no-display --max-frames 6 --remote-ack-timeout-sec 0 --label-move-sec 0 --label-stabilization-sec 0 --label-return-sec 0 --snapshot-line-tolerance-ratio 0.20 --snapshot-output-dir navigate_snapshots_test --preview-fps 0 --yolo-fps 0
```
Risultato:
```text
SNAPSHOT 0001 track=2 frame=3 pos=gaylord 1
CENTRA_ETICHETTA dx=+314px dy=-498px
SCATTA_FOTO_ETICHETTA snapshot_0001_track_002_label_payload.jpg
INVIA_ROI_REMOTA snapshot_0001_track_002_label_payload.jpg
```
Crop generati:
```text
label_payload: (94, 371, 3)
ocr_payload/gaylord: (1079, 1114, 3)
```
## Limiti noti
- Il tracking etichetta non e' ancora un tracker separato; per ora usa la detection etichetta contenuta nel gaylord durante la finestra candidata.
- Se piu' etichette sono contenute nello stesso gaylord, viene scelto un fallback deterministico. In condizioni corrette non dovrebbe accadere.
- La GUI DearPyGUI e' stata aggiornata solo per non regredire, ma la validazione visuale di questa fase e' pensata sulle finestre OpenCV.

View File

@@ -0,0 +1,41 @@
# Aggiornamento 2026-05-16 10:37
## Obiettivo
Rendere piu' leggibile la simulazione di movimento verso l'etichetta nella finestra OpenCV dei comandi.
## Modifiche
- Aumentate di un secondo le pause configurate:
- `label_move_sec`: da 2.0 a 3.0;
- `label_stabilization_sec`: da 1.0 a 2.0;
- `label_return_sec`: da 2.0 a 3.0.
- Aggiunto un riquadro grafico nella finestra `flywms comandi`.
- Durante `CENTRA_ETICHETTA` il riquadro mostra una freccia grande dal centro gaylord verso nord-est/etichetta.
- Durante `RITORNA_CENTRO_GAYLORD` il riquadro mostra una freccia grande dall'etichetta verso il centro gaylord.
## Verifiche
Compilazione:
```powershell
python -m py_compile flywms_navigation.py flywms_navigation_gui.py
```
Lettura configurazione:
```text
3.0 2.0 3.0
```
Run headless forzato:
```powershell
python flywms_navigation.py --no-display --max-frames 6 --remote-ack-timeout-sec 0 --label-move-sec 0 --label-stabilization-sec 0 --label-return-sec 0 --snapshot-line-tolerance-ratio 0.20 --snapshot-output-dir navigate_snapshots_test --preview-fps 0 --yolo-fps 0
```
La verifica visuale va fatta con le finestre OpenCV:
```powershell
python flywms_navigation.py
```

View File

@@ -0,0 +1,30 @@
# Aggiornamento 2026-05-16 10:47
## Obiettivo
Rendere piu' leggibili nella finestra comandi i delta di spostamento verso l'etichetta e di ritorno al centro gaylord.
## Modifiche
- Nel riquadro grafico della finestra `flywms comandi` vengono ora mostrati in grande:
- `DX ORIZZONTALE`;
- `DY VERTICALE`.
- Durante `CENTRA_ETICHETTA` i valori sono quelli dal centro gaylord al centro etichetta.
- Durante `RITORNA_CENTRO_GAYLORD` i valori sono invertiti, quindi rappresentano il movimento dall'etichetta al centro gaylord.
- Il comando visuale della fase di ritorno contiene ora anche `dx` e `dy`.
## Verifiche
Compilazione:
```powershell
python -m py_compile flywms_navigation.py flywms_navigation_gui.py
```
Run headless forzato:
```powershell
python flywms_navigation.py --no-display --max-frames 6 --remote-ack-timeout-sec 0 --label-move-sec 0 --label-stabilization-sec 0 --label-return-sec 0 --snapshot-line-tolerance-ratio 0.20 --snapshot-output-dir navigate_snapshots_test --preview-fps 0 --yolo-fps 0
```
La verifica effettiva dei delta grandi va fatta con finestra OpenCV attiva.

View File

@@ -0,0 +1,50 @@
# Aggiornamento 2026-05-16 10:49
## Obiettivo
Rendere configurabile la posizione e dimensione delle finestre OpenCV.
## Modifiche
Aggiunti parametri in `flywms_navigation.ini`:
```ini
window_layout_enabled = true
navigate_window = 20,40,1100,620
commands_window = 1140,40,760,520
snapshot_window = 1140,590,520,360
label_window = 1140,980,520,260
```
Formato:
```text
x,y,width,height
```
In `flywms_navigation.py` sono state aggiunte:
- `parse_window_rect`;
- `apply_window_layout`.
La funzione viene chiamata subito dopo la creazione delle finestre OpenCV.
## Verifiche
Compilazione:
```powershell
python -m py_compile flywms_navigation.py flywms_navigation_gui.py
```
Lettura configurazione:
```text
True
20,40,1100,620
1140,40,760,520
1140,590,520,360
1140,980,520,260
```
Nota: OpenCV applica posizione e dimensione best effort. Su Windows di solito funziona, ma DPI scaling e multi-monitor possono introdurre differenze.

View File

@@ -0,0 +1,110 @@
# Aggiornamento 2026-05-16 11:04
## Milestone prima implementazione comunicazione WMS
Questa milestone documenta la specifica concordata prima di completare la parte client-server reale tra `flywms_navigation.py` e un server WMS demo.
## Obiettivo
Implementare una comunicazione reale, asincrona e configurabile tra:
- client navigazione: `flywms_navigation.py`;
- server WMS demo: nuovo script FastAPI.
Il client deve inviare il crop etichetta e i metadati dello snapshot. Il server deve ricevere, salvare i payload per debug, simulare OCR/WMS e rispondere `ACK` o `NACK`.
## Decisioni concordate
- Server demo basato su FastAPI.
- Dipendenze installate:
- `fastapi`;
- `uvicorn`;
- `python-multipart`.
- Il server deve salvare fisicamente le immagini ricevute in una cartella di debug.
- Modalita' risposta configurabili:
- `always-ack`;
- `always-nack`;
- `alternate`;
- `random`.
- Il client deve poter inviare a un server su altro computer tramite IP/porta configurabili.
- Il loop video/navigazione non deve fare la POST direttamente: invio tramite worker asincrono.
- La sequenza logica rimane:
- stop;
- scatta etichetta;
- invia WMS;
- attende risposta o timeout;
- riparte.
- L'attesa della risposta deve essere gestita senza congelare brutalmente il rendering.
## Configurazione prevista lato navigazione
Da aggiungere/validare in `flywms_navigation.ini`:
```ini
wms_enabled = false
wms_server_url = http://127.0.0.1:8088/api/v1/navigation-snapshot
wms_client_id = flywms-demo-01
wms_timeout_sec = 2.0
wms_queue_max_size = 8
wms_send_gaylord_debug = true
```
Per server remoto:
```ini
wms_server_url = http://192.168.1.50:8088/api/v1/navigation-snapshot
```
## Configurazione prevista lato server
Da implementare in `flywms_wms_server.py`:
```text
host = 0.0.0.0
port = 8088
fake_ack_mode = always-ack
fake_processing_sec = 0.5
received_dir = wms_received
```
## Protocollo previsto
Endpoint:
```text
POST /api/v1/navigation-snapshot
```
Payload multipart:
- `metadata`: JSON;
- `label_image`: JPEG crop etichetta;
- `gaylord_image`: JPEG opzionale/debug.
Risposta:
```json
{
"status": "ACK",
"request_id": "...",
"message": "codice valido su WMS",
"fake_ocr_text": "DEMO-0001",
"processing_ms": 120
}
```
Oppure:
```json
{
"status": "NACK",
"request_id": "...",
"message": "codice non riconosciuto",
"fake_ocr_text": "",
"processing_ms": 120
}
```
## Nota operativa
Al momento della creazione di questa milestone erano gia' stati installati i pacchetti FastAPI e iniziati i primi campi/config del client WMS. La milestone serve a fissare la specifica prima di proseguire con il completamento.

View File

@@ -0,0 +1,132 @@
# Aggiornamento 2026-05-16 11:12
## Obiettivo
Completare la prima implementazione reale client-server tra `flywms_navigation.py` e un server WMS demo.
## Implementazione
### Client navigazione
In `flywms_navigation.py` sono stati aggiunti:
- `WmsSnapshotJob`;
- `WmsResult`;
- `WmsAsyncClient`;
- creazione multipart HTTP senza base64;
- worker thread con coda non bloccante;
- invio del crop etichetta e, se configurato, del frame/debug gaylord;
- attesa di risposta WMS dopo il ritorno simulato al centro gaylord;
- gestione `ACK`, `NACK`, `ERROR`, `TIMEOUT`.
Il loop video non esegue direttamente la POST: accoda il job e il worker invia in background.
### Server WMS demo
Aggiunto `flywms_wms_server.py` basato su FastAPI.
Endpoint:
```text
POST /api/v1/navigation-snapshot
GET /health
```
Payload multipart:
- `metadata`: JSON;
- `label_image`: JPEG crop etichetta;
- `gaylord_image`: JPEG opzionale/debug.
Il server salva ogni richiesta in:
```text
wms_received/
```
oppure nella directory passata con `--received-dir`.
Modalita risposta:
- `always-ack`;
- `always-nack`;
- `alternate`;
- `random`.
### Configurazione
Parametri aggiunti in `flywms_navigation.ini`:
```ini
wms_enabled = false
wms_server_url = http://127.0.0.1:8088/api/v1/navigation-snapshot
wms_client_id = flywms-demo-01
wms_timeout_sec = 2.0
wms_queue_max_size = 8
wms_send_gaylord_debug = true
wms_server_host = 0.0.0.0
wms_server_port = 8088
wms_received_dir = wms_received
wms_fake_ack_mode = always-ack
wms_fake_processing_sec = 0.5
```
Aggiornato `.gitignore`:
```text
wms_received*/
```
## Test end-to-end
Server avviato in job PowerShell locale su:
```text
http://127.0.0.1:8088
```
Health check:
```json
{"status":"ok"}
```
Client headless con snapshot forzato:
```powershell
python flywms_navigation.py --no-display --max-frames 6 --snapshot-line-tolerance-ratio 0.20 --snapshot-output-dir navigate_snapshots_test --preview-fps 0 --yolo-fps 0 --label-move-sec 0 --label-stabilization-sec 0 --label-return-sec 0 --remote-ack-timeout-sec 5 --wms-enabled --wms-server-url http://127.0.0.1:8088/api/v1/navigation-snapshot
```
Risultato client:
```text
[WMS] job accodato request_id=77cba060-1433-43f3-a387-3811e6734907 snapshot=1
[WMS] risposta request_id=77cba060-1433-43f3-a387-3811e6734907 status=ACK message=codice valido su WMS
[WMS] ACK request_id=77cba060-1433-43f3-a387-3811e6734907 snapshot=1 codice valido su WMS
[CMD] RIPARTI_DESTRA
```
Risultati salvati dal server:
```text
wms_received_test/000001_77cba060-1433-43f3-a387-3811e6734907/metadata.json
wms_received_test/000001_77cba060-1433-43f3-a387-3811e6734907/snapshot_0001_track_002_frame.jpg
wms_received_test/000001_77cba060-1433-43f3-a387-3811e6734907/snapshot_0001_track_002_label_payload.jpg
```
Metadati ricevuti includono:
- `request_id`;
- `client_id`;
- `snapshot_id`;
- `simulated_position`;
- `track_id`;
- `gaylord_bbox`;
- `label_bbox`;
- `movement_vector_px`.
## Note
- La sequenza fisica simulata resta: movimento verso etichetta, stabilizzazione, scatto, ritorno al gaylord.
- L'invio WMS parte dopo lo scatto etichetta e puo' procedere mentre viene simulato il ritorno.
- La ripartenza viene comandata solo dopo risposta WMS o timeout.

View File

@@ -0,0 +1,49 @@
# Aggiornamento 2026-05-16 11:29
## Bugfix
Corretto crash all'avvio di `flywms_navigation.py --wms-enabled`.
Errore:
```text
AttributeError: 'Namespace' object has no attribute 'window_layout_enabled'
```
## Causa
I parametri layout finestre erano presenti in `flywms_navigation.ini` e nei default di `load_navigation_config`, ma mancavano in `parse_args()`.
## Correzione
Aggiunti argomenti CLI:
```text
--window-layout-enabled
--navigate-window
--commands-window
--snapshot-window
--label-window
```
## Verifiche
Compilazione:
```powershell
python -m py_compile flywms_navigation.py flywms_navigation_gui.py flywms_wms_server.py
```
Parsing argomenti:
```text
True 20,40,1100,620 1140,40,760,520 1140,590,520,360 1140,980,520,260
```
Run headless:
```powershell
python flywms_navigation.py --no-display --max-frames 1 --wms-enabled
```
Risultato: avvio e chiusura corretti senza crash.

View File

@@ -0,0 +1,62 @@
# Aggiornamento 2026-05-16 12:04
## Milestone prima implementazione OCR lato WMS server
## Obiettivo
Evolvere `flywms_wms_server.py` da server demo ACK/NACK a server WMS demo con pipeline remota:
1. riceve crop etichetta e metadati dal client navigazione;
2. mostra l'immagine ricevuta in una finestra server;
3. esegue OCR sul crop etichetta;
4. prepara payload WMS finale;
5. salva payload WMS su disco;
6. simula verifica WMS con risposta `ACK`/`NACK`;
7. risponde realmente al client.
## Decisioni concordate
- Il codice OCR deve idealmente essere il codice numerico reale sull'etichetta.
- Se OCR reale non e' disponibile o non legge nulla, usare fallback demo configurabile.
- Il server deve rispondere realmente al client.
- Il client, per ora, continua a mostrare la risposta ma mantiene la logica di attesa demo gia' configurata.
- In futuro il tempo di attesa verra' spostato completamente lato server e il client si adeguera' alla risposta.
- La finestra payload server mostra solo l'ultimo gaylord ricevuto.
- Il server salva anche il payload WMS finale in JSON, oltre al metadata originale.
- L'esito `ACK/NACK` resta governato da `wms_fake_ack_mode`.
## Finestre server previste
- `wms immagine ricevuta`: crop etichetta ricevuto dal client.
- `wms payload`: ultimo payload WMS con informazioni utili:
- request id;
- client id / drone;
- snapshot id;
- posizione;
- codice OCR;
- esito WMS;
- data/ora ricezione;
- track id;
- bbox etichetta;
- vettore movimento.
## Configurazione prevista
Da aggiungere in `flywms_navigation.ini`:
```ini
wms_ui_enabled = true
wms_ocr_enabled = true
wms_ocr_mode = auto
wms_fake_ocr_prefix = UDC
wms_fake_ocr_delay_sec = 0.2
wms_fake_wms_delay_sec = 1.0
wms_operator = udrone
wms_site = demo-magazzino
```
## Note tecniche
La GUI OpenCV del server non deve essere aggiornata direttamente dall'handler HTTP. L'handler aggiorna stato condiviso thread-safe; un thread UI separato aggiorna le finestre OpenCV.
L'OCR reale verra' implementato cercando prima le opzioni gia' disponibili nell'ambiente/progetto. Se non disponibili, il server resta funzionante con fallback demo e lo segnala nel payload.

View File

@@ -0,0 +1,116 @@
# Aggiornamento 2026-05-16 12:08
## Obiettivo
Implementare lato `flywms_wms_server.py` la prima pipeline WMS/OCR demo con finestre server.
## Implementazione server
Il server ora:
- riceve crop etichetta e metadati;
- salva `metadata.json`;
- esegue OCR sul crop etichetta;
- salva `wms_payload.json`;
- aggiorna una finestra OpenCV con l'immagine ricevuta;
- aggiorna una finestra OpenCV con il payload WMS dell'ultimo gaylord;
- risponde al client con `ACK/NACK` e codice OCR.
## OCR
Motore implementato:
- EasyOCR, se disponibile;
- fallback demo se OCR non legge nulla o se viene avviato con `--no-ocr`.
Sul crop di test, EasyOCR e' disponibile ma non ha letto testo utile; e' stato usato fallback:
```text
UDC-0001
```
Il payload indica esplicitamente:
```json
"ocr_backend": "fake",
"ocr_fallback_used": true
```
oppure `easyocr-fallback` quando EasyOCR gira ma non trova testo.
## Configurazione aggiunta
In `flywms_navigation.ini`:
```ini
wms_ui_enabled = true
wms_ocr_enabled = true
wms_ocr_mode = easyocr
wms_fake_ocr_prefix = UDC
wms_fake_ocr_delay_sec = 0.2
wms_operator = udrone
wms_site = demo-magazzino
```
Il server supporta anche:
```powershell
--no-ui
--no-ocr
```
per test headless.
## Client
Il client navigazione ora mostra anche il codice OCR ricevuto:
```text
OCR_CODICE UDC-0001
```
La risposta server include nel messaggio:
```text
codice valido su WMS: UDC-0001
```
## Verifiche
Compilazione:
```powershell
python -m py_compile flywms_wms_server.py flywms_navigation.py flywms_navigation_gui.py
```
Test OCR diretto sul crop etichetta:
```text
OcrServerResult(text='UDC-0001', raw_text='', confidence=0.0, backend='easyocr-fallback', fallback_used=True)
```
Test end-to-end server/client con server headless:
```powershell
python flywms_wms_server.py --host 127.0.0.1 --port 8088 --received-dir wms_received_test --fake-processing-sec 0.1 --fake-ocr-delay-sec 0 --no-ui --no-ocr
```
Client:
```powershell
python flywms_navigation.py --no-display --max-frames 6 --snapshot-line-tolerance-ratio 0.20 --snapshot-output-dir navigate_snapshots_test --preview-fps 0 --yolo-fps 0 --label-move-sec 0 --label-stabilization-sec 0 --label-return-sec 0 --remote-ack-timeout-sec 5 --wms-enabled --wms-server-url http://127.0.0.1:8088/api/v1/navigation-snapshot
```
Risultato:
```text
[WMS] risposta request_id=... status=ACK message=codice valido su WMS: UDC-0001
[WMS] ACK request_id=... snapshot=1 codice valido su WMS: UDC-0001
[CMD] RIPARTI_DESTRA
```
## Note
- La finestra server mostra l'ultimo payload ricevuto.
- Il server salva `wms_payload.json` oltre a `metadata.json`.
- L'esito `ACK/NACK` resta governato da `wms_fake_ack_mode`.

View File

@@ -0,0 +1,43 @@
# Aggiornamento 2026-05-16 12:10
## Obiettivo
Evitare falsi codici UDC quando OCR non determina davvero il testo.
## Decisione
Il primo gaylord ha etichetta tagliata. In quel caso il server non deve inventare un codice come `UDC-0001`.
Quando OCR fallisce o non legge testo utile, il codice restituito deve essere:
```text
udc non determinato
```
## Modifiche
In `flywms_wms_server.py`:
- cambiato fallback OCR;
- aggiunto campo configurabile `undetermined_code_text`;
- il payload mantiene `ocr_fallback_used = true`.
In `flywms_navigation.ini`:
```ini
wms_undetermined_code_text = udc non determinato
```
## Verifiche
Compilazione:
```powershell
python -m py_compile flywms_wms_server.py flywms_navigation.py flywms_navigation_gui.py
```
Test OCR sul crop del primo gaylord:
```text
OcrServerResult(text='udc non determinato', raw_text='', confidence=0.0, backend='easyocr-fallback', fallback_used=True)
```

View File

@@ -0,0 +1,188 @@
# Aggiornamento 2026-05-16 13:01
## Salvataggio discussione prima del riavvio PC
Questo file riassume lo stato della discussione e del lavoro fatto finora, per evitare perdita di contesto in caso di crash o riavvio.
## Stato generale
Il progetto FlyWMS sta evolvendo da una demo locale di navigazione YOLO/OpenCV verso una pipeline distribuita:
- `flywms_navigation.py`: simulatore drone/navigazione;
- `flywms_wms_server.py`: server WMS demo;
- comunicazione reale HTTP multipart tra client navigazione e server WMS;
- crop etichetta come payload principale per OCR/WMS;
- gaylord come ancora semantica per associare etichetta e posizione.
## Decisioni importanti prese
- Il gaylord resta l'oggetto di riferimento: serve a sapere a quale UDC/posizione appartiene l'etichetta.
- L'OCR deve lavorare sul crop dell'etichetta, non sul crop del gaylord intero.
- L'etichetta valida deve avere bbox contenuto nel bbox del gaylord centrato.
- Detection etichetta fuori dal gaylord vengono ignorate.
- Se il gaylord e' centrato ma non viene trovata un'etichetta contenuta, si segnala `ETICHETTA_NON_TROVATA` e si attende un frame successivo.
- Il movimento drone verso etichetta e ritorno al centro gaylord e' simulato con vettore pixel `dx/dy`.
- La freccia nera bidirezionale rappresenta il movimento dal centro gaylord al centro etichetta.
- La finestra comandi mostra anche grandi delta:
- `DX ORIZZONTALE`;
- `DY VERTICALE`.
- Le finestre OpenCV client sono configurabili da INI.
- Il server WMS non era inizialmente dotato di finestre; poi e' stato deciso di aggiungere finestre server demo.
- Il server WMS deve salvare immagini e payload ricevuti.
- L'esito `ACK/NACK` per ora resta configurabile e simulato.
- Il codice OCR deve essere reale quando possibile; se OCR non determina il codice, non bisogna inventare un valore.
- Per il primo gaylord, avendo etichetta tagliata, il codice deve essere:
```text
udc non determinato
```
## File principali modificati o aggiunti
- `flywms_navigation.py`
- tracking gaylord;
- associazione etichetta contenuta nel bbox gaylord;
- crop etichetta;
- freccia movimento;
- finestre OpenCV;
- client WMS asincrono;
- invio HTTP multipart;
- gestione ACK/NACK/ERROR/TIMEOUT.
- `flywms_navigation.ini`
- parametri navigazione;
- parametri FPS preview/YOLO;
- parametri etichetta;
- parametri finestre OpenCV;
- parametri client/server WMS;
- parametri OCR/fallback.
- `flywms_wms_server.py`
- server FastAPI;
- endpoint `/api/v1/navigation-snapshot`;
- endpoint `/health`;
- salvataggio immagini e metadati;
- OCR EasyOCR con fallback;
- payload WMS finale;
- finestre OpenCV server;
- opzioni `--no-ui` e `--no-ocr`.
- `.gitignore`
- aggiunto `wms_received*/`.
## Milestone create nella sessione
- `aggiornamento-2026-05-16-09-03.md`
- `aggiornamento-2026-05-16-10-14.md`
- `aggiornamento-2026-05-16-10-37.md`
- `aggiornamento-2026-05-16-10-47.md`
- `aggiornamento-2026-05-16-10-49.md`
- `aggiornamento-2026-05-16-11-04.md`
- `aggiornamento-2026-05-16-11-12.md`
- `aggiornamento-2026-05-16-11-29.md`
- `aggiornamento-2026-05-16-12-04.md`
- `aggiornamento-2026-05-16-12-08.md`
- `aggiornamento-2026-05-16-12-10.md`
## Test eseguiti
Compilazione piu' volte:
```powershell
python -m py_compile flywms_navigation.py flywms_navigation_gui.py flywms_wms_server.py
```
Test client-server WMS locale:
```text
[WMS] job accodato request_id=...
[WMS] risposta request_id=... status=ACK message=codice valido su WMS: ...
[CMD] RIPARTI_DESTRA
```
Server ha salvato:
```text
metadata.json
wms_payload.json
snapshot_..._frame.jpg
snapshot_..._label_payload.jpg
```
OCR su primo crop etichetta:
```text
OcrServerResult(text='udc non determinato', raw_text='', confidence=0.0, backend='easyocr-fallback', fallback_used=True)
```
## Problema attuale prima del riavvio
Eseguendo:
```powershell
python flywms_navigation.py --wms-enabled
```
il programma sembra non partire. Dopo `CTRL+C`, lo stack trace mostra che era fermo durante l'import di Ultralytics/PyTorch:
```text
detector = UltralyticsDetector(args.weights, args.ultralytics_device)
from ultralytics import YOLO
import torch
KeyboardInterrupt
```
Questo indica che il blocco avviene prima dell'avvio della logica WMS e prima dell'apertura del video: il processo sta impiegando molto tempo a importare `torch`/`ultralytics` o e' rallentato da stato del PC/disco/ambiente Python.
Non sembra causato direttamente dal codice WMS, perche' lo stack trace si ferma in import PyTorch.
## Diagnosi da fare dopo riavvio
1. Verificare tempo import PyTorch/Ultralytics:
```powershell
python -c "import time; t=time.time(); import torch; print('torch', time.time()-t); t=time.time(); from ultralytics import YOLO; print('ultralytics', time.time()-t)"
```
2. Verificare avvio senza WMS:
```powershell
python flywms_navigation.py --no-display --max-frames 1
```
3. Verificare avvio con WMS ma senza finestre:
```powershell
python flywms_navigation.py --no-display --max-frames 1 --wms-enabled
```
4. Avviare server WMS:
```powershell
python flywms_wms_server.py --host 0.0.0.0 --port 8088
```
5. Avviare navigazione:
```powershell
python flywms_navigation.py --wms-enabled
```
## Stato git noto
Ultimo commit locale fatto:
```text
1186c3b configura fps preview e yolo
```
Il branch risultava avanti di 1 commit rispetto a `origin/master` prima delle modifiche successive.
Molte modifiche successive risultano non ancora committate. Prima di proseguire dopo il riavvio conviene fare:
```powershell
git status --short --branch
python -m py_compile flywms_navigation.py flywms_navigation_gui.py flywms_wms_server.py
```
e poi valutare commit/push.

View File

@@ -0,0 +1,26 @@
# Aggiornamento 2026-05-16 14:34
## Milestone test Tesseract OCR
## Obiettivo
Valutare Tesseract come alternativa piu' leggera a EasyOCR per leggere codici numerici sulle etichette.
## Motivazione
EasyOCR si e' rivelato fragile sui crop piccoli/tagliati e molto pesante in RAM/tempo di avvio. Per codici numerici semplici, Tesseract con whitelist `0123456789` potrebbe essere piu' controllabile.
## Strategia
- Verificare se `tesseract.exe` e' installato.
- Se manca, installare/abilitare Tesseract e il package Python `pytesseract`.
- Aggiungere supporto server a `wms_ocr_mode = tesseract`.
- Usare OCR numerico con:
- preprocess OpenCV;
- whitelist cifre;
- `--psm 7` o simile;
- fallback `udc non determinato` se non legge un codice valido.
## Nota
Tesseract va considerato un test, non ancora la soluzione definitiva. Se confonde cifre o produce risultati instabili, dovra' restituire `udc non determinato`.

View File

@@ -0,0 +1,75 @@
# Aggiornamento 2026-05-16 17:18
## Test supporto Tesseract
## Stato
E' stato installato il wrapper Python:
```powershell
python -m pip install pytesseract
```
Sul PC pero' non risulta installato il binario:
```text
tesseract.exe
```
`where tesseract` non trova nulla.
## Modifiche implementate
In `flywms_wms_server.py` e' stato aggiunto supporto opzionale a:
```text
wms_ocr_mode = tesseract
```
Il server ora:
- usa `pytesseract` se disponibile;
- usa whitelist numerica `0123456789`;
- prova piu' preprocess OpenCV;
- richiede consenso tra almeno due varianti;
- se Tesseract non e' installato o il risultato e' ambiguo, restituisce `udc non determinato`.
E' stato aggiunto un controllo preventivo:
- se `wms_tesseract_cmd` e' configurato ma il file non esiste, fallback immediato;
- se `tesseract` non e' nel PATH, fallback immediato.
Questo evita che `pytesseract` resti appeso cercando un binario assente.
## Verifica eseguita
Sul crop:
```text
navigate_snapshots/snapshot_0002_track_003_label_payload.jpg
```
con OCR mode `tesseract`, senza tesseract installato:
```text
Tesseract fallback: tesseract.exe non trovato nel PATH
OcrServerResult(text='udc non determinato', raw_text='', confidence=0.0, backend='tesseract-missing', fallback_used=True)
```
## Nota
Il file `flywms_navigation.ini` al momento non e' stato aggiornato con `wms_tesseract_cmd` perche' il patch tool non e' riuscito a scriverlo in questa fase. I default del server contengono gia' `wms_tesseract_cmd = ""`, quindi il codice resta funzionante. Va aggiornato l'INI appena il file non risulta piu' bloccato.
## Prossimo passo
Installare Tesseract per Windows e poi testare:
```powershell
python flywms_wms_server.py --ocr-mode tesseract
```
Se `tesseract.exe` non e' nel PATH, avviare con:
```powershell
python flywms_wms_server.py --ocr-mode tesseract --tesseract-cmd "C:\Program Files\Tesseract-OCR\tesseract.exe"
```

View File

@@ -0,0 +1,84 @@
# Aggiornamento 2026-05-16 17:19
## Milestone OCR alternativo
## Stato corrente
EasyOCR e' stato giudicato troppo fragile e troppo pesante per la demo attuale:
- confonde cifre critiche, ad esempio `8` e `2`;
- puo' consumare molta RAM;
- puo' rallentare o bloccare i test;
- non e' adatto a restituire codici WMS se la confidenza e' bassa.
La regola semantica decisa e':
```text
meglio "udc non determinato" che un codice sbagliato
```
## Decisione
Testare Tesseract come alternativa piu' leggera e piu' controllabile per OCR numerico.
La logica prevista:
- whitelist solo cifre `0123456789`;
- piu' preprocess OpenCV;
- consenso tra varianti;
- soglia di confidenza;
- fallback `udc non determinato` se il risultato e' assente o ambiguo.
## Implementazione fatta
In `flywms_wms_server.py`:
- aggiunto supporto a `ocr_mode = tesseract`;
- aggiunto uso opzionale di `pytesseract`;
- aggiunto controllo preventivo del binario `tesseract.exe`;
- aggiunti preprocess:
- grayscale;
- sharpen;
- Otsu threshold;
- adaptive threshold;
- Otsu invertito;
- aggiunta logica di consenso;
- se Tesseract manca, il server non resta appeso.
## Ambiente
Wrapper Python installato:
```text
pytesseract
```
Binario non trovato:
```text
tesseract.exe
```
Verifica:
```text
Tesseract fallback: tesseract.exe non trovato nel PATH
OcrServerResult(text='udc non determinato', raw_text='', confidence=0.0, backend='tesseract-missing', fallback_used=True)
```
## Prossimi passi
1. Installare Tesseract per Windows.
2. Verificare se `tesseract.exe` entra nel PATH.
3. Se non entra nel PATH, avviare il server con:
```powershell
python flywms_wms_server.py --ocr-mode tesseract --tesseract-cmd "C:\Program Files\Tesseract-OCR\tesseract.exe"
```
4. Testare sui crop etichetta gia' salvati.
5. Se resta fragile, valutare una mini rete neurale dedicata ai 10 caratteri numerici.
## Nota
Il file `flywms_navigation.ini` non e' ancora stato aggiornato con la voce `wms_tesseract_cmd` perche' in questa fase il patch tool non e' riuscito a scriverlo. Il server ha comunque default interni funzionanti.

View File

@@ -0,0 +1,69 @@
# Aggiornamento 2026-05-16 19:46
## Valutazione YOLO digits come OCR alternativo
Abbiamo analizzato il progetto `thawro/yolov8-digits-detection`:
- repository: https://github.com/thawro/yolov8-digits-detection
- modello ONNX disponibile: https://huggingface.co/spaces/thawro/yolov8-digits-detection/tree/main/models
L'idea e' usare YOLO non per trovare il gaylord o l'etichetta, ma per leggere le cifre dentro il crop etichetta:
1. il client `flywms_navigation.py` individua gaylord ed etichetta come oggi;
2. il server WMS riceve il crop dell'etichetta;
3. il server passa il crop a un detector YOLO digits;
4. il detector restituisce bbox e classe delle singole cifre;
5. il server ordina le cifre da sinistra a destra;
6. il codice UDC finale e' la concatenazione delle cifre riconosciute;
7. se la sequenza non e' valida, il server deve rispondere `udc non determinato`.
## Considerazioni tecniche
Il progetto dichiara un dataset basato su cifre manoscritte HWD+, ma il demo video mostra anche riconoscimento su caratteri stampati. Questo significa che non va escluso a priori: va provato sui nostri crop reali, perche' potrebbe generalizzare abbastanza bene sulle etichette del video.
Vantaggi:
- modello piccolo: `yolo.onnx` e' circa 12 MB;
- potenzialmente molto piu' leggero di EasyOCR;
- puo' evitare il caricamento di EasyOCR/PyTorch nel server WMS;
- architettura adatta al nostro caso, perche' il codice UDC e' composto da cifre;
- output interpretabile: bbox, confidenza e classe di ogni cifra.
Rischi:
- il dominio di training non e' identico alle nostre etichette industriali;
- alcune cifre critiche, ad esempio 8 e 2, potrebbero comunque essere confuse;
- serve una fase di validazione su immagini reali delle nostre etichette;
- la licenza del repository/modello deve essere verificata prima di un uso oltre la demo;
- il modello riconosce singole cifre, quindi dovremo implementare noi ordinamento, filtri e validazione.
## Proposta di integrazione
Non conviene importare tutto il progetto come package. Conviene invece:
- scaricare/posizionare il modello `yolo.onnx` in una cartella locale, ad esempio `models/yolo_digits/yolo.onnx`;
- aggiungere una modalita' OCR nel server:
```ini
wms_ocr_mode = yolo_digits
wms_yolo_digits_model = models/yolo_digits/yolo.onnx
wms_yolo_digits_conf = 0.35
wms_yolo_digits_expected_len = 6
```
- implementare nel server una classe tipo `YoloDigitsOcr`;
- usare `onnxruntime` oppure OpenCV DNN per l'inferenza;
- restituire sempre un risultato conservativo:
- codice numerico solo se la sequenza supera i controlli minimi;
- `udc non determinato` in caso di ambiguita', crop tagliato o confidenza insufficiente.
## Decisione pratica
La strada e' promettente e probabilmente piu' coerente del generico OCR classico. La prova corretta da fare e':
1. integrare il modello come modalita' sperimentale separata;
2. non sostituire ancora EasyOCR/Tesseract;
3. eseguire test sui crop etichetta salvati dal nostro server;
4. confrontare errori e tempi rispetto a EasyOCR e Tesseract;
5. decidere se basta il modello esistente oppure serve fine-tuning su nostre etichette.

View File

@@ -0,0 +1,25 @@
# Aggiornamento 2026-05-16 19:52
## Nuovo laboratorio YOLO OCR
E' stata creata la cartella separata:
```text
C:\devel\yolo-ocr
```
Scopo della cartella:
- lavorare in modo pulito sull'ipotesi YOLO digits OCR;
- evitare di sporcare subito `flywms_navigation.py` e `flywms_wms_server.py`;
- provare il modello `thawro/yolov8-digits-detection` sui crop reali delle etichette;
- misurare accuratezza e tempi di inferenza;
- preparare eventualmente una pipeline di addestramento/fine-tuning con immagini nostre;
- trasferire in FlyWMS solo una soluzione gia' validata.
Decisione tecnica:
- `C:\devel\flywms` resta il progetto principale;
- `C:\devel\yolo-ocr` diventa il laboratorio OCR isolato;
- le prossime operazioni relative a YOLO digits OCR dovranno usare `C:\devel\yolo-ocr` come directory di lavoro.

View File

@@ -0,0 +1,94 @@
# Aggiornamento 2026-05-16 20:12
## Primo test YOLO digits su crop etichetta
E' stato scaricato il progetto:
```text
C:\devel\yolo-ocr\external\yolov8-digits-detection
```
Sono state trovate 28 immagini di test in:
```text
C:\devel\yolo-ocr\immagini_test
```
Nota: la cartella indicata verbalmente come `C:\yolo-ocr\immagini_test` non esisteva; le immagini erano nella cartella corretta `C:\devel\yolo-ocr\immagini_test`.
## Runner creato
E' stato creato:
```text
C:\devel\yolo-ocr\tools\batch_yolo_digits.py
```
Il runner:
- carica i modelli ONNX del progetto;
- non installa PyTorch;
- evita `pytorch_lightning`, usato dal progetto solo per logging;
- esegue inferenza CPU;
- ordina le cifre rilevate da sinistra a destra;
- salva un CSV con codice, numero cifre, confidenza media e tempo;
- salva immagini annotate con bbox delle cifre;
- salva una tavola di controllo visivo.
## Risultato con soglia 0.25
Output:
```text
immagini=28
codici_numerici=24
load_sec=2.679
infer_ms_medio=52.7
conf=0.25
iou=0.7
```
Il modello e' veloce: circa 53 ms per crop su CPU.
Qualita':
- riconosce davvero alcune cifre stampate;
- quindi la pista YOLO digits e' concreta;
- tuttavia spesso riconosce solo una parte del codice;
- alcuni crop restituiscono 1-3 cifre invece del codice completo;
- il modello out-of-the-box non e' ancora sufficiente per lettura affidabile UDC.
## Risultato con soglia 0.10
Output:
```text
immagini=28
codici_numerici=27
load_sec=1.881
infer_ms_medio=61.5
conf=0.1
iou=0.7
```
La soglia piu' bassa fa trovare piu' cifre, ma introduce duplicati e classi sbagliate. Quindi il problema non si risolve solo abbassando la soglia.
## Conclusione
Il modello esistente e' utile come base sperimentale, ma non e' pronto da integrare nel WMS come OCR affidabile.
La cosa positiva e' che:
- il modello e' leggero;
- gira senza PyTorch;
- riconosce cifre stampate almeno parzialmente;
- la velocita' e' adeguata;
- puo' diventare molto interessante con fine-tuning su immagini reali delle nostre etichette.
Prossimo passo consigliato:
1. migliorare preprocessing e crop locale, cercando di isolare meglio la riga numerica;
2. creare una piccola tabella ground truth per le 28 immagini;
3. misurare accuracy reale cifra per cifra e codice intero;
4. valutare fine-tuning del modello con immagini nostre annotate.

View File

@@ -0,0 +1,29 @@
# Aggiornamento 2026-05-17 09:30
## Decisione: passare al fine-tuning YOLO digits
Dopo il primo test del modello `thawro/yolov8-digits-detection` sui crop reali, la direzione scelta e' passare direttamente al fine-tuning.
Motivazione:
- il modello base riconosce alcune cifre stampate;
- la velocita' CPU e' adeguata;
- l'accuratezza out-of-the-box non e' sufficiente;
- i nostri codici UDC hanno dominio visivo specifico: font, stampa, prospettiva, luce, sfocatura, tagli parziali;
- un OCR generico resta fragile, mentre un detector addestrato sulle nostre etichette puo' migliorare molto.
Percorso previsto:
1. raccogliere crop reali delle etichette;
2. annotare ogni cifra con bbox e classe `0..9`;
3. creare dataset YOLO con split `train/val/test`;
4. fare training/fine-tuning YOLOv8 detection;
5. esportare il modello in ONNX;
6. testare il modello nel laboratorio `C:\devel\yolo-ocr`;
7. integrare nel server WMS solo quando il risultato e' affidabile.
Decisione architetturale:
- il fine-tuning resta nel laboratorio `C:\devel\yolo-ocr`;
- FlyWMS non viene toccato finche' il modello OCR non e' validato.

View File

@@ -0,0 +1,53 @@
# Aggiornamento 2026-05-17 09:39
## Preparazione dataset per fine-tuning YOLO OCR
E' stata creata la struttura dataset nel laboratorio:
```text
C:\devel\yolo-ocr\dataset
```
Cartelle principali:
```text
dataset\images\to_annotate
dataset\images\train
dataset\images\val
dataset\images\test
dataset\labels\to_annotate
dataset\labels\train
dataset\labels\val
dataset\labels\test
dataset\annotations_raw
dataset\manifests
```
Sono state copiate 28 immagini in:
```text
C:\devel\yolo-ocr\dataset\images\to_annotate
```
E' stato creato:
```text
C:\devel\yolo-ocr\dataset\data.yaml
C:\devel\yolo-ocr\dataset\README_ANNOTAZIONE.md
```
Regola di annotazione:
- annotare ogni singola cifra del codice UDC;
- usare classi `0..9`;
- non annotare l'intera etichetta;
- non annotare testi, righe o loghi;
- non annotare cifre troppo ambigue.
Valutazione quantita' dati:
- le 28 immagini attuali sono poche;
- possono bastare per una demo controllata o per un primo proof-of-concept;
- non bastano per una soluzione robusta da campo;
- per robustezza reale servira' arrivare a centinaia di crop, includendo casi difficili.

View File

@@ -0,0 +1,18 @@
# Aggiornamento 2026-05-17 10:08
## Configurazione Label Studio per cifre UDC
E' stato creato il file XML di configurazione Label Studio:
```text
C:\devel\yolo-ocr\dataset\label_studio_digits_config.xml
```
Contiene dieci classi rettangolari:
```text
0 1 2 3 4 5 6 7 8 9
```
Questa configurazione serve ad annotare ogni singola cifra del codice UDC con un bbox rettangolare standard, compatibile con export YOLO detection.

View File

@@ -0,0 +1,20 @@
# Aggiornamento 2026-05-17 10:37
## Avvio fase fine-tuning YOLO OCR
L'utente ha completato una prima sessione di tagging in Label Studio e ha chiesto di usare le immagini annotate per lanciare il fine-tuning.
Verifiche iniziali:
- le immagini sono presenti in `C:\devel\yolo-ocr\dataset\images\to_annotate`;
- non risultano ancora file YOLO `.txt` in `dataset\labels\to_annotate`;
- non risultano export Label Studio nuovi nella cartella export standard;
- probabilmente le annotazioni sono ancora nel database SQLite interno di Label Studio.
Prossimo passo:
- leggere le annotazioni dal database Label Studio;
- convertirle in formato YOLO;
- creare split `train/val/test`;
- avviare un primo training dimostrativo.

View File

@@ -0,0 +1,82 @@
# Aggiornamento 2026-05-17 11:21
## Fine-tuning YOLO OCR completato
Sono state convertite le annotazioni Label Studio del progetto:
```text
project_id=4
nome=addestramento ocr
```
Dataset YOLO creato:
```text
C:\devel\yolo-ocr\dataset
```
Split:
```text
images=26
boxes=156
train=19
val=5
test=2
```
Training lanciato inizialmente su GPU, ma fallito per incompatibilita':
```text
GPU: NVIDIA GeForce GTX 1050
problema: PyTorch installato non supporta compute capability sm_61
errore: CUDA error: no kernel image is available for execution on the device
```
Training rilanciato e completato su CPU:
```powershell
yolo detect train data=C:\devel\yolo-ocr\dataset\data.yaml model=yolov8n.pt epochs=60 imgsz=640 batch=4 workers=0 device=cpu project=C:\devel\yolo-ocr\training_runs name=digits_yolov8n_demo_cpu patience=15 seed=42
```
Risultati finali su validation:
```text
precision: 0.603
recall: 0.556
mAP50: 0.753
mAP50-95: 0.582
```
Risultati su test:
```text
images: 2
boxes: 12
precision: 0.535
recall: 0.771
mAP50: 0.752
mAP50-95: 0.488
```
Artefatti stabili copiati in:
```text
C:\devel\yolo-ocr\models\udc_digits_yolov8n_demo.pt
C:\devel\yolo-ocr\models\udc_digits_yolov8n_demo.onnx
```
Riepilogo tecnico:
```text
C:\devel\yolo-ocr\TRAINING_SUMMARY.md
```
Interpretazione:
- il fine-tuning funziona;
- il modello addestrato e' molto piu' promettente del modello generico provato ieri;
- l'export ONNX funziona;
- il dataset e' pero' ancora troppo piccolo;
- la metrica mAP e' incoraggiante, ma ora serve misurare la lettura del codice intero, perche' per il WMS una sola cifra sbagliata rende errato tutto il codice UDC.

View File

@@ -0,0 +1,46 @@
# Aggiornamento 2026-05-17 14:48
## Installazione RTX 3050 e verifica stack GPU
Nuova GPU rilevata:
```text
NVIDIA GeForce RTX 3050
VRAM: 6144 MiB
Driver: 596.36
CUDA driver runtime: 13.2
```
PyTorch vede correttamente la GPU:
```text
torch: 2.11.0+cu128
cuda_available: True
device_count: 1
device_name: NVIDIA GeForce RTX 3050
capability: (8, 6)
```
Questo risolve il problema avuto con la GTX 1050, che non era compatibile con il build PyTorch CUDA installato.
Modifica effettuata:
```text
flywms_navigation.ini
ultralytics_device = 0
```
Situazione stack:
- PyTorch/Ultralytics: GPU utilizzabile;
- OpenCV: attualmente `GUI: NONE`, quindi va ripristinito `opencv-python` non headless;
- ONNX Runtime: attualmente solo CPU provider, ma per il modello OCR piccolo puo' bastare CPU;
- Label Studio ha installato `opencv-python-headless`, causando la perdita delle finestre OpenCV.
Prossime azioni consigliate:
1. ripristinare OpenCV GUI;
2. fare benchmark FlyWMS con `ultralytics_device = 0`;
3. rilanciare training/fine-tuning OCR su GPU se servono nuovi modelli;
4. valutare ONNX Runtime GPU solo se l'OCR ONNX diventa un collo di bottiglia.

View File

@@ -0,0 +1,52 @@
# Aggiornamento 2026-05-17 14:51
## Diagnosi rallentamento avvio programmi
Sintomo:
- i programmi partono molto lentamente;
- non compaiono errori evidenti;
- anche il comando minimale `python -c "print('ok')"` impiega circa 4 secondi;
- import pesanti come `torch` restano molto lenti.
Indizi trovati:
```text
git-lfs attivo in piu' processi
MsMpEng / Microsoft Defender attivo
pCloud attivo
SearchIndexer attivo
testhd2.mp4 modificato come file LFS
```
Processi `git-lfs` osservati:
```text
git-lfs 10916
git-lfs 13380
git-lfs 15520
git-lfs 17104
```
Ipotesi piu' probabile:
- Git LFS sta lavorando/scansionando file grandi, in particolare video;
- Defender e/o pCloud stanno scansionando o sincronizzando gli stessi file;
- questo crea pressione su disco/I/O;
- l'avvio di Python, PyTorch, Label Studio e altri programmi diventa molto lento anche senza errori espliciti.
Altri punti:
- alcuni comandi diagnostici Windows richiedono privilegi amministrativi e sono stati negati;
- `opencv-python-headless` resta installato e OpenCV risulta `GUI: NONE`;
- questo problema OpenCV e' separato dal rallentamento generale, ma va sistemato.
Azioni consigliate:
1. fermare i processi `git-lfs` appesi;
2. evitare che pCloud sincronizzi `C:\devel`;
3. aggiungere esclusioni Defender per `C:\devel` e per le cartelle Python/progetto;
4. gestire i video grandi fuori dal repository o confermare intenzionalmente le modifiche LFS;
5. ripristinare OpenCV GUI;
6. creare ambienti Python separati invece di continuare a installare tutto nel Python globale.

View File

@@ -0,0 +1,75 @@
# Aggiornamento 2026-05-17 20:36
## Test accuratezza codice intero YOLO OCR
E' stato creato il test:
```text
C:\devel\yolo-ocr\tools\evaluate_code_accuracy.py
```
Scopo:
- usare il modello fine-tuned;
- rilevare le cifre su ogni crop;
- ordinare i bbox da sinistra a destra;
- ricostruire il codice UDC;
- confrontarlo con la ground truth derivata dalle label YOLO;
- produrre CSV e immagini annotate.
Primo test:
```powershell
python tools\evaluate_code_accuracy.py --split test --device 0 --conf 0.25 --iou 0.50 --output outputs\code_eval_test
python tools\evaluate_code_accuracy.py --split all --device 0 --conf 0.25 --iou 0.50 --output outputs\code_eval_all
```
Risultati:
```text
split=test
images=2
code_ok=0
code_accuracy=0.0000
char_accuracy=0.5385
```
```text
split=all
images=26
code_ok=0
code_accuracy=0.0000
char_accuracy=0.5389
```
Esempi:
```text
expected=187184 predicted=1871182
expected=182368 predicted=18288
```
Sono state provate anche soglie diverse:
```text
conf=0.40 iou=0.40 -> code_accuracy=0.0000 char_accuracy=0.5316
conf=0.50 iou=0.40 -> code_accuracy=0.0385 char_accuracy=0.4872
conf=0.60 iou=0.40 -> code_accuracy=0.0000 char_accuracy=0.4487
```
Conclusione:
- il fine-tuning ha migliorato molto la capacita' di rilevare bbox cifra rispetto al modello generico;
- pero' la lettura del codice intero non e' ancora affidabile;
- gli errori principali sono duplicazioni, cifre mancanti e confusione nelle ultime cifre;
- il prefisso `182` / `187` viene spesso agganciato, ma non basta per il WMS;
- non conviene integrare ancora il modello in FlyWMS come OCR operativo.
Prossimi passi consigliati:
1. aumentare dataset annotato;
2. introdurre post-processing specifico per codice a 6 cifre;
3. valutare crop piu' stretto della sola riga numerica;
4. usare metriche di codice intero come criterio principale;
5. integrare in FlyWMS solo come modalita' sperimentale quando il codice intero migliora.

View File

@@ -0,0 +1,166 @@
# Aggiornamento 2026-05-17 20:57
## Obiettivo
Provare la strada del dataset aumentato per il riconoscimento OCR numerico via YOLO, senza integrare ancora nulla in `flywms`.
## Dataset aumentato
Creato lo script:
```text
C:\devel\yolo-ocr\tools\augment_yolo_dataset.py
```
Generato il dataset:
```text
C:\devel\yolo-ocr\dataset_augmented
```
Composizione:
```text
train: 247 immagini, 1482 bbox
val: 5 immagini, 30 bbox
test: 2 immagini, 12 bbox
```
Nota importante: l'aumento dati e' applicato solo al train. Validation e test restano immagini originali, quindi sono utili per misurare se il modello generalizza.
Augmentation applicate:
- piccole rotazioni;
- piccole traslazioni;
- scala leggera;
- contrasto/luminosita';
- CLAHE;
- sharpening;
- blur leggero;
- rumore;
- compressione JPEG simulata.
## Training GPU
Addestramento eseguito sulla RTX 3050:
```powershell
yolo detect train data=C:\devel\yolo-ocr\dataset_augmented\data.yaml model=yolov8n.pt epochs=120 imgsz=640 batch=16 workers=0 device=0 project=C:\devel\yolo-ocr\training_runs name=digits_yolov8n_aug_gpu patience=30 seed=42
```
Il training e' terminato con early stopping:
```text
Best epoch: 11
Epoch completate: 41
Durata: circa 0.098 ore
```
Metriche YOLO finali su validation:
```text
precision: 0.826
recall: 0.873
mAP50: 0.990
mAP50-95: 0.745
```
Velocita' indicativa:
```text
preprocess: 0.6 ms
inference: 7.7 ms
postprocess: 4.1 ms
```
## Artefatti
Peso PyTorch:
```text
C:\devel\yolo-ocr\models\udc_digits_yolov8n_aug.pt
```
Export ONNX:
```text
C:\devel\yolo-ocr\models\udc_digits_yolov8n_aug.onnx
```
Run originale:
```text
C:\devel\yolo-ocr\training_runs\digits_yolov8n_aug_gpu
```
## Valutazione codice intero
Con soglia standard:
```powershell
python C:\devel\yolo-ocr\tools\evaluate_code_accuracy.py --model C:\devel\yolo-ocr\training_runs\digits_yolov8n_aug_gpu\weights\best.pt --dataset C:\devel\yolo-ocr\dataset_augmented --split test --device 0 --conf 0.25 --iou 0.50 --output C:\devel\yolo-ocr\outputs\code_eval_aug_test
```
Risultato su test:
```text
images=2
code_ok=0
code_accuracy=0.0000
char_accuracy=0.3333
```
Il problema era una cifra mancante nei due codici:
```text
187184 -> 17184
182368 -> 18268
```
Abbassando la soglia a `conf=0.15`, sui 2 test il codice completo diventa corretto:
```text
conf=0.15, iou=0.35..0.65
code_accuracy=1.0000
char_accuracy=1.0000
```
Pero' sulla validation originale, sempre con `conf=0.15`, il risultato resta insufficiente:
```text
images=5
code_ok=1
code_accuracy=0.2000
char_accuracy=0.7812
```
Esempi validation:
```text
187184 -> 187184 OK
182430 -> 18243601 NO
182519 -> 182610 NO
182511 -> 182611 NO
182242 -> 18224 NO
```
## Conclusione
La strada del dataset aumentato e' utile: le metriche YOLO sugli oggetti sono migliorate molto e la GPU funziona bene.
Pero' il problema reale non e' ancora risolto, perche' noi non dobbiamo solo rilevare cifre: dobbiamo ricostruire un codice numerico affidabile. Il modello ora e' sensibile alla soglia di confidenza e produce ancora cifre duplicate, mancanti o confuse.
Non conviene integrare questo OCR in `flywms` in questa forma.
## Prossimo passo tecnico
Prima di altro training, serve migliorare il post-processing del codice:
- soglia bassa controllata;
- ordinamento sinistra-destra;
- clustering/merge dei bbox vicini;
- vincolo codice a 6 cifre;
- gestione duplicati;
- report per capire quali cifre sono davvero ambigue.
Solo dopo questa fase ha senso decidere se aumentare ancora il dataset o cambiare architettura.

View File

@@ -0,0 +1,196 @@
# Aggiornamento 2026-05-18 14:18
## Obiettivo
Avviare la pista PaddleOCR come alternativa OCR tarabile sui crop etichetta, lasciando per ora da parte YOLO caratteri e senza integrare nulla in `flywms`.
## Decisione tecnica
Per PaddleOCR non usiamo le annotazioni bbox cifra-per-cifra.
Usiamo invece l'etichetta intera:
```text
crop_etichetta.jpg<TAB>codice_completo
```
Questo e' piu' coerente con l'obiettivo demo: dato un crop dell'etichetta, ottenere direttamente il codice UDC.
## Dataset creato
Creato script:
```text
C:\devel\yolo-ocr\tools\prepare_paddleocr_dataset.py
```
Generata cartella:
```text
C:\devel\yolo-ocr\paddleocr-recognizer
```
Dataset:
```text
C:\devel\yolo-ocr\paddleocr-recognizer\dataset
```
Conteggi:
```text
train: 475 immagini
val: 5 immagini
test: 2 immagini
```
Il train contiene varianti aumentate dell'intera etichetta. Validation e test restano crop originali.
File prodotti:
```text
dataset\train_list.txt
dataset\val_list.txt
dataset\test_list.txt
digit_dict.txt
preprocessed_preview\
README.md
README_OPERATIVO.md
```
## Preprocessing/augmentation
Lo script genera varianti controllate:
- upscale a altezza standard;
- contrasto/luminosita';
- CLAHE;
- sharpening;
- blur leggero;
- rumore;
- compressione JPEG;
- rotazione leggera.
Le preview dei preprocessing per validation/test sono in:
```text
C:\devel\yolo-ocr\paddleocr-recognizer\preprocessed_preview
```
## Baseline multi-pass
Creato script:
```text
C:\devel\yolo-ocr\tools\paddleocr_multipass_eval.py
```
Scopo:
- provare piu' preprocessing sullo stesso crop;
- passare ogni variante a PaddleOCR;
- tenere solo le cifre;
- preferire codici della lunghezza attesa;
- salvare CSV e immagini varianti.
## Blocco tecnico
PaddleOCR e' installato:
```text
paddleocr 3.5.0
paddlex 3.5.2
paddlepaddle 3.3.1
paddlepaddle-gpu 3.3.1
```
Ma l'esecuzione fallisce prima dell'OCR:
```text
OSError: [WinError 127] Impossibile trovare la procedura specificata.
Error loading C:\Python313\Lib\site-packages\paddle\..\nvidia\cudnn\bin\cudnn_cnn64_9.dll or one of its dependencies.
```
Ho provato ad aggiungere esplicitamente le cartelle `nvidia\*\bin` al loader DLL Windows nello script, ma non basta.
Il problema sembra nello stack Paddle GPU/CUDA/cuDNN globale, non nel dataset.
## Ambiente isolato
Per non modificare lo stack globale, creato un virtualenv dedicato:
```text
C:\devel\yolo-ocr\.venv-paddle-cpu
```
Installati nel virtualenv:
```text
paddlepaddle==3.3.1
paddleocr==3.5.0
opencv-python-headless
```
In questo ambiente PaddleOCR parte correttamente e usa i modelli gia' presenti in cache:
```text
PP-OCRv5_server_det
en_PP-OCRv5_mobile_rec
```
## Correzione parser
L'API PaddleOCR 3.5 restituisce i risultati in chiavi:
```text
rec_texts
rec_scores
```
Lo script `paddleocr_multipass_eval.py` e' stato corretto per leggere questo formato.
## Baseline OCR PaddleOCR
Validation:
```text
images=5
ok=5
accuracy=1.0000
```
Test:
```text
images=2
ok=2
accuracy=1.0000
```
File risultati:
```text
C:\devel\yolo-ocr\paddleocr-recognizer\outputs\multipass_val_venv_fixed\multipass_results.csv
C:\devel\yolo-ocr\paddleocr-recognizer\outputs\multipass_test_venv_fixed\multipass_results.csv
```
Esempi:
```text
182430 -> 182430
182242 -> 182242
182519 -> 182519
182511 -> 182511
187184 -> 187184
182368 -> 182368
```
## Conclusione
La pista PaddleOCR e' molto piu' promettente del previsto per il demo.
Il dataset e gli script sono pronti, e la baseline standard con preprocessing multi-pass legge correttamente tutti i crop disponibili in validation/test.
Il campione e' pero' molto piccolo, quindi non e' ancora una prova di robustezza.
Per il prossimo passo non farei subito fine-tuning: integrerei prima nel server WMS una modalita' OCR sperimentale basata su PaddleOCR multi-pass, con fallback `UDC_NON_DETERMINATO` quando non c'e' un candidato numerico credibile.

View File

@@ -0,0 +1,115 @@
# Aggiornamento 2026-05-18 14:39
## Obiettivo
Migliorare la pista PaddleOCR prima di pensare all'integrazione in `flywms`.
## Modifiche
Aggiornato:
```text
C:\devel\yolo-ocr\tools\paddleocr_multipass_eval.py
```
Migliorie introdotte:
- resize su piu' altezze (`64,96,128`);
- variante con bordo bianco per evitare tagli ai caratteri;
- CLAHE;
- sharpening;
- threshold adattivo;
- Otsu;
- Otsu + close morfologico;
- denoise;
- scelta finale a consenso.
La scelta finale ora non dipende solo dal massimo score PaddleOCR.
Il punteggio considera:
- codice letto da piu' varianti;
- numero di varianti distinte che producono lo stesso codice;
- bonus se il codice ha la lunghezza attesa;
- penalita' per codici troppo corti o troppo lunghi.
## Risultati configurazione estesa
Validation:
```text
images=5
ok=5
accuracy=1.0000
```
Test:
```text
images=2
ok=2
accuracy=1.0000
```
Output:
```text
C:\devel\yolo-ocr\paddleocr-recognizer\outputs\multipass_val_consensus
C:\devel\yolo-ocr\paddleocr-recognizer\outputs\multipass_test_consensus
```
La lettura corretta e' sostenuta da molti voti, quindi non sembra casuale:
```text
182430 -> 19 voti
182242 -> 12 voti
182519 -> 20 voti
182511 -> 18 voti
187184 -> 18/19 voti
182368 -> 14 voti
```
## Risultati configurazione leggera
Testata anche la configurazione con sola altezza `96`, piu' adatta al server:
```powershell
--target-heights 96
```
Validation:
```text
images=5
ok=5
accuracy=1.0000
```
Test:
```text
images=2
ok=2
accuracy=1.0000
```
Output:
```text
C:\devel\yolo-ocr\paddleocr-recognizer\outputs\multipass_val_h96
C:\devel\yolo-ocr\paddleocr-recognizer\outputs\multipass_test_h96
```
## Conclusione
Possiamo migliorare ancora raccogliendo nuove immagini reali, ma sul materiale attuale PaddleOCR standard con preprocessing multi-pass e consenso e' gia' solido per una demo.
La configurazione leggera `--target-heights 96` e' la candidata migliore per la prima integrazione sperimentale nel server WMS.
L'integrazione dovra' comunque mantenere un fallback:
```text
UDC_NON_DETERMINATO
```
quando non c'e' un candidato a 6 cifre con consenso sufficiente.

View File

@@ -0,0 +1,166 @@
# Aggiornamento 2026-05-18 14:58
## Obiettivo
Integrare PaddleOCR nel server WMS demo, senza modificare la GUI e senza integrare ancora una interfaccia DearPyGUI.
## Implementazione
Creato worker persistente:
```text
C:\devel\flywms\flywms_paddleocr_worker.py
```
Il worker gira con il virtualenv isolato:
```text
C:\devel\yolo-ocr\.venv-paddle-cpu\Scripts\python.exe
```
Il server WMS resta nel suo ambiente Python attuale e comunica col worker via stdin/stdout JSON. Questo evita il problema del Paddle GPU globale:
```text
WinError 127 su cudnn_cnn64_9.dll
```
## Server WMS
Aggiornato:
```text
C:\devel\flywms\flywms_wms_server.py
```
Nuovo backend OCR:
```text
--ocr-mode paddleocr
```
Backend disponibili:
```text
paddleocr
easyocr
tesseract
fake
```
Il worker PaddleOCR resta caricato tra richieste successive, quindi non ricarica il modello a ogni snapshot.
## Parametri INI
Aggiornato:
```text
C:\devel\flywms\flywms_navigation.ini
```
Nuovi parametri:
```text
wms_ocr_mode = paddleocr
wms_paddle_python = C:\devel\yolo-ocr\.venv-paddle-cpu\Scripts\python.exe
wms_paddle_worker_script = C:\devel\flywms\flywms_paddleocr_worker.py
wms_paddle_target_heights = 96
wms_paddle_variant_set = fast
wms_paddle_expected_digits = 6
wms_paddle_min_votes = 2
wms_paddle_min_confidence = 0.70
```
Aggiornati anche i timeout, perche' PaddleOCR CPU richiede piu' di 2 secondi:
```text
remote_ack_timeout_sec = 10.0
wms_timeout_sec = 12.0
```
## Logica OCR
La configurazione `fast` prova 3 varianti:
```text
originale
CLAHE
CLAHE + sharpening
```
Il codice viene accettato solo se:
- e' numerico;
- ha 6 cifre;
- ha almeno 2 voti tra le varianti;
- supera la confidenza minima.
In caso contrario il server restituisce:
```text
udc non determinato
```
## Test eseguiti
Compilazione:
```text
python -m py_compile flywms_wms_server.py flywms_paddleocr_worker.py
```
Test worker diretto:
```text
input: snapshot_0019_track_123_label_payload_orig.jpg
output: 182368
votes: 3
backend: paddleocr
```
Test server locale senza GUI:
```powershell
python C:\devel\flywms\flywms_wms_server.py --no-ui --port 8098 --received-dir C:\devel\flywms\wms_received_test --ocr-mode paddleocr --paddle-variant-set fast --fake-processing-sec 0
```
Richiesta 1:
```text
ocr_text: 182368
ocr_backend: paddleocr
ocr_fallback_used: false
ocr_votes: 3
processing_ms: circa 8500 ms
```
Richiesta 2 con worker gia' caldo:
```text
ocr_text: 187184
ocr_backend: paddleocr
ocr_fallback_used: false
ocr_votes: 3
processing_ms: circa 3200 ms
```
## Nota prestazionale
Il primo snapshot paga il caricamento del worker PaddleOCR. Gli snapshot successivi sono piu' veloci ma restano nell'ordine di qualche secondo su CPU.
Per il demo va bene, ma il timeout del client deve restare piu' alto del vecchio valore da 2 secondi.
## Prossimo passo
Avviare il server:
```powershell
python flywms_wms_server.py
```
Poi avviare la navigazione:
```powershell
python flywms_navigation.py --wms-enabled
```
Verificare nella finestra comandi che compaia il codice OCR restituito dal WMS.

View File

@@ -0,0 +1,50 @@
# Aggiornamento 2026-05-18 15:28
## Problema
Avviando:
```powershell
python flywms_wms_server.py
```
il server partiva, ma il thread UI OpenCV falliva:
```text
cv2.error: The function is not implemented
```
Causa: nel Python globale e' installata una build OpenCV headless, quindi `cv2.namedWindow` non e' disponibile.
## Correzione
Aggiornato:
```text
C:\devel\flywms\flywms_wms_server.py
```
Ora `server_ui_loop()` intercetta l'errore `cv2.error` su `namedWindow`, logga il problema e disabilita solo la UI OpenCV del server.
FastAPI e PaddleOCR restano attivi.
## Verifica
Avvio di test:
```powershell
python C:\devel\flywms\flywms_wms_server.py --port 8099
```
Risultato:
```text
UI OpenCV disabilitata: highgui non disponibile
Uvicorn running on http://0.0.0.0:8099
```
Il server non si chiude piu' per l'errore della finestra OpenCV.
## Nota
Quando sistemeremo OpenCV non-headless o passeremo a DearPyGUI per il server, la UI potra' essere riabilitata senza cambiare la parte WMS/OCR.

View File

@@ -0,0 +1,107 @@
# Aggiornamento 2026-05-18 15:42
## Obiettivo
Ripristinare OpenCV con supporto CUDA, come nel setup precedente basato sulle wheel `cudawarped/opencv-python-cuda-wheels`.
## Situazione iniziale
Dopo la reinstallazione per riavere le finestre OpenCV, lo stato era:
```text
OpenCV GUI: WIN32UI
OpenCV CUDA devices: 0
```
Quindi la GUI era tornata, ma CUDA no.
## Installazione wheel CUDA
Installata la wheel:
```text
https://github.com/cudawarped/opencv-python-cuda-wheels/releases/download/4.12.0.88/opencv_contrib_python-4.12.0.88-cp37-abi3-win_amd64.whl
```
Motivo della scelta:
- il sistema ha CUDA Toolkit `v12.9`;
- la release `4.12.0.88` e' compilata contro CUDA `12.9`;
- la RTX 3050 ha compute capability `(8, 6)`, supportata dalla wheel.
## Correzione config.py
Dopo l'installazione, `import cv2` falliva per DLL mancanti.
Aggiornato:
```text
C:\Python313\Lib\site-packages\cv2\config.py
```
Aggiunti i path alle DLL NVIDIA presenti in:
```text
C:\Python313\Lib\site-packages\nvidia\cudnn\bin
C:\Python313\Lib\site-packages\nvidia\cublas\bin
C:\Python313\Lib\site-packages\nvidia\cuda_runtime\bin
C:\Python313\Lib\site-packages\nvidia\cufft\bin
C:\Python313\Lib\site-packages\nvidia\curand\bin
C:\Python313\Lib\site-packages\nvidia\cusolver\bin
C:\Python313\Lib\site-packages\nvidia\cusparse\bin
C:\Python313\Lib\site-packages\nvidia\nvjitlink\bin
```
## Verifica OpenCV
Risultato:
```text
cv2 4.12.0
GUI: WIN32UI
NVIDIA CUDA: YES (ver 12.9.86, CUFFT CUBLAS NVCUVID NVCUVENC)
cuDNN: YES (ver 9.10.2)
cuda devices: 1
gpu resize: OK
namedWindow: OK
```
Quindi OpenCV ora ha sia GUI sia CUDA.
## Stato GPU complessivo
YOLO / Ultralytics:
```text
torch_cuda True
NVIDIA GeForce RTX 3050
```
OpenCV:
```text
CUDA attivo
cuda devices 1
```
PaddleOCR:
```text
paddle 3.3.1
cuda False
device cpu
```
PaddleOCR resta volutamente nel virtualenv CPU per evitare il problema precedente sulle DLL/cuDNN Paddle GPU.
## Nota operativa
Ora ha senso lavorare sui target:
```text
preview/acquisizione: 24 fps
YOLO: circa 14-15 fps
OCR/WMS: asincrono, non deve bloccare la preview
```
OpenCV CUDA non accelera automaticamente tutto il codice: serve usare esplicitamente funzioni `cv2.cuda.*` nei punti in cui conviene. Pero' ora la base CUDA e' disponibile.

View File

@@ -0,0 +1,82 @@
# Aggiornamento 2026-05-18 15:53
## Obiettivo
Portare subito le chiamate disponibili verso il percorso piu' veloce possibile, usando CUDA dove il costo di trasferimento CPU/GPU e' giustificato e attivando FP16 per YOLO su GPU.
## Modifiche
- Aggiunti helper OpenCV CUDA con fallback CPU:
- `opencv_cuda_available()`
- `cuda_resize(...)`
- `cuda_cvt_color(...)`
- Gli helper usano CUDA solo sopra una soglia minima di pixel, per evitare di rallentare crop piccoli con upload/download GPU.
- Applicati gli helper a:
- preview e resize in `flywms_navigation.py`
- preprocessing e preview in `flywms_wms_server.py`
- preprocessing nel worker `flywms_paddleocr_worker.py`
- conversione BGR/RGBA della GUI DearPyGUI in `flywms_navigation_gui.py`
- Aggiunto parametro `yolo_half` in `flywms_navigation.ini`.
- Aggiunti flag CLI:
- `--yolo-half`
- `--no-yolo-half`
- `UltralyticsDetector` passa `half=True` a `model.predict(...)` quando il device non e' CPU.
- Log iniziale aggiornato con:
- stato OpenCV CUDA
- stato YOLO half precision
## Verifiche
Compilazione:
```powershell
python -m py_compile flywms_navigation.py flywms_wms_server.py flywms_paddleocr_worker.py flywms_navigation_gui.py
```
OpenCV:
```text
cv2 4.12.0
cuda_devices 1
GUI: WIN32UI
NVIDIA CUDA: YES (ver 12.9.86, CUFFT CUBLAS NVCUVID NVCUVENC)
cuDNN: YES (ver 9.10.2)
```
Helper CUDA:
```text
opencv_cuda True (720, 1280, 3) (720, 1280)
```
Run headless breve:
```powershell
python flywms_navigation.py --no-display --max-frames 40
```
Risultato rilevante:
```text
OpenCV CUDA: attivo
YOLO half precision: attiva
```
Misura inferenza YOLO sullo stesso frame:
```text
iter 0 ms 8430.9
iter 1 ms 27.5
iter 2 ms 26.7
iter 3 ms 28.5
iter 4 ms 30.4
```
Il primo giro e' warm-up del modello. A regime YOLO e' circa 27-30 ms/frame sul frame di test, quindi compatibile con un target dimostrativo di 14-15 fps lato inferenza se la pipeline non introduce altri colli di bottiglia.
## Note tecniche
- OpenCV CUDA non accelera automaticamente tutte le chiamate: bisogna usare esplicitamente `cv2.cuda.*`.
- Per crop piccoli e preprocessing leggero puo' essere piu' veloce restare su CPU.
- PaddleOCR resta attualmente nel virtualenv CPU; gli helper nel worker fanno fallback se OpenCV CUDA non e' disponibile in quell'ambiente.
- La prossima ottimizzazione importante sara' evitare warm-up visibile all'avvio, eseguendo una inferenza iniziale prima del loop operativo.

View File

@@ -0,0 +1,43 @@
# Aggiornamento 2026-05-18 18:15
## Obiettivo
Rendere espliciti a video i diversi FPS della demo, separando:
- FPS nominali della sorgente video
- target preview
- FPS reali del loop
- target YOLO
- FPS reali di YOLO
## Modifiche
- In [flywms_navigation.py](C:/devel/flywms/flywms_navigation.py) ho aggiunto `format_fps_value(...)` per mostrare valori puliti o `n/d`.
- All'avvio della navigazione ora viene loggato:
- `FPS sorgente=...`
- `preview_target=...`
- `yolo_target=...`
- Nell'overlay della finestra `flywms navigate` ora compare una riga del tipo:
```text
frame=123 src_fps=30.0 preview_target=24.0 fps=9.8 yolo_target=15.0 yolo_fps=9.7 yolo=28ms det=2 labels=2 tracks=2 snap=0
```
- In [flywms_navigation_gui.py](C:/devel/flywms/flywms_navigation_gui.py) ho reso coerente lo stesso overlay.
- Nella GUI DearPyGUI ho anche riallineato il costruttore del detector al nuovo parametro `yolo_half`.
## Verifica
Compilazione:
```powershell
python -m py_compile flywms_navigation.py flywms_navigation_gui.py
```
Esempio di testo generato:
```text
frame=1 src_fps=30.0 preview_target=24.0 fps=1000.0 yolo_target=15.0 yolo_fps=0.0 yolo=0ms det=0 labels=0 tracks=0 snap=0
```
Il valore `fps=1000.0` in questo micro-test e' artificiale, perche' non deriva da un run reale ma da una costruzione istantanea della stringa. In esecuzione reale il valore utile e' quello mostrato live in overlay.

View File

@@ -0,0 +1,67 @@
# Aggiornamento 2026-05-18 18:30
## Obiettivo
Registrare in un file dedicato le tempistiche dettagliate della pipeline, riducendo allo stesso tempo il costo della preview principale spostando la telemetria fuori dall'overlay.
## Modifiche
- Aggiunta classe `PerfLogWriter` in [flywms_navigation.py](C:/devel/flywms/flywms_navigation.py) con:
- bufferizzazione in memoria
- flush periodico a tempo
- flush per numero di righe
- Nuovi parametri INI/CLI:
- `perf_log_path`
- `perf_log_flush_interval_sec`
- `perf_log_flush_lines`
- Il file di default e' `tempistiche.txt`.
- Per ogni frame vengono scritte colonne TSV con:
- `read_ms`
- `yolo_ms`
- `track_ms`
- `draw_ms`
- `ui_ms`
- `snapshot_pause_ms`
- `wms_wait_ms`
- `loop_ms`
- `loop_fps`
- `yolo_real_fps`
- conteggi oggetti/track/snapshot
- ultimo comando navigazione
- La finestra principale `flywms navigate` non disegna piu':
- riga FPS/metriche
- ultimo comando
- testo sopra i bbox dei gaylord
- testo sopra i bbox etichetta
- Restano nella preview principale:
- bbox
- centri
- fascia utile
- freccia di movimento simulato
- Aggiornata anche [flywms_navigation_gui.py](C:/devel/flywms/flywms_navigation_gui.py) per il nuovo renderer alleggerito e per il costruttore `UltralyticsDetector(..., yolo_half)`.
## Verifica
Compilazione:
```powershell
python -m py_compile flywms_navigation.py flywms_navigation_gui.py
```
Run di test:
```powershell
python flywms_navigation.py --no-display --max-frames 20 --perf-log-path tempistiche-test.txt
```
Prime righe del log:
```text
ts frame_id run_yolo read_ms yolo_ms track_ms draw_ms ui_ms snapshot_pause_ms wms_wait_ms loop_ms loop_fps yolo_real_fps src_fps preview_target yolo_target det labels tracks active snapshots command
2026-05-18 18:31:14 1 1 39.710 10065.399 0.106 0.000 0.000 0.000 0.000 10150.452 0.099 0.099 30.0 24.0 15.0 2 2 2 2 0 ""
2026-05-18 18:31:14 2 1 7.376 38.036 0.222 0.000 0.000 0.000 0.000 47.984 0.196 0.196 30.0 24.0 15.0 2 2 2 2 0 ""
```
## Nota
Nel test headless `draw_ms` e `ui_ms` sono giustamente nulli. Nel run reale con finestre aperte il file `tempistiche.txt` mostrera' il costo della parte grafica e delle pause simulate, che e' proprio quello che ci serve per capire dove si perde il target FPS.

View File

@@ -0,0 +1,58 @@
# Aggiornamento 2026-05-18 19:14
## Obiettivo
Preparare una versione benchmark della navigazione senza interfaccia, con cattura/preview target a 30 fps, per confrontare il costo della UI rispetto alla pipeline pura.
## Modifiche
- Aggiunto profilo `benchmark` in [flywms_navigation.py](C:/devel/flywms/flywms_navigation.py):
- `--benchmark-mode`
- `--benchmark-preview-fps`
- Aggiunte le controparti INI in [flywms_navigation.ini](C:/devel/flywms/flywms_navigation.ini):
- `benchmark_mode = false`
- `benchmark_preview_fps = 30.0`
- Quando `benchmark_mode` e' attivo:
- forza `no_display = true`
- forza `window_layout_enabled = false`
- forza `realtime_playback = true`
- forza `preview_fps = benchmark_preview_fps`
- se il log tempi e' quello di default, lo sposta su `tempistiche-benchmark.txt`
## Uso
```powershell
python flywms_navigation.py --benchmark-mode
```
Oppure, con file log esplicito:
```powershell
python flywms_navigation.py --benchmark-mode --perf-log-path tempistiche-benchmark.txt
```
## Verifica
Compilazione:
```powershell
python -m py_compile flywms_navigation.py
```
Run breve:
```powershell
python flywms_navigation.py --benchmark-mode --max-frames 40
```
Output iniziale verificato:
```text
Profilo benchmark attivo: no_display=true preview_fps=30.0 log=tempistiche-benchmark.txt
FPS sorgente=30.0 preview_target=30.0 yolo_target=15.0
Log tempistiche: C:\devel\flywms\tempistiche-benchmark.txt
```
## Nota
Nel run corto di test c'e' stato un warm-up molto pesante di YOLO/NMS al primo frame, quindi quel log non va ancora usato come benchmark finale. Serve un run completo o almeno piu' lungo per avere un confronto sensato con la demo.

View File

@@ -0,0 +1,97 @@
# Aggiornamento 2026-05-18 19:58
## Analisi benchmark
File analizzato:
- [tempistiche-benchmark.txt](C:/devel/flywms/tempistiche-benchmark.txt)
Nota metodologica:
- Il file conteneva due intestazioni (`ts ...`) perche' un run breve di verifica era stato appeso al run completo.
- L'analisi e' stata fatta filtrando solo le righe con `frame_id` numerico.
## Risultati principali
- Frame validi: `19789`
- Snapshot: `69`
- Video sorgente: `30.0 fps`
- Durata nominale video: circa `658.3 s` = `10.97 min`
Benchmark completo:
- `total_loop_s = 2004.19 s`
- `total_pause_s = 552.03 s`
- `total_wms_s = 690.03 s`
- `total_active_s = 762.13 s`
Interpretazione:
- Le pause snapshot headless valgono circa `8.0 s` ciascuna.
- L'attesa WMS simulata headless vale circa `10.0 s` per snapshot.
- Quindi il benchmark completo include ancora il comportamento demo di snapshot+attesa, solo senza finestre.
## Prestazioni nette della pipeline
Togliendo pause snapshot e attesa WMS:
- `active_fps = 25.97`
- `active_yolo_fps = 12.69`
Tempi medi in regime stabile:
- `mean_loop_ms = 36.90`
- `mean_read_ms = 6.10`
- `mean_yolo_ms = 31.20`
- `mean_track_ms = 0.18`
- `mean_draw_ms = 0.00`
- `mean_ui_ms = 0.00`
Percentili:
- `loop_p50 = 33.91 ms`
- `loop_p90 = 68.93 ms`
- `loop_p95 = 73.14 ms`
- `yolo_p50 = 27.85 ms`
- `yolo_p90 = 38.05 ms`
- `yolo_p95 = 46.10 ms`
## Confronto con la demo OpenCV
Run demo precedente:
- `demo_active_s = 1092.85 s`
- `demo_draw_ui_s = 597.18 s`
Run benchmark:
- `benchmark_active_s = 762.13 s`
- `benchmark_draw_ui_s = 0`
Risparmio netto:
- `active_s_saved = 330.72 s` circa `5.51 min`
## Conclusione
La UI OpenCV pesava davvero molto, ma non era l'unico problema.
Tolte completamente le finestre:
- la pipeline netta scende a circa `12.70 min`
- il video reale dura circa `10.97 min`
Residuo rispetto all'obiettivo netto:
- circa `103.8 s` = `1.73 min`
Questa parte residua dipende principalmente da:
- inferenza YOLO ancora sotto il target di `15 fps`
- costo di `cap.read()`
- scheduling single-thread del loop
Quindi:
- la demo con interfaccia va ripensata per non ricadere nel costo UI attuale
- la pipeline core, senza UI, e' ormai abbastanza vicina all'obiettivo ma non ancora coincidente con la durata del video

View File

@@ -3,7 +3,7 @@
; 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
video = testhd2_edit.mp4
; OBBLIGATORIO: si.
; Ruolo: modello Ultralytics/YOLO moderno usato per rilevare gaylord ed etichette.
@@ -11,9 +11,9 @@ video = testhd.mp4
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".
; Ruolo: device usato da Ultralytics. Usa "cpu" per forzare CPU; con GPU compatibile usare "0".
; Default se non indicato: cpu
ultralytics_device = cpu
ultralytics_device = 0
; OBBLIGATORIO: no.
; Ruolo: dimensione input YOLO. 640 e' il valore usato nel training rapido.
@@ -30,6 +30,12 @@ min_confidence = 0.25
; Default se non indicato: gaylord
target_class = gaylord
; OBBLIGATORIO: no.
; Ruolo: classe etichetta associata al gaylord centrato.
; Il bbox etichetta viene accettato solo se e' contenuto nel bbox del gaylord.
; Default se non indicato: etichetta
label_class = etichetta
; OBBLIGATORIO: no.
; Ruolo: numero massimo di frame in cui una track puo' non essere vista prima di essere rimossa.
; Default se non indicato: 8
@@ -89,6 +95,11 @@ edge_margin_ratio = 0.0
; Default se non indicato: 0.03
ocr_payload_pad_ratio = 0.03
; OBBLIGATORIO: no.
; Ruolo: padding aggiunto al bbox etichetta prima di salvare il crop inviato all'OCR remoto.
; Default se non indicato: 0.20
label_payload_pad_ratio = 0.20
; OBBLIGATORIO: no.
; Ruolo: trend minimo dell'area bbox negli ultimi frame. Valori negativi tollerano leggera uscita.
; Default se non indicato: -0.35
@@ -106,15 +117,168 @@ snapshot_window_frames = 1
snapshot_output_dir = navigate_snapshots
; OBBLIGATORIO: no.
; Ruolo: tempo simulato con cui il drone attende OCR remoto + verifica WMS.
; Ruolo: tempo massimo con cui il drone attende OCR remoto + verifica WMS.
; Con PaddleOCR il primo avvio del worker puo' richiedere alcuni secondi.
; Default se non indicato: 2.0
remote_ack_timeout_sec = 2.0
remote_ack_timeout_sec = 10.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: se true, invia realmente il payload etichetta al server WMS demo via HTTP.
; Se false, mantiene la risposta remota simulata locale.
; Default se non indicato: false
wms_enabled = false
; OBBLIGATORIO: no.
; Ruolo: endpoint HTTP del server WMS demo, anche su altro PC.
; Esempio rete: http://192.168.1.50:8088/api/v1/navigation-snapshot
; Default se non indicato: http://127.0.0.1:8088/api/v1/navigation-snapshot
wms_server_url = http://127.0.0.1:8088/api/v1/navigation-snapshot
; OBBLIGATORIO: no.
; Ruolo: identificativo del client/drone demo mandato nei metadati.
; Default se non indicato: flywms-demo-01
wms_client_id = flywms-demo-01
; OBBLIGATORIO: no.
; Ruolo: timeout HTTP del worker WMS, in secondi.
; Deve essere maggiore del tempo OCR server; con PaddleOCR fast caldo circa 3s, primo avvio circa 8-10s.
; Default se non indicato: 2.0
wms_timeout_sec = 12.0
; OBBLIGATORIO: no.
; Ruolo: numero massimo di snapshot in coda verso il WMS.
; Default se non indicato: 8
wms_queue_max_size = 8
; OBBLIGATORIO: no.
; Ruolo: se true, oltre al crop etichetta invia anche il frame/debug gaylord.
; Default se non indicato: true
wms_send_gaylord_debug = true
; OBBLIGATORIO: no.
; Ruolo: host su cui il server WMS demo FastAPI resta in ascolto.
; 0.0.0.0 consente connessioni da altri PC della rete.
; Default se non indicato: 0.0.0.0
wms_server_host = 0.0.0.0
; OBBLIGATORIO: no.
; Ruolo: porta del server WMS demo FastAPI.
; Default se non indicato: 8088
wms_server_port = 8088
; OBBLIGATORIO: no.
; Ruolo: directory in cui il server salva immagini e metadati ricevuti.
; Default se non indicato: wms_received
wms_received_dir = wms_received
; OBBLIGATORIO: no.
; Ruolo: risposta simulata server. Valori: always-ack, always-nack, alternate, random.
; Default se non indicato: always-ack
wms_fake_ack_mode = always-ack
; OBBLIGATORIO: no.
; Ruolo: ritardo finto del server per simulare OCR/WMS.
; Default se non indicato: 0.5
wms_fake_processing_sec = 0.5
; OBBLIGATORIO: no.
; Ruolo: se true, il server WMS demo apre le finestre OpenCV immagine/payload.
; Default se non indicato: true
wms_ui_enabled = true
; OBBLIGATORIO: no.
; Ruolo: se true, il server prova OCR reale sul crop etichetta.
; Default se non indicato: true
wms_ocr_enabled = true
; OBBLIGATORIO: no.
; Ruolo: motore OCR server. Valori: paddleocr, easyocr, tesseract, fake.
; Default se non indicato: paddleocr
wms_ocr_mode = paddleocr
; OBBLIGATORIO: no.
; Ruolo: Python del virtualenv isolato usato dal worker PaddleOCR.
; Default se non indicato: C:\devel\yolo-ocr\.venv-paddle-cpu\Scripts\python.exe
wms_paddle_python = C:\devel\yolo-ocr\.venv-paddle-cpu\Scripts\python.exe
; OBBLIGATORIO: no.
; Ruolo: script worker PaddleOCR persistente.
; Default se non indicato: flywms_paddleocr_worker.py nella cartella flywms.
wms_paddle_worker_script = C:\devel\flywms\flywms_paddleocr_worker.py
; OBBLIGATORIO: no.
; Ruolo: altezze a cui ridimensionare il crop etichetta prima dell'OCR.
; Per demo 96 e' la configurazione leggera. Per debug: 64,96,128.
; Default se non indicato: 96
wms_paddle_target_heights = 96
; OBBLIGATORIO: no.
; Ruolo: quante varianti preprocessing usare nel worker PaddleOCR.
; Valori: fast, balanced, full.
; fast usa originale, CLAHE, CLAHE+sharpen ed e' il default per il server demo.
; Default se non indicato: fast
wms_paddle_variant_set = fast
; OBBLIGATORIO: no.
; Ruolo: numero di cifre atteso nel codice UDC.
; Default se non indicato: 6
wms_paddle_expected_digits = 6
; OBBLIGATORIO: no.
; Ruolo: numero minimo di varianti OCR che devono concordare sul codice.
; Default se non indicato: 2
wms_paddle_min_votes = 2
; OBBLIGATORIO: no.
; Ruolo: confidenza minima del candidato PaddleOCR scelto.
; Default se non indicato: 0.70
wms_paddle_min_confidence = 0.70
; OBBLIGATORIO: no.
; Ruolo: prefisso del codice fallback se OCR reale non legge nulla.
; Default se non indicato: UDC
wms_fake_ocr_prefix = UDC
; OBBLIGATORIO: no.
; Ruolo: testo restituito quando OCR non determina il codice reale.
; Default se non indicato: udc non determinato
wms_undetermined_code_text = udc non determinato
; OBBLIGATORIO: no.
; Ruolo: ritardo finto OCR/fallback in secondi.
; Default se non indicato: 0.2
wms_fake_ocr_delay_sec = 0.2
; OBBLIGATORIO: no.
; Ruolo: operatore/drone simulato registrato nel payload WMS.
; Default se non indicato: udrone
wms_operator = udrone
; OBBLIGATORIO: no.
; Ruolo: sito/magazzino simulato registrato nel payload WMS.
; Default se non indicato: demo-magazzino
wms_site = demo-magazzino
; OBBLIGATORIO: no.
; Ruolo: durata simulata del movimento drone dal centro gaylord al centro etichetta.
; Default se non indicato: 3.0
label_move_sec = 3.0
; OBBLIGATORIO: no.
; Ruolo: attesa simulata per stabilizzare la foto ravvicinata dell'etichetta.
; Default se non indicato: 2.0
label_stabilization_sec = 2.0
; OBBLIGATORIO: no.
; Ruolo: durata simulata del ritorno dal centro etichetta al centro gaylord.
; Default se non indicato: 3.0
label_return_sec = 3.0
; OBBLIGATORIO: no.
; Ruolo: direzione simulata di ripartenza dopo ACK. Valori: destra, sinistra.
; Default se non indicato: destra
@@ -125,6 +289,17 @@ scan_direction = destra
; Default se non indicato: 1280
preview_width = 1280
; OBBLIGATORIO: no.
; Ruolo: profilo benchmark senza finestre OpenCV.
; Se true forza no_display e usa benchmark_preview_fps come target di cattura/preview.
; Default se non indicato: false
benchmark_mode = false
; OBBLIGATORIO: no.
; Ruolo: FPS target di cattura/preview per il profilo benchmark.
; Default se non indicato: 30.0
benchmark_preview_fps = 30.0
; OBBLIGATORIO: no.
; Ruolo: se true, il video di test viene riprodotto rispettando il framerate originale.
; Default se non indicato: true
@@ -137,6 +312,12 @@ realtime_playback = true
; Default se non indicato: 24.0
preview_fps = 24.0
; OBBLIGATORIO: no.
; Ruolo: se true, usa FP16/half precision per YOLO quando gira su GPU.
; Su CPU viene ignorato automaticamente.
; Default se non indicato: true
yolo_half = true
; 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.
@@ -153,6 +334,21 @@ max_frames = 0
; Default se non indicato: 2.0
stats_interval = 2.0
; OBBLIGATORIO: no.
; Ruolo: file di log delle tempistiche dettagliate per frame.
; Default se non indicato: tempistiche.txt
perf_log_path = tempistiche.txt
; OBBLIGATORIO: no.
; Ruolo: intervallo massimo tra due flush del file di tempistiche.
; Default se non indicato: 2.0
perf_log_flush_interval_sec = 2.0
; OBBLIGATORIO: no.
; Ruolo: numero di righe bufferizzate prima del flush del file di tempistiche.
; Default se non indicato: 120
perf_log_flush_lines = 120
; OBBLIGATORIO: no.
; Ruolo: ogni quanti frame aggiornare il moto apparente stimato dalle track.
; Default se non indicato: 5
@@ -177,3 +373,25 @@ flash_alpha = 0.70
; Ruolo: se true, disabilita tutte le finestre video. Usarlo solo per test headless.
; Default se non indicato: false
no_display = false
; OBBLIGATORIO: no.
; Ruolo: se true, posiziona e ridimensiona le finestre OpenCV usando i valori sotto.
; Formato finestre: x,y,width,height
; Default se non indicato: true
window_layout_enabled = true
; OBBLIGATORIO: no.
; Ruolo: posizione finestra principale con tracking gaylord/etichetta.
navigate_window = 20,40,1100,620
; OBBLIGATORIO: no.
; Ruolo: posizione finestra comandi e indicatori movimento.
commands_window = 1140,40,760,520
; OBBLIGATORIO: no.
; Ruolo: posizione finestra snapshot gaylord.
snapshot_window = 1140,590,520,360
; OBBLIGATORIO: no.
; Ruolo: posizione finestra crop etichetta.
label_window = 1140,980,520,260

File diff suppressed because it is too large Load Diff

View File

@@ -53,13 +53,15 @@ class NavigationDemoEngine:
def __init__(self, args):
self.args = args
nav.require_file(args.weights, "modello Ultralytics")
self.detector = nav.UltralyticsDetector(args.weights, args.ultralytics_device)
self.detector = nav.UltralyticsDetector(args.weights, args.ultralytics_device, args.yolo_half)
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
self.video_fps = video_fps
self.preview_fps = preview_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 = (
@@ -82,6 +84,7 @@ class NavigationDemoEngine:
self.next_yolo_time = self.start_time
self.last_yolo_ms = 0.0
self.gaylords: list[nav.Detection] = []
self.labels: list[nav.Detection] = []
self.tracks: list[nav.Track] = []
self.stop_reason = ""
@@ -123,6 +126,10 @@ class NavigationDemoEngine:
det for det in detections
if det.class_name.strip().lower() == self.args.target_class.strip().lower()
]
self.labels = [
det for det in detections
if det.class_name.strip().lower() == self.args.label_class.strip().lower()
]
self.tracks = self.tracker.update(self.gaylords, self.frame_id, frame.shape[1])
if (
@@ -140,6 +147,7 @@ class NavigationDemoEngine:
frame,
self.frame_id,
timestamp,
self.labels,
)
if snapshot is not None:
new_snapshots.append(snapshot)
@@ -149,17 +157,19 @@ 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"yolo_fps={self.yolo_cycles / elapsed:.1f} yolo={self.last_yolo_ms:.0f}ms "
f"det={len(self.gaylords)} tracks={len(self.tracks)} "
f"frame={self.frame_id} src_fps={nav.format_fps_value(self.video_fps)} "
f"preview_target={nav.format_fps_value(self.preview_fps)} fps={self.frame_id / elapsed:.1f} "
f"yolo_target={nav.format_fps_value(self.args.yolo_fps)} yolo_fps={self.yolo_cycles / elapsed:.1f} "
f"yolo={self.last_yolo_ms:.0f}ms det={len(self.gaylords)} labels={len(self.labels)} "
f"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,
self.labels,
self.navigator.label_movement_arrow,
)
return DemoFrameState(
@@ -335,7 +345,7 @@ class NavigationDemoGui:
def bgr_to_rgba_float(frame: np.ndarray) -> np.ndarray:
rgba = cv2.cvtColor(frame, cv2.COLOR_BGR2RGBA)
rgba = nav.cuda_cvt_color(frame, cv2.COLOR_BGR2RGBA)
return np.asarray(rgba, dtype=np.float32).ravel() / 255.0

340
flywms_paddleocr_worker.py Normal file
View File

@@ -0,0 +1,340 @@
from __future__ import annotations
import json
import os
import re
import site
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import Any
import cv2
import numpy as np
CUDA_MIN_PIXELS = 640 * 360
def opencv_cuda_available() -> bool:
try:
return hasattr(cv2, "cuda") and cv2.cuda.getCudaEnabledDeviceCount() > 0
except cv2.error:
return False
OPENCV_CUDA_AVAILABLE = opencv_cuda_available()
def cuda_resize(
image: np.ndarray,
size: tuple[int, int],
interpolation: int = cv2.INTER_LINEAR,
min_pixels: int = CUDA_MIN_PIXELS,
) -> np.ndarray:
if not OPENCV_CUDA_AVAILABLE or image.size < min_pixels:
return cv2.resize(image, size, interpolation=interpolation)
try:
gpu = cv2.cuda_GpuMat()
gpu.upload(image)
return cv2.cuda.resize(gpu, size, interpolation=interpolation).download()
except cv2.error:
return cv2.resize(image, size, interpolation=interpolation)
def cuda_cvt_color(
image: np.ndarray,
code: int,
min_pixels: int = CUDA_MIN_PIXELS,
) -> np.ndarray:
if not OPENCV_CUDA_AVAILABLE or image.size < min_pixels:
return cv2.cvtColor(image, code)
try:
gpu = cv2.cuda_GpuMat()
gpu.upload(image)
return cv2.cuda.cvtColor(gpu, code).download()
except cv2.error:
return cv2.cvtColor(image, code)
@dataclass(frozen=True)
class Candidate:
text: str
score: float
variant: str
def add_nvidia_dll_dirs() -> None:
if os.name != "nt":
return
for site_dir in site.getsitepackages():
nvidia_root = Path(site_dir) / "nvidia"
if not nvidia_root.exists():
continue
for bin_dir in nvidia_root.glob("*/bin"):
if bin_dir.exists():
os.add_dll_directory(str(bin_dir))
def resize_to_height(image: np.ndarray, target_height: int) -> np.ndarray:
h, w = image.shape[:2]
if h == target_height:
return image
scale = target_height / max(1, h)
return cuda_resize(image, (max(1, int(w * scale)), target_height), interpolation=cv2.INTER_CUBIC)
def add_border(image: np.ndarray, ratio: float = 0.08) -> np.ndarray:
h, w = image.shape[:2]
pad_x = max(2, int(w * ratio))
pad_y = max(2, int(h * ratio))
return cv2.copyMakeBorder(
image,
pad_y,
pad_y,
pad_x,
pad_x,
cv2.BORDER_CONSTANT,
value=(255, 255, 255),
)
def clahe_bgr(image: np.ndarray) -> np.ndarray:
lab = cuda_cvt_color(image, cv2.COLOR_BGR2LAB)
l_channel, a_channel, b_channel = cv2.split(lab)
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(4, 4))
merged = cv2.merge([clahe.apply(l_channel), a_channel, b_channel])
return cuda_cvt_color(merged, cv2.COLOR_LAB2BGR)
def sharpen(image: np.ndarray, strength: float = 0.7) -> np.ndarray:
blurred = cv2.GaussianBlur(image, (0, 0), sigmaX=1.0)
return cv2.addWeighted(image, 1.0 + strength, blurred, -strength, 0)
def variants_for_height(image: np.ndarray, target_height: int) -> dict[str, np.ndarray]:
base = resize_to_height(image, target_height)
bordered = add_border(base)
gray = cuda_cvt_color(base, cv2.COLOR_BGR2GRAY)
clahe = clahe_bgr(base)
sharp = sharpen(clahe)
adaptive = cv2.adaptiveThreshold(
gray,
255,
cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY,
17,
5,
)
otsu = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)[1]
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (2, 2))
close = cv2.morphologyEx(otsu, cv2.MORPH_CLOSE, kernel, iterations=1)
denoise = cv2.fastNlMeansDenoising(gray, h=7, templateWindowSize=7, searchWindowSize=21)
return {
"orig": base,
"orig_border": bordered,
"gray": cuda_cvt_color(gray, cv2.COLOR_GRAY2BGR),
"clahe": clahe,
"clahe_sharp": sharp,
"adaptive": cuda_cvt_color(adaptive, cv2.COLOR_GRAY2BGR),
"otsu": cuda_cvt_color(otsu, cv2.COLOR_GRAY2BGR),
"otsu_close": cuda_cvt_color(close, cv2.COLOR_GRAY2BGR),
"denoise": cuda_cvt_color(denoise, cv2.COLOR_GRAY2BGR),
}
def build_variants(image: np.ndarray, target_heights: list[int]) -> dict[str, np.ndarray]:
all_variants: dict[str, np.ndarray] = {}
for height in target_heights:
for name, variant in variants_for_height(image, height).items():
all_variants[f"h{height}_{name}"] = variant
return all_variants
def filter_variants(all_variants: dict[str, np.ndarray], variant_set: str) -> dict[str, np.ndarray]:
if variant_set == "full":
return all_variants
balanced_names = ("orig", "orig_border", "clahe", "clahe_sharp", "adaptive")
fast_names = ("orig", "clahe", "clahe_sharp")
allowed = balanced_names if variant_set == "balanced" else fast_names
return {
name: image
for name, image in all_variants.items()
if any(name.endswith(f"_{suffix}") for suffix in allowed)
}
def extract_candidates(result: Any) -> list[tuple[str, float]]:
candidates: list[tuple[str, float]] = []
def walk(value: Any) -> None:
if value is None:
return
if isinstance(value, dict):
rec_texts = value.get("rec_texts")
rec_scores = value.get("rec_scores")
if isinstance(rec_texts, list):
for idx, text in enumerate(rec_texts):
score = rec_scores[idx] if isinstance(rec_scores, list) and idx < len(rec_scores) else 0.0
if isinstance(text, str):
candidates.append((text, float(score)))
text = value.get("rec_text") or value.get("text")
score = value.get("rec_score") or value.get("score")
if isinstance(text, str):
candidates.append((text, float(score) if score is not None else 0.0))
for child in value.values():
walk(child)
return
if isinstance(value, (list, tuple)):
if len(value) >= 2 and isinstance(value[1], tuple) and len(value[1]) >= 2:
text, score = value[1][0], value[1][1]
if isinstance(text, str):
candidates.append((text, float(score)))
for child in value:
walk(child)
walk(result)
dedup: dict[str, float] = {}
for text, score in candidates:
digits = re.sub(r"\D+", "", text)
if not digits:
continue
dedup[digits] = max(score, dedup.get(digits, 0.0))
return sorted(dedup.items(), key=lambda item: item[1], reverse=True)
def choose_best(candidates: list[Candidate], expected_digits: int) -> tuple[str, float, int, str, float]:
if not candidates:
return "", 0.0, 0, "", 0.0
grouped: dict[str, list[Candidate]] = {}
for candidate in candidates:
grouped.setdefault(candidate.text, []).append(candidate)
best_text = ""
best_rank = -999.0
best_score = 0.0
best_votes = 0
best_variant = ""
for text, group in grouped.items():
max_conf = max(item.score for item in group)
votes = len(group)
unique_variants = len({item.variant for item in group})
length_penalty = abs(len(text) - expected_digits) * 0.35
exact_bonus = 0.35 if len(text) == expected_digits else 0.0
consensus_bonus = min(0.30, votes * 0.035) + min(0.20, unique_variants * 0.025)
rank = max_conf + exact_bonus + consensus_bonus - length_penalty
if rank > best_rank:
best_text = text
best_rank = rank
best_score = max_conf
best_votes = votes
best_variant = max(group, key=lambda item: item.score).variant
return best_text, best_score, best_votes, best_variant, best_rank
def parse_target_heights(raw: str) -> list[int]:
values = []
for part in raw.split(","):
part = part.strip()
if part:
values.append(int(part))
return values or [96]
def make_ocr():
add_nvidia_dll_dirs()
from paddleocr import PaddleOCR
return PaddleOCR(
lang="en",
use_doc_orientation_classify=False,
use_doc_unwarping=False,
use_textline_orientation=False,
text_rec_score_thresh=0.0,
)
def predict_one(ocr: Any, image: np.ndarray) -> list[tuple[str, float]]:
if hasattr(ocr, "predict"):
result = ocr.predict(image)
else:
result = ocr.ocr(image, det=False, cls=False)
return extract_candidates(result)
def handle_request(ocr: Any, request: dict[str, Any]) -> dict[str, Any]:
image_path = Path(str(request["image_path"]))
target_heights = parse_target_heights(str(request.get("target_heights", "96")))
variant_set = str(request.get("variant_set", "fast")).strip().lower()
expected_digits = int(request.get("expected_digits", 6))
min_votes = int(request.get("min_votes", 2))
min_confidence = float(request.get("min_confidence", 0.70))
image = cv2.imread(str(image_path), cv2.IMREAD_COLOR)
if image is None:
return {
"ok": False,
"text": "",
"raw_text": "",
"confidence": 0.0,
"votes": 0,
"variant": "",
"reason": f"unreadable_image:{image_path}",
"candidates": [],
}
candidates: list[Candidate] = []
ocr_variants = filter_variants(build_variants(image, target_heights), variant_set)
for variant_name, variant_image in ocr_variants.items():
for text, score in predict_one(ocr, variant_image):
candidates.append(Candidate(text=text, score=score, variant=variant_name))
text, confidence, votes, variant, rank = choose_best(candidates, expected_digits)
accepted = bool(text) and len(text) == expected_digits and votes >= min_votes and confidence >= min_confidence
sorted_candidates = sorted(candidates, key=lambda item: item.score, reverse=True)
raw_text = " | ".join(f"{item.text}:{item.score:.3f}:{item.variant}" for item in sorted_candidates[:18])
return {
"ok": accepted,
"text": text if accepted else "",
"best_text": text,
"raw_text": raw_text,
"confidence": confidence,
"votes": votes,
"variant": variant,
"rank": rank,
"reason": "ok" if accepted else "low_consensus_or_invalid_length",
"candidates": [
{"text": item.text, "score": item.score, "variant": item.variant}
for item in sorted_candidates[:18]
],
}
def main() -> int:
ocr = make_ocr()
print(json.dumps({"ready": True}), flush=True)
for line in sys.stdin:
line = line.strip()
if not line:
continue
if line == "__quit__":
break
try:
request = json.loads(line)
response = handle_request(ocr, request)
except Exception as exc:
response = {
"ok": False,
"text": "",
"raw_text": "",
"confidence": 0.0,
"votes": 0,
"variant": "",
"reason": f"worker_error:{exc}",
"candidates": [],
}
print(json.dumps(response, ensure_ascii=True), flush=True)
return 0
if __name__ == "__main__":
raise SystemExit(main())

789
flywms_wms_server.py Normal file
View File

@@ -0,0 +1,789 @@
import argparse
import asyncio
import configparser
import json
import random
import re
import shutil
import subprocess
import threading
import time
from dataclasses import dataclass
from datetime import datetime, timezone
from pathlib import Path
import cv2
import numpy as np
import uvicorn
from fastapi import FastAPI, File, Form, UploadFile
DEFAULT_CONFIG_PATH = "flywms_navigation.ini"
CUDA_MIN_PIXELS = 640 * 360
def opencv_cuda_available() -> bool:
try:
return hasattr(cv2, "cuda") and cv2.cuda.getCudaEnabledDeviceCount() > 0
except cv2.error:
return False
OPENCV_CUDA_AVAILABLE = opencv_cuda_available()
def cuda_resize(
image: np.ndarray,
size: tuple[int, int],
interpolation: int = cv2.INTER_LINEAR,
min_pixels: int = CUDA_MIN_PIXELS,
) -> np.ndarray:
if not OPENCV_CUDA_AVAILABLE or image.size < min_pixels:
return cv2.resize(image, size, interpolation=interpolation)
try:
gpu = cv2.cuda_GpuMat()
gpu.upload(image)
return cv2.cuda.resize(gpu, size, interpolation=interpolation).download()
except cv2.error:
return cv2.resize(image, size, interpolation=interpolation)
def cuda_cvt_color(
image: np.ndarray,
code: int,
min_pixels: int = CUDA_MIN_PIXELS,
) -> np.ndarray:
if not OPENCV_CUDA_AVAILABLE or image.size < min_pixels:
return cv2.cvtColor(image, code)
try:
gpu = cv2.cuda_GpuMat()
gpu.upload(image)
return cv2.cuda.cvtColor(gpu, code).download()
except cv2.error:
return cv2.cvtColor(image, code)
app = FastAPI(title="FlyWMS Demo WMS Server")
server_state = {
"received_dir": "wms_received",
"fake_ack_mode": "always-ack",
"fake_processing_sec": 0.5,
"ui_enabled": True,
"ocr_enabled": True,
"ocr_mode": "paddleocr",
"tesseract_cmd": "",
"paddle_python": r"C:\devel\yolo-ocr\.venv-paddle-cpu\Scripts\python.exe",
"paddle_worker_script": str(Path(__file__).with_name("flywms_paddleocr_worker.py")),
"paddle_target_heights": "96",
"paddle_variant_set": "fast",
"paddle_expected_digits": 6,
"paddle_min_votes": 2,
"paddle_min_confidence": 0.70,
"fake_ocr_prefix": "UDC",
"undetermined_code_text": "udc non determinato",
"fake_ocr_delay_sec": 0.2,
"operator": "udrone",
"site": "demo-magazzino",
"counter": 0,
}
state_lock = threading.Lock()
ui_lock = threading.Lock()
ui_state = {
"image": None,
"payload_lines": ["In attesa di payload WMS"],
"updated": False,
"stop": False,
}
@dataclass(frozen=True)
class OcrServerResult:
text: str
raw_text: str
confidence: float
backend: str
fallback_used: bool
votes: int = 0
variant: str = ""
class PaddleOcrWorker:
def __init__(
self,
python_path: str,
script_path: str,
target_heights: str,
variant_set: str,
expected_digits: int,
min_votes: int,
min_confidence: float,
) -> None:
self.python_path = python_path
self.script_path = script_path
self.target_heights = target_heights
self.variant_set = variant_set
self.expected_digits = expected_digits
self.min_votes = min_votes
self.min_confidence = min_confidence
self.lock = threading.Lock()
self.process: subprocess.Popen[str] | None = None
def close(self) -> None:
with self.lock:
process = self.process
self.process = None
if process is None:
return
try:
if process.stdin:
process.stdin.write("__quit__\n")
process.stdin.flush()
except Exception:
pass
try:
process.wait(timeout=2.0)
except subprocess.TimeoutExpired:
process.kill()
def predict(self, image_path: Path) -> dict[str, object]:
with self.lock:
process = self._ensure_process()
if process.stdin is None or process.stdout is None:
raise RuntimeError("worker stdio non disponibile")
request = {
"image_path": str(image_path),
"target_heights": self.target_heights,
"variant_set": self.variant_set,
"expected_digits": self.expected_digits,
"min_votes": self.min_votes,
"min_confidence": self.min_confidence,
}
process.stdin.write(json.dumps(request, ensure_ascii=True) + "\n")
process.stdin.flush()
line = process.stdout.readline()
if not line:
self.process = None
raise RuntimeError("worker PaddleOCR terminato")
return json.loads(line)
def _ensure_process(self) -> subprocess.Popen[str]:
if self.process is not None and self.process.poll() is None:
return self.process
python_path = Path(self.python_path)
script_path = Path(self.script_path)
if not python_path.exists():
raise RuntimeError(f"python PaddleOCR non trovato: {python_path}")
if not script_path.exists():
raise RuntimeError(f"worker PaddleOCR non trovato: {script_path}")
self.process = subprocess.Popen(
[str(python_path), str(script_path)],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
text=True,
encoding="utf-8",
errors="replace",
creationflags=subprocess.CREATE_NO_WINDOW if hasattr(subprocess, "CREATE_NO_WINDOW") else 0,
)
assert self.process.stdout is not None
ready_line = self.process.stdout.readline()
if not ready_line:
raise RuntimeError("worker PaddleOCR non avviato")
ready = json.loads(ready_line)
if not ready.get("ready"):
raise RuntimeError(f"worker PaddleOCR risposta inattesa: {ready}")
return self.process
_paddle_worker: PaddleOcrWorker | None = None
_paddle_worker_lock = threading.Lock()
def log(msg: str) -> None:
print(f"[{time.strftime('%H:%M:%S')}] {msg}", flush=True)
def load_server_config(path_str: str) -> dict[str, object]:
defaults: dict[str, object] = {
"wms_server_host": "0.0.0.0",
"wms_server_port": 8088,
"wms_received_dir": "wms_received",
"wms_fake_ack_mode": "always-ack",
"wms_fake_processing_sec": 0.5,
"wms_ui_enabled": True,
"wms_ocr_enabled": True,
"wms_ocr_mode": "paddleocr",
"wms_tesseract_cmd": "",
"wms_paddle_python": r"C:\devel\yolo-ocr\.venv-paddle-cpu\Scripts\python.exe",
"wms_paddle_worker_script": str(Path(__file__).with_name("flywms_paddleocr_worker.py")),
"wms_paddle_target_heights": "96",
"wms_paddle_variant_set": "fast",
"wms_paddle_expected_digits": 6,
"wms_paddle_min_votes": 2,
"wms_paddle_min_confidence": 0.70,
"wms_fake_ocr_prefix": "UDC",
"wms_undetermined_code_text": "udc non determinato",
"wms_fake_ocr_delay_sec": 0.2,
"wms_operator": "udrone",
"wms_site": "demo-magazzino",
}
path = Path(path_str)
if not path.exists():
return defaults
parser = configparser.ConfigParser()
parser.read(path, encoding="utf-8")
section = parser["navigation"] if parser.has_section("navigation") else {}
for key, default_value in defaults.items():
if key not in section:
continue
if isinstance(default_value, bool):
defaults[key] = parser.getboolean("navigation", key, fallback=default_value)
elif isinstance(default_value, int):
defaults[key] = parser.getint("navigation", key, fallback=default_value)
elif isinstance(default_value, float):
defaults[key] = parser.getfloat("navigation", key, fallback=default_value)
else:
defaults[key] = section.get(key, str(default_value)).strip()
return defaults
def parse_args():
pre = argparse.ArgumentParser(add_help=False)
pre.add_argument("--config", default=DEFAULT_CONFIG_PATH, help="File configurazione INI")
pre_args, _ = pre.parse_known_args()
defaults = load_server_config(pre_args.config)
ap = argparse.ArgumentParser(parents=[pre])
ap.add_argument("--host", default=defaults["wms_server_host"], help="Host bind server")
ap.add_argument("--port", type=int, default=defaults["wms_server_port"], help="Porta server")
ap.add_argument("--received-dir", default=defaults["wms_received_dir"], help="Directory upload ricevuti")
ap.add_argument(
"--fake-ack-mode",
choices=["always-ack", "always-nack", "alternate", "random"],
default=defaults["wms_fake_ack_mode"],
help="Modalita risposta WMS demo",
)
ap.add_argument(
"--fake-processing-sec",
type=float,
default=defaults["wms_fake_processing_sec"],
help="Ritardo elaborazione finta",
)
ap.add_argument("--ui-enabled", action="store_true", default=defaults["wms_ui_enabled"], help="Mostra finestre OpenCV server")
ap.add_argument("--no-ui", action="store_false", dest="ui_enabled", help="Disabilita finestre OpenCV server")
ap.add_argument("--ocr-enabled", action="store_true", default=defaults["wms_ocr_enabled"], help="Esegue OCR reale se disponibile")
ap.add_argument("--no-ocr", action="store_false", dest="ocr_enabled", help="Disabilita OCR reale e usa fallback demo")
ap.add_argument("--ocr-mode", choices=["paddleocr", "easyocr", "tesseract", "fake"], default=defaults["wms_ocr_mode"], help="Motore OCR server")
ap.add_argument("--tesseract-cmd", default=defaults["wms_tesseract_cmd"], help="Percorso esplicito tesseract.exe")
ap.add_argument("--paddle-python", default=defaults["wms_paddle_python"], help="Python del virtualenv PaddleOCR")
ap.add_argument("--paddle-worker-script", default=defaults["wms_paddle_worker_script"], help="Script worker PaddleOCR")
ap.add_argument("--paddle-target-heights", default=defaults["wms_paddle_target_heights"], help="Altezze resize PaddleOCR, es. 96 oppure 64,96,128")
ap.add_argument("--paddle-variant-set", choices=["fast", "balanced", "full"], default=defaults["wms_paddle_variant_set"], help="Numero varianti preprocessing PaddleOCR")
ap.add_argument("--paddle-expected-digits", type=int, default=defaults["wms_paddle_expected_digits"], help="Numero cifre atteso codice UDC")
ap.add_argument("--paddle-min-votes", type=int, default=defaults["wms_paddle_min_votes"], help="Consenso minimo varianti PaddleOCR")
ap.add_argument("--paddle-min-confidence", type=float, default=defaults["wms_paddle_min_confidence"], help="Confidenza minima PaddleOCR")
ap.add_argument("--fake-ocr-prefix", default=defaults["wms_fake_ocr_prefix"], help="Prefisso fallback OCR demo")
ap.add_argument("--undetermined-code-text", default=defaults["wms_undetermined_code_text"], help="Testo usato se OCR non determina il codice")
ap.add_argument("--fake-ocr-delay-sec", type=float, default=defaults["wms_fake_ocr_delay_sec"], help="Ritardo OCR demo/fallback")
ap.add_argument("--operator", default=defaults["wms_operator"], help="Operatore WMS demo")
ap.add_argument("--site", default=defaults["wms_site"], help="Sito/magazzino demo")
return ap.parse_args()
@app.get("/health")
def health() -> dict[str, str]:
return {"status": "ok"}
@app.post("/api/v1/navigation-snapshot")
async def receive_navigation_snapshot(
metadata: str = Form(...),
label_image: UploadFile = File(...),
gaylord_image: UploadFile | None = File(None),
) -> dict[str, object]:
started = time.perf_counter()
try:
meta = json.loads(metadata)
except json.JSONDecodeError:
meta = {"raw_metadata": metadata}
with state_lock:
server_state["counter"] = int(server_state["counter"]) + 1
counter = int(server_state["counter"])
request_id = str(meta.get("request_id") or f"server-{counter:06d}")
snapshot_id = meta.get("snapshot_id", counter)
received_dir = Path(str(server_state["received_dir"]))
request_dir = received_dir / f"{counter:06d}_{safe_name(request_id)}"
request_dir.mkdir(parents=True, exist_ok=True)
label_path = request_dir / safe_upload_name(label_image.filename, "label.jpg")
label_bytes = await label_image.read()
label_path.write_bytes(label_bytes)
gaylord_path = None
gaylord_bytes = b""
if gaylord_image is not None:
gaylord_path = request_dir / safe_upload_name(gaylord_image.filename, "gaylord.jpg")
gaylord_bytes = await gaylord_image.read()
gaylord_path.write_bytes(gaylord_bytes)
(request_dir / "metadata.json").write_text(
json.dumps(meta, indent=2, ensure_ascii=True),
encoding="utf-8",
)
image = cv2.imdecode(np.frombuffer(label_bytes, dtype=np.uint8), cv2.IMREAD_COLOR)
ocr_result = await asyncio.to_thread(run_server_ocr, image, int(snapshot_id), label_path)
fake_processing_sec = float(server_state["fake_processing_sec"])
if fake_processing_sec > 0:
await asyncio.sleep(fake_processing_sec)
status = choose_status(str(server_state["fake_ack_mode"]), counter)
udc_code = ocr_result.text if status == "ACK" else ""
message = (
f"codice valido su WMS: {udc_code}"
if status == "ACK"
else f"codice non riconosciuto: {ocr_result.text}"
)
processing_ms = (time.perf_counter() - started) * 1000.0
received_at = datetime.now(timezone.utc).isoformat()
wms_payload = {
"request_id": request_id,
"client_id": meta.get("client_id", ""),
"site": server_state["site"],
"operator": server_state["operator"],
"received_at": received_at,
"snapshot_id": snapshot_id,
"position": meta.get("simulated_position", ""),
"track_id": meta.get("track_id", ""),
"udc_code": udc_code,
"ocr_raw_text": ocr_result.raw_text,
"ocr_confidence": ocr_result.confidence,
"ocr_backend": ocr_result.backend,
"ocr_fallback_used": ocr_result.fallback_used,
"ocr_votes": ocr_result.votes,
"ocr_variant": ocr_result.variant,
"validation_status": status,
"validation_message": message,
"label_bbox": meta.get("label_bbox", []),
"gaylord_bbox": meta.get("gaylord_bbox", []),
"movement_vector_px": meta.get("movement_vector_px", {}),
"label_image_path": str(label_path),
"gaylord_image_path": str(gaylord_path) if gaylord_path else "",
}
(request_dir / "wms_payload.json").write_text(
json.dumps(wms_payload, indent=2, ensure_ascii=True),
encoding="utf-8",
)
update_ui_state(image, wms_payload)
log(
f"received request_id={request_id} snapshot={snapshot_id} "
f"label_bytes={len(label_bytes)} gaylord_bytes={len(gaylord_bytes)} "
f"ocr='{ocr_result.text}' status={status}"
)
return {
"status": status,
"request_id": request_id,
"message": message,
"ocr_text": udc_code,
"ocr_raw_text": ocr_result.raw_text,
"ocr_confidence": ocr_result.confidence,
"ocr_backend": ocr_result.backend,
"ocr_fallback_used": ocr_result.fallback_used,
"ocr_votes": ocr_result.votes,
"ocr_variant": ocr_result.variant,
"processing_ms": round(processing_ms, 1),
"saved_dir": str(request_dir),
"wms_payload_path": str(request_dir / "wms_payload.json"),
"label_image_path": str(label_path),
"gaylord_image_path": str(gaylord_path) if gaylord_path else "",
}
def run_server_ocr(image: np.ndarray | None, snapshot_id: int, label_path: Path | None = None) -> OcrServerResult:
fake_delay = float(server_state["fake_ocr_delay_sec"])
if fake_delay > 0:
time.sleep(fake_delay)
fallback = str(server_state["undetermined_code_text"])
if image is None:
return OcrServerResult(fallback, "", 0.0, "fallback", True)
if not bool(server_state["ocr_enabled"]) or str(server_state["ocr_mode"]) == "fake":
return OcrServerResult(fallback, fallback, 1.0, "fake", True)
if str(server_state["ocr_mode"]) == "paddleocr":
if label_path is None:
return OcrServerResult(fallback, "", 0.0, "paddleocr-missing-path", True)
return run_paddleocr_ocr(label_path, fallback)
if str(server_state["ocr_mode"]) == "tesseract":
return run_tesseract_ocr(image, fallback)
try:
reader = get_easyocr_reader()
ocr_input = preprocess_label_for_ocr(image)
results = reader.readtext(
ocr_input,
allowlist="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-",
detail=1,
paragraph=False,
)
except Exception as exc:
log(f"OCR fallback: {exc}")
return OcrServerResult(fallback, "", 0.0, "easyocr-error", True)
raw_parts: list[str] = []
best_text = ""
best_conf = 0.0
for item in results:
text = str(item[1]).strip()
conf = float(item[2]) if len(item) > 2 else 0.0
if text:
raw_parts.append(text)
candidate = normalize_ocr_code(text)
if candidate and conf >= best_conf:
best_text = candidate
best_conf = conf
raw_text = " ".join(raw_parts)
if not best_text:
digits = re.sub(r"\D+", "", raw_text)
if digits:
best_text = digits
best_conf = max(best_conf, 0.01)
if best_text:
return OcrServerResult(best_text, raw_text, best_conf, "easyocr", False)
return OcrServerResult(fallback, raw_text, best_conf, "easyocr-fallback", True)
def get_paddle_worker() -> PaddleOcrWorker:
global _paddle_worker
with _paddle_worker_lock:
if _paddle_worker is None:
_paddle_worker = PaddleOcrWorker(
python_path=str(server_state["paddle_python"]),
script_path=str(server_state["paddle_worker_script"]),
target_heights=str(server_state["paddle_target_heights"]),
variant_set=str(server_state["paddle_variant_set"]),
expected_digits=int(server_state["paddle_expected_digits"]),
min_votes=int(server_state["paddle_min_votes"]),
min_confidence=float(server_state["paddle_min_confidence"]),
)
return _paddle_worker
def close_paddle_worker() -> None:
global _paddle_worker
with _paddle_worker_lock:
worker = _paddle_worker
_paddle_worker = None
if worker is not None:
worker.close()
def run_paddleocr_ocr(label_path: Path, fallback: str) -> OcrServerResult:
try:
response = get_paddle_worker().predict(label_path)
except Exception as exc:
log(f"PaddleOCR fallback: {exc}")
close_paddle_worker()
return OcrServerResult(fallback, "", 0.0, "paddleocr-error", True)
text = str(response.get("text") or "")
best_text = str(response.get("best_text") or text)
raw_text = str(response.get("raw_text") or best_text)
confidence = float(response.get("confidence") or 0.0)
votes = int(response.get("votes") or 0)
variant = str(response.get("variant") or "")
if response.get("ok") and text:
return OcrServerResult(text, raw_text, confidence, "paddleocr", False, votes=votes, variant=variant)
reason = str(response.get("reason") or "fallback")
raw_with_reason = f"{raw_text} [{reason}]".strip()
return OcrServerResult(fallback, raw_with_reason, confidence, "paddleocr-fallback", True, votes=votes, variant=variant)
def run_tesseract_ocr(image: np.ndarray, fallback: str) -> OcrServerResult:
try:
import pytesseract
except Exception as exc:
log(f"Tesseract fallback: pytesseract non disponibile: {exc}")
return OcrServerResult(fallback, "", 0.0, "tesseract-unavailable", True)
tesseract_cmd = str(server_state.get("tesseract_cmd") or "").strip()
if tesseract_cmd:
if not Path(tesseract_cmd).exists():
log(f"Tesseract fallback: binario non trovato: {tesseract_cmd}")
return OcrServerResult(fallback, "", 0.0, "tesseract-missing", True)
pytesseract.pytesseract.tesseract_cmd = tesseract_cmd
elif shutil.which("tesseract") is None:
log("Tesseract fallback: tesseract.exe non trovato nel PATH")
return OcrServerResult(fallback, "", 0.0, "tesseract-missing", True)
variants = build_tesseract_variants(image)
candidates: list[tuple[str, float, str]] = []
config = "--psm 7 --oem 3 -c tessedit_char_whitelist=0123456789"
for name, variant in variants:
try:
data = pytesseract.image_to_data(
variant,
config=config,
output_type=pytesseract.Output.DICT,
)
except Exception as exc:
log(f"Tesseract fallback su variante {name}: {exc}")
return OcrServerResult(fallback, "", 0.0, "tesseract-error", True)
texts = [text.strip() for text in data.get("text", []) if text and text.strip()]
confs: list[float] = []
for conf in data.get("conf", []):
try:
value = float(conf)
except (TypeError, ValueError):
continue
if value >= 0:
confs.append(value / 100.0)
raw = "".join(texts)
digits = re.sub(r"\D+", "", raw)
confidence = sum(confs) / len(confs) if confs else 0.0
if digits:
candidates.append((digits, confidence, name))
if not candidates:
return OcrServerResult(fallback, "", 0.0, "tesseract-fallback", True)
votes: dict[str, list[float]] = {}
for digits, confidence, _ in candidates:
votes.setdefault(digits, []).append(confidence)
best_digits, best_confs = max(
votes.items(),
key=lambda item: (len(item[1]), sum(item[1]) / max(len(item[1]), 1)),
)
consensus_count = len(best_confs)
avg_conf = sum(best_confs) / consensus_count
raw_text = "; ".join(f"{digits}@{conf:.2f}/{name}" for digits, conf, name in candidates)
if consensus_count < 2 or avg_conf < 0.45:
return OcrServerResult(fallback, raw_text, avg_conf, "tesseract-ambiguous", True)
return OcrServerResult(best_digits, raw_text, avg_conf, "tesseract", False)
def build_tesseract_variants(image: np.ndarray) -> list[tuple[str, np.ndarray]]:
h, w = image.shape[:2]
scale = 4.0 if w < 500 else 2.0
resized = cuda_resize(image, (int(w * scale), int(h * scale)), interpolation=cv2.INTER_CUBIC)
gray = cuda_cvt_color(resized, cv2.COLOR_BGR2GRAY)
gray = cv2.bilateralFilter(gray, 7, 45, 45)
sharpen = cv2.addWeighted(gray, 1.6, cv2.GaussianBlur(gray, (0, 0), 1.2), -0.6, 0)
_, otsu = cv2.threshold(sharpen, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
adaptive = cv2.adaptiveThreshold(
sharpen,
255,
cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY,
31,
7,
)
return [
("gray", gray),
("sharpen", sharpen),
("otsu", otsu),
("adaptive", adaptive),
("otsu_inv", cv2.bitwise_not(otsu)),
]
_easyocr_reader = None
_easyocr_lock = threading.Lock()
def get_easyocr_reader():
global _easyocr_reader
with _easyocr_lock:
if _easyocr_reader is None:
import easyocr
_easyocr_reader = easyocr.Reader(["en"], gpu=False, verbose=False)
return _easyocr_reader
def preprocess_label_for_ocr(image: np.ndarray) -> np.ndarray:
h, w = image.shape[:2]
scale = 3.0 if w < 450 else 2.0
resized = cuda_resize(image, (int(w * scale), int(h * scale)), interpolation=cv2.INTER_CUBIC)
gray = cuda_cvt_color(resized, cv2.COLOR_BGR2GRAY)
gray = cv2.bilateralFilter(gray, 7, 45, 45)
return cuda_cvt_color(gray, cv2.COLOR_GRAY2BGR)
def normalize_ocr_code(text: str) -> str:
compact = re.sub(r"[^0-9A-Za-z-]+", "", text).strip("-")
digits = re.sub(r"\D+", "", compact)
return digits if digits else compact
def update_ui_state(image: np.ndarray | None, payload: dict[str, object]) -> None:
if not bool(server_state["ui_enabled"]):
return
lines = [
"ULTIMO PAYLOAD WMS",
f"request_id: {payload.get('request_id', '')}",
f"client_id: {payload.get('client_id', '')}",
f"site: {payload.get('site', '')}",
f"operator: {payload.get('operator', '')}",
f"received_at: {payload.get('received_at', '')}",
f"snapshot_id: {payload.get('snapshot_id', '')}",
f"position: {payload.get('position', '')}",
f"track_id: {payload.get('track_id', '')}",
f"udc_code: {payload.get('udc_code', '')}",
f"ocr_raw: {payload.get('ocr_raw_text', '')}",
f"ocr_backend: {payload.get('ocr_backend', '')}",
f"ocr_fallback: {payload.get('ocr_fallback_used', '')}",
f"ocr_votes: {payload.get('ocr_votes', '')}",
f"ocr_variant: {payload.get('ocr_variant', '')}",
f"status: {payload.get('validation_status', '')}",
f"label_bbox: {payload.get('label_bbox', '')}",
f"movement: {payload.get('movement_vector_px', '')}",
]
with ui_lock:
ui_state["image"] = None if image is None else image.copy()
ui_state["payload_lines"] = lines
ui_state["updated"] = True
def choose_status(mode: str, counter: int) -> str:
if mode == "always-nack":
return "NACK"
if mode == "alternate":
return "ACK" if counter % 2 == 1 else "NACK"
if mode == "random":
return "ACK" if random.random() >= 0.5 else "NACK"
return "ACK"
def safe_upload_name(filename: str | None, fallback: str) -> str:
name = Path(filename or fallback).name
return safe_name(name) or fallback
def safe_name(value: str) -> str:
allowed = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789._-"
return "".join(ch if ch in allowed else "_" for ch in value)[:160]
def server_ui_loop() -> None:
try:
cv2.namedWindow("wms immagine ricevuta", cv2.WINDOW_NORMAL)
cv2.namedWindow("wms payload", cv2.WINDOW_NORMAL)
except cv2.error as exc:
log(f"UI OpenCV disabilitata: highgui non disponibile ({exc})")
with ui_lock:
ui_state["stop"] = True
return
cv2.resizeWindow("wms immagine ricevuta", 640, 360)
cv2.resizeWindow("wms payload", 900, 560)
cv2.moveWindow("wms immagine ricevuta", 40, 40)
cv2.moveWindow("wms payload", 720, 40)
while True:
with ui_lock:
if ui_state["stop"]:
break
image = None if ui_state["image"] is None else ui_state["image"].copy()
lines = list(ui_state["payload_lines"])
if image is None:
image_display = np.full((360, 640, 3), 240, dtype=np.uint8)
cv2.putText(
image_display,
"In attesa immagine etichetta",
(30, 180),
cv2.FONT_HERSHEY_SIMPLEX,
0.9,
(0, 0, 0),
2,
cv2.LINE_AA,
)
else:
image_display = resize_preview(image, 640)
cv2.imshow("wms immagine ricevuta", image_display)
cv2.imshow("wms payload", draw_payload_window(lines))
key = cv2.waitKey(100) & 0xFF
if key in (27, ord("q")):
with ui_lock:
ui_state["stop"] = True
break
cv2.destroyWindow("wms immagine ricevuta")
cv2.destroyWindow("wms payload")
def resize_preview(frame: np.ndarray, max_width: int) -> np.ndarray:
h, w = frame.shape[:2]
if max_width <= 0 or w <= max_width:
return frame
scale = max_width / float(w)
return cuda_resize(frame, (int(w * scale), int(h * scale)), interpolation=cv2.INTER_LINEAR)
def draw_payload_window(lines: list[str]) -> np.ndarray:
canvas = np.full((560, 900, 3), 245, dtype=np.uint8)
y = 42
for idx, line in enumerate(lines[:18]):
color = (0, 0, 160) if idx == 0 else (0, 0, 0)
scale = 0.9 if idx == 0 else 0.63
thickness = 2 if idx == 0 else 1
cv2.putText(
canvas,
str(line)[:110],
(24, y),
cv2.FONT_HERSHEY_SIMPLEX,
scale,
color,
thickness,
cv2.LINE_AA,
)
y += 34 if idx == 0 else 30
return canvas
def main() -> int:
args = parse_args()
server_state["received_dir"] = args.received_dir
server_state["fake_ack_mode"] = args.fake_ack_mode
server_state["fake_processing_sec"] = args.fake_processing_sec
server_state["ui_enabled"] = args.ui_enabled
server_state["ocr_enabled"] = args.ocr_enabled
server_state["ocr_mode"] = args.ocr_mode
server_state["tesseract_cmd"] = args.tesseract_cmd
server_state["paddle_python"] = args.paddle_python
server_state["paddle_worker_script"] = args.paddle_worker_script
server_state["paddle_target_heights"] = args.paddle_target_heights
server_state["paddle_variant_set"] = args.paddle_variant_set
server_state["paddle_expected_digits"] = args.paddle_expected_digits
server_state["paddle_min_votes"] = args.paddle_min_votes
server_state["paddle_min_confidence"] = args.paddle_min_confidence
server_state["fake_ocr_prefix"] = args.fake_ocr_prefix
server_state["undetermined_code_text"] = args.undetermined_code_text
server_state["fake_ocr_delay_sec"] = args.fake_ocr_delay_sec
server_state["operator"] = args.operator
server_state["site"] = args.site
Path(args.received_dir).mkdir(parents=True, exist_ok=True)
log(
f"FlyWMS WMS demo server host={args.host} port={args.port} "
f"mode={args.fake_ack_mode} received_dir={args.received_dir}"
)
log(f"OpenCV CUDA: {'attivo' if OPENCV_CUDA_AVAILABLE else 'non disponibile'}")
ui_thread = None
if args.ui_enabled:
ui_thread = threading.Thread(target=server_ui_loop, name="wms-server-ui", daemon=True)
ui_thread.start()
try:
uvicorn.run(app, host=args.host, port=args.port, log_level="info")
finally:
close_paddle_worker()
with ui_lock:
ui_state["stop"] = True
if ui_thread is not None:
ui_thread.join(timeout=2.0)
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -1,116 +1,255 @@
# FlyWMS handoff - GUI navigazione
# FlyWMS handoff
Data: 2026-05-15
Data: 2026-05-18
## Stato repository
- Branch: `master`
- Remote: `origin` su Gitea
- Ultimo lavoro: aggiunta shell DearPyGUI separata per la simulazione dimostrativa di `flywms_navigation.py`.
- Commit da creare per questa milestone: `pipeline in linea single thread`
- Tag git previsto per questa milestone: `pipeline-in-linea-single-thread`
## Contesto recuperato
## Stato funzionale attuale
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':
La base di lavoro e' ancora una pipeline sostanzialmente single-thread, con logica completa di navigazione/demo in Python.
- 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()`.
Componenti principali presenti:
## Decisione architetturale
- [flywms_navigation.py](C:/devel/flywms/flywms_navigation.py)
- detector Ultralytics YOLO
- tracking geometrico
- logica snapshot gaylord + etichetta
- macchina a stati demo per movimento verso etichetta, stabilizzazione, scatto, ritorno, attesa WMS
- client WMS asincrono
- log prestazioni dettagliato per frame
- profilo `benchmark`
- [flywms_wms_server.py](C:/devel/flywms/flywms_wms_server.py)
- server demo FastAPI
- ricezione payload e immagini
- OCR lato server
- salvataggio payload/immagini ricevute
- [flywms_paddleocr_worker.py](C:/devel/flywms/flywms_paddleocr_worker.py)
- worker persistente per PaddleOCR
- [flywms_navigation_gui.py](C:/devel/flywms/flywms_navigation_gui.py)
- shell DearPyGUI separata dalla logica principale
La GUI DearPyGUI e' stata tenuta in un file separato, `flywms_navigation_gui.py`.
## Stato GPU / librerie
Motivo: non toccare il ciclo originale e non rischiare regressioni nella logica di navigazione. La GUI importa `flywms_navigation.py` e riusa:
Verificato su questo PC:
- `UltralyticsDetector`;
- `LightweightTracker`;
- `NavigationController`;
- `draw_navigation_debug`;
- configurazione da `flywms_navigation.ini`.
- OpenCV con CUDA e GUI Win32 attivi
- cuDNN attivo
- YOLO/Ultralytics su GPU RTX 3050
- `yolo_half = true` supportato
In pratica `flywms_navigation.py` resta il simulatore CLI/OpenCV funzionante; `flywms_navigation_gui.py` e' una shell dimostrativa sopra la stessa logica.
PaddleOCR al momento resta nel worker dedicato e non e' il focus della prossima rifattorizzazione.
## Implementato
## Configurazione importante
File nuovo:
File: [flywms_navigation.ini](C:/devel/flywms/flywms_navigation.ini)
- `flywms_navigation_gui.py`
Valori chiave attuali:
Funzioni principali:
- `ultralytics_device = 0`
- `yolo_half = true`
- `preview_fps = 24.0`
- `yolo_fps = 15.0`
- `benchmark_mode = false`
- `benchmark_preview_fps = 30.0`
- `perf_log_path = tempistiche.txt`
- `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.
Il video di lavoro locale usato durante i test recenti e' `testhd2_edit.mp4`.
Nota: i file video locali non sono necessariamente da committare.
DearPyGUI e' stato installato nell'ambiente Python corrente con:
## Log e benchmark
### Log demo con UI
File:
- [tempistiche.txt](C:/devel/flywms/tempistiche.txt)
Risultati principali gia' analizzati:
- video sorgente: circa `10.97 min` a `30 fps`
- run demo completo molto piu' lento a causa di:
- UI OpenCV pesante
- pause snapshot
- attese WMS
Numeri rilevanti del run demo:
- `demo_active_s = 1092.85 s`
- `demo_draw_ui_s = 597.18 s`
Conclusione:
- la UI OpenCV e il rendering accessorio pesano moltissimo
- la pipeline di calcolo non deve piu' essere accoppiata alla UI in questo modo
### Benchmark headless
File:
- [tempistiche-benchmark.txt](C:/devel/flywms/tempistiche-benchmark.txt)
Profilo usato:
```powershell
python -m pip install dearpygui
python flywms_navigation.py --benchmark-mode
```
## Verifiche eseguite
Caratteristiche del profilo benchmark:
Compilazione:
- nessuna finestra OpenCV
- `preview_target = 30 fps`
- log tempi separato
Risultati benchmark gia' analizzati:
- `benchmark_active_s = 762.13 s`
- durata video reale: `658.3 s`
- scarto residuo netto: circa `103.8 s` = `1.73 min`
- `active_fps = 25.97`
- `active_yolo_fps = 12.69`
Tempi medi in regime stabile:
- `mean_loop_ms = 36.90`
- `mean_read_ms = 6.10`
- `mean_yolo_ms = 31.20`
- `mean_track_ms = 0.18`
Conclusione benchmark:
- togliere la UI fa recuperare circa `5.5 min`
- la pipeline core e' vicina all'obiettivo ma non ancora allineata alla durata reale del video
- il residuo e' dovuto soprattutto a:
- inferenza YOLO sotto il target dei `15 fps`
- `cap.read()`
- struttura single-thread del loop
## Decisione architetturale emersa oggi
La prossima fase non deve essere "ottimizzare la GUI attuale", ma formalizzare una separazione netta dei ruoli.
### Ruoli da formalizzare
`Capture`
- acquisisce frame
- acquisisce posa drone nello stesso istante
- produce un pacchetto coerente `frame + pose + timestamp + frame_id`
`Vision / YOLO`
- riceve il frame gia' corredato di posa
- produce bbox, associazioni, offset, misure visive
- non decide il workflow
- non parla direttamente con WMS
`Navigation / Control`
- interpreta i risultati visivi
- decide comandi di scansione, centraggio, scatto, ritorno, ripresa
- e' il vero orchestratore della missione
`Drone I/O`
- invia i comandi al drone o al simulatore
- riceve stato / ack / posa aggiornata
- esegue lo scatto reale
`WMS Client`
- riceve payload finale pronto
- invia al WMS
- non decide il volo
## Punto concettuale gia' chiarito
Ogni frame che YOLO riceve deve avere gia' associata la posa del drone.
Quindi il thread di cattura non passa solo l'immagine, ma un record strutturato del tipo:
```text
CapturedFrame {
frame_id
timestamp
image
pose_x
pose_y
pose_z
yaw
pitch
roll
}
```
YOLO non deve rileggere la posizione "corrente" a posteriori.
La posa da usare per interpretare quel frame e' quella congelata al momento della cattura.
## Domande aperte per domani
1. Formalizzare le strutture dati tra thread:
- `CapturedFrame`
- `VisionResult`
- `DroneCommand`
- `CommandResult`
- `WmsPayload`
2. Disegnare la pipeline multi-thread minima:
- `capture_thread`
- `vision_thread`
- `navigation/control_thread`
- `wms_thread`
3. Decidere se il primo passo implementativo deve essere:
- solo `capture + vision` separati
- oppure `capture + vision + command pipeline`
4. Formalizzare bene il flusso "scatto etichetta":
- il controller decide
- il layer drone/scatto produce immagine e posa
- il payload finale va al WMS senza far diventare YOLO l'orchestratore del sistema
## Raccomandazione per il prossimo avvio
Domani, in una nuova chat, partire da qui:
1. leggere questo `handoff.md`
2. leggere gli ultimi aggiornamenti:
- [aggiornamento-2026-05-18-18-30.md](C:/devel/flywms/aggiornamento-2026-05-18-18-30.md)
- [aggiornamento-2026-05-18-19-14.md](C:/devel/flywms/aggiornamento-2026-05-18-19-14.md)
- [aggiornamento-2026-05-18-19-58.md](C:/devel/flywms/aggiornamento-2026-05-18-19-58.md)
3. cominciare scrivendo le specifiche formali di:
- responsabilita'
- messaggi tra thread
- ownership dei dati
- chi comanda chi
## Comandi utili
Server WMS:
```powershell
python -m py_compile flywms_navigation_gui.py flywms_navigation.py
python flywms_wms_server.py
```
Test motore GUI senza finestra persistente:
Demo navigazione con UI OpenCV:
```text
state1 1 (720, 1280, 3)
state2 2 (720, 1280, 3)
```powershell
python flywms_navigation.py --wms-enabled
```
Test aggiornamento UI/texture:
Benchmark headless:
```text
ui step ok
960 540
```powershell
python flywms_navigation.py --benchmark-mode
```
Avvio manuale usato:
GUI DearPyGUI:
```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.