Compare commits
3 Commits
98b43ce903
...
aa17d7ffd3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa17d7ffd3 | ||
|
|
e86c05a885 | ||
|
|
f728524ee6 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -12,3 +12,4 @@ dataset_yolo/labels/*_backup_before_remap_*/
|
||||
runs/flywms_dataset_check/
|
||||
runs/flywms_dataset_check_1epoch/
|
||||
navigate_snapshots*/
|
||||
wms_received*/
|
||||
|
||||
99
aggiornamento-2026-05-16-10-14.md
Normal file
99
aggiornamento-2026-05-16-10-14.md
Normal 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.
|
||||
41
aggiornamento-2026-05-16-10-37.md
Normal file
41
aggiornamento-2026-05-16-10-37.md
Normal 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
|
||||
```
|
||||
30
aggiornamento-2026-05-16-10-47.md
Normal file
30
aggiornamento-2026-05-16-10-47.md
Normal 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.
|
||||
50
aggiornamento-2026-05-16-10-49.md
Normal file
50
aggiornamento-2026-05-16-10-49.md
Normal 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.
|
||||
110
aggiornamento-2026-05-16-11-04.md
Normal file
110
aggiornamento-2026-05-16-11-04.md
Normal 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.
|
||||
132
aggiornamento-2026-05-16-11-12.md
Normal file
132
aggiornamento-2026-05-16-11-12.md
Normal 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.
|
||||
49
aggiornamento-2026-05-16-11-29.md
Normal file
49
aggiornamento-2026-05-16-11-29.md
Normal 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.
|
||||
62
aggiornamento-2026-05-16-12-04.md
Normal file
62
aggiornamento-2026-05-16-12-04.md
Normal 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.
|
||||
116
aggiornamento-2026-05-16-12-08.md
Normal file
116
aggiornamento-2026-05-16-12-08.md
Normal 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`.
|
||||
43
aggiornamento-2026-05-16-12-10.md
Normal file
43
aggiornamento-2026-05-16-12-10.md
Normal 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)
|
||||
```
|
||||
188
aggiornamento-2026-05-16-13-01.md
Normal file
188
aggiornamento-2026-05-16-13-01.md
Normal 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.
|
||||
26
aggiornamento-2026-05-16-14-34.md
Normal file
26
aggiornamento-2026-05-16-14-34.md
Normal 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`.
|
||||
75
aggiornamento-2026-05-16-17-18.md
Normal file
75
aggiornamento-2026-05-16-17-18.md
Normal 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"
|
||||
```
|
||||
84
aggiornamento-2026-05-16-17-19.md
Normal file
84
aggiornamento-2026-05-16-17-19.md
Normal 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.
|
||||
69
aggiornamento-2026-05-16-19-46.md
Normal file
69
aggiornamento-2026-05-16-19-46.md
Normal 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.
|
||||
|
||||
25
aggiornamento-2026-05-16-19-52.md
Normal file
25
aggiornamento-2026-05-16-19-52.md
Normal 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.
|
||||
|
||||
94
aggiornamento-2026-05-16-20-12.md
Normal file
94
aggiornamento-2026-05-16-20-12.md
Normal 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.
|
||||
|
||||
29
aggiornamento-2026-05-17-09-30.md
Normal file
29
aggiornamento-2026-05-17-09-30.md
Normal 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.
|
||||
|
||||
53
aggiornamento-2026-05-17-09-39.md
Normal file
53
aggiornamento-2026-05-17-09-39.md
Normal 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.
|
||||
|
||||
18
aggiornamento-2026-05-17-10-08.md
Normal file
18
aggiornamento-2026-05-17-10-08.md
Normal 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.
|
||||
|
||||
20
aggiornamento-2026-05-17-10-37.md
Normal file
20
aggiornamento-2026-05-17-10-37.md
Normal 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.
|
||||
|
||||
82
aggiornamento-2026-05-17-11-21.md
Normal file
82
aggiornamento-2026-05-17-11-21.md
Normal 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.
|
||||
|
||||
46
aggiornamento-2026-05-17-14-48.md
Normal file
46
aggiornamento-2026-05-17-14-48.md
Normal 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.
|
||||
|
||||
52
aggiornamento-2026-05-17-14-51.md
Normal file
52
aggiornamento-2026-05-17-14-51.md
Normal 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.
|
||||
|
||||
75
aggiornamento-2026-05-17-20-36.md
Normal file
75
aggiornamento-2026-05-17-20-36.md
Normal 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.
|
||||
|
||||
166
aggiornamento-2026-05-17-20-57.md
Normal file
166
aggiornamento-2026-05-17-20-57.md
Normal 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.
|
||||
196
aggiornamento-2026-05-18-14-18.md
Normal file
196
aggiornamento-2026-05-18-14-18.md
Normal 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.
|
||||
115
aggiornamento-2026-05-18-14-39.md
Normal file
115
aggiornamento-2026-05-18-14-39.md
Normal 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.
|
||||
166
aggiornamento-2026-05-18-14-58.md
Normal file
166
aggiornamento-2026-05-18-14-58.md
Normal 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.
|
||||
50
aggiornamento-2026-05-18-15-28.md
Normal file
50
aggiornamento-2026-05-18-15-28.md
Normal 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.
|
||||
107
aggiornamento-2026-05-18-15-42.md
Normal file
107
aggiornamento-2026-05-18-15-42.md
Normal 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.
|
||||
82
aggiornamento-2026-05-18-15-53.md
Normal file
82
aggiornamento-2026-05-18-15-53.md
Normal 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.
|
||||
43
aggiornamento-2026-05-18-18-15.md
Normal file
43
aggiornamento-2026-05-18-18-15.md
Normal 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.
|
||||
67
aggiornamento-2026-05-18-18-30.md
Normal file
67
aggiornamento-2026-05-18-18-30.md
Normal 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.
|
||||
58
aggiornamento-2026-05-18-19-14.md
Normal file
58
aggiornamento-2026-05-18-19-14.md
Normal 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.
|
||||
97
aggiornamento-2026-05-18-19-58.md
Normal file
97
aggiornamento-2026-05-18-19-58.md
Normal 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
|
||||
86
aggiornamento-2026-05-29-16-38.md
Normal file
86
aggiornamento-2026-05-29-16-38.md
Normal file
@@ -0,0 +1,86 @@
|
||||
# Aggiornamento 2026-05-29 16:38
|
||||
|
||||
## Step 1 - Core / Observer
|
||||
|
||||
Completati:
|
||||
|
||||
- documento di progetto Step 1 in [step1_core_observer_design.md](/C:/devel/flywms/step1_core_observer_design.md)
|
||||
- separazione iniziale tra core runtime e observer esterno
|
||||
- configurazione observer aggiunta in [flywms_navigation.ini](/C:/devel/flywms/flywms_navigation.ini)
|
||||
- nuovo entrypoint observer in [flywms_navigation_observer.py](/C:/devel/flywms/flywms_navigation_observer.py)
|
||||
|
||||
## Modifiche implementate
|
||||
|
||||
In [flywms_navigation.py](/C:/devel/flywms/flywms_navigation.py):
|
||||
|
||||
- aggiunto `ObserverPublisher`
|
||||
- il core puo' pubblicare su `localhost`:
|
||||
- telemetria strutturata
|
||||
- preview JPEG a bassa frequenza
|
||||
- quando `observer_enabled = true`:
|
||||
- la UI locale integrata viene disattivata
|
||||
- il core non aspetta l'observer
|
||||
- l'observer puo' essere assente o disconnettersi senza bloccare la pipeline
|
||||
- la sequenza snapshot headless e' stata resa osservabile per fasi:
|
||||
- `move`
|
||||
- `stabilize`
|
||||
- `capture`
|
||||
- `return`
|
||||
- `wait_wms`
|
||||
|
||||
In [flywms_navigation_observer.py](/C:/devel/flywms/flywms_navigation_observer.py):
|
||||
|
||||
- processo separato
|
||||
- connessione TCP locale al core
|
||||
- visualizzazione di:
|
||||
- preview navigazione
|
||||
- pannello comandi/stato
|
||||
- snapshot
|
||||
- crop etichetta
|
||||
|
||||
## Verifiche eseguite
|
||||
|
||||
1. Compilazione Python:
|
||||
|
||||
```bash
|
||||
python -m py_compile flywms_navigation.py flywms_navigation_observer.py
|
||||
```
|
||||
|
||||
Esito: ok.
|
||||
|
||||
2. Smoke test core con observer attivo ma non collegato:
|
||||
|
||||
```bash
|
||||
python flywms_navigation.py --video testhd2_edit.mp4 --observer-enabled --max-frames 30
|
||||
```
|
||||
|
||||
Esito:
|
||||
|
||||
- il core parte regolarmente
|
||||
- la UI locale viene disattivata come previsto
|
||||
- il publisher si mette in ascolto su `127.0.0.1:8765`
|
||||
- il run chiude senza blocchi anche senza observer collegato
|
||||
|
||||
## Stato attuale
|
||||
|
||||
Lo Step 1 e' implementato nella sua forma iniziale e il lato critico del core e' verificato.
|
||||
|
||||
Resta da fare una verifica manuale della GUI observer in esecuzione reale, cioe':
|
||||
|
||||
1. avviare `flywms_navigation_observer.py`
|
||||
2. avviare il core con `--observer-enabled`
|
||||
3. controllare fluidita', frequenza preview e correttezza dei pannelli
|
||||
|
||||
## Comandi di avvio previsti
|
||||
|
||||
Observer:
|
||||
|
||||
```bash
|
||||
python flywms_navigation_observer.py
|
||||
```
|
||||
|
||||
Core:
|
||||
|
||||
```bash
|
||||
python flywms_navigation.py --video testhd2_edit.mp4 --observer-enabled
|
||||
```
|
||||
75
aggiornamento-2026-05-30-18-12.md
Normal file
75
aggiornamento-2026-05-30-18-12.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# Aggiornamento 2026-05-30 18:12
|
||||
|
||||
## Step 2 - Capture / Inference
|
||||
|
||||
Completati:
|
||||
|
||||
- documento di progetto Step 2 in [step2_capture_inference_design.md](/C:/devel/flywms/step2_capture_inference_design.md)
|
||||
- introduzione del pacchetto `CapturedFrame`
|
||||
- introduzione del `CaptureWorker`
|
||||
- propagazione della posa congelata dal capture fino allo snapshot
|
||||
|
||||
## Modifiche implementate
|
||||
|
||||
In [flywms_navigation.py](/C:/devel/flywms/flywms_navigation.py):
|
||||
|
||||
- aggiunto `CapturedFrame` con:
|
||||
- `frame_id`
|
||||
- `timestamp`
|
||||
- `video_time_sec`
|
||||
- `frame`
|
||||
- `pose`
|
||||
- `read_ms`
|
||||
- aggiunto `sample_demo_pose(...)`
|
||||
- aggiunto `CaptureWorker` con coda `maxsize=1`
|
||||
- politica di coda: `latest frame wins`
|
||||
- `cap.read()` spostato fuori dal loop principale
|
||||
- `NavigationController.process_track(...)` ora consuma `CapturedFrame`
|
||||
- `CandidateSnapshot` e `NavigationSnapshot` ora portano anche `capture_pose`
|
||||
- metadata snapshot JSON aggiornati: `drone_pose_simulated` ora deriva dal frame catturato
|
||||
|
||||
## Correzione importante sulle metriche
|
||||
|
||||
Con `latest frame wins`, il `frame_id` della sorgente puo' saltare. Per questo:
|
||||
|
||||
- `frame_id` resta l'identificativo del frame sorgente;
|
||||
- `processed_frames` misura invece quanti frame il core ha davvero consumato.
|
||||
|
||||
Le metriche `loop_fps` e i log prestazionali ora usano `processed_frames`, non `frame_id`.
|
||||
|
||||
## Verifiche eseguite
|
||||
|
||||
1. Compilazione Python:
|
||||
|
||||
```bash
|
||||
python -m py_compile flywms_navigation.py flywms_navigation_observer.py
|
||||
```
|
||||
|
||||
Esito: ok.
|
||||
|
||||
2. Smoke test core senza observer:
|
||||
|
||||
```bash
|
||||
python flywms_navigation.py --video testhd2_edit.mp4 --max-frames 30
|
||||
```
|
||||
|
||||
Esito: ok.
|
||||
|
||||
3. Smoke test core con observer attivo:
|
||||
|
||||
```bash
|
||||
python flywms_navigation.py --video testhd2_edit.mp4 --observer-enabled --max-frames 30
|
||||
```
|
||||
|
||||
Esito: ok.
|
||||
|
||||
## Nota operativa
|
||||
|
||||
Dopo lo Step 2, `--max-frames` resta un limite sui frame della sorgente video, non sui frame effettivamente processati.
|
||||
|
||||
Questo va bene per smoke test rapidi, ma non e' il modo corretto per giudicare la resa finale della pipeline realtime. Il benchmark successivo va fatto sulla durata completa del video, confrontando:
|
||||
|
||||
- durata nominale video;
|
||||
- durata effettiva demo;
|
||||
- `processed_frames`;
|
||||
- `yolo_fps` reale.
|
||||
73
aggiornamento-2026-05-30-19-08.md
Normal file
73
aggiornamento-2026-05-30-19-08.md
Normal file
@@ -0,0 +1,73 @@
|
||||
# Aggiornamento 2026-05-30 19:08
|
||||
|
||||
## Step 3 - Inferenza adattiva
|
||||
|
||||
Documentazione:
|
||||
|
||||
- [step3_adaptive_inference_design.md](/C:/devel/flywms/step3_adaptive_inference_design.md)
|
||||
|
||||
Implementato:
|
||||
|
||||
- scheduler YOLO adattivo con tre stati:
|
||||
- `idle`
|
||||
- `tracking`
|
||||
- `critical`
|
||||
- parametri configurabili:
|
||||
- `adaptive_yolo_enabled`
|
||||
- `idle_yolo_fps`
|
||||
- `tracking_yolo_fps`
|
||||
- `critical_yolo_fps`
|
||||
- telemetria observer aggiornata con `yolo_mode`
|
||||
|
||||
## Esito benchmark
|
||||
|
||||
Run completo con:
|
||||
|
||||
- `adaptive_yolo_enabled = true`
|
||||
- `idle = 8`
|
||||
- `tracking = 12`
|
||||
- `critical = 15`
|
||||
|
||||
Risultato finale:
|
||||
|
||||
- durata demo totale: `874.73 s`
|
||||
- durata demo netta senza WMS: `796.71 s`
|
||||
- durata video: `658.14 s`
|
||||
- scostamento netto: `+138.57 s` = `+21.1%`
|
||||
|
||||
Confronto con run precedente senza adattivo:
|
||||
|
||||
- netto precedente: `756.18 s`
|
||||
- netto con adattivo: `796.71 s`
|
||||
- peggioramento: circa `+40.5 s`
|
||||
|
||||
## Interpretazione
|
||||
|
||||
La riduzione della frequenza YOLO nei tratti idle non ha compensato l'effetto collaterale introdotto sul comportamento del tracking e della logica snapshot.
|
||||
|
||||
In pratica:
|
||||
|
||||
- il costo medio YOLO e' sceso;
|
||||
- ma il comportamento globale della missione e' peggiorato;
|
||||
- quindi questa prima forma di inferenza adattiva non e' vantaggiosa.
|
||||
|
||||
## Decisione
|
||||
|
||||
La feature resta disponibile nel codice come opzione sperimentale, ma viene disattivata di default:
|
||||
|
||||
- [flywms_navigation.py](/C:/devel/flywms/flywms_navigation.py)
|
||||
- [flywms_navigation.ini](/C:/devel/flywms/flywms_navigation.ini)
|
||||
|
||||
Default attuale:
|
||||
|
||||
- `adaptive_yolo_enabled = false`
|
||||
|
||||
## Conclusione
|
||||
|
||||
Lo Step 3, nella forma provata oggi, non va adottato come ottimizzazione di default.
|
||||
|
||||
Se si vorra' tornare su questa strada, servira' una strategia piu' mirata, ad esempio:
|
||||
|
||||
- adattamento solo in assenza totale di target per un certo tempo;
|
||||
- nessuna riduzione di frequenza quando esistono track vive;
|
||||
- oppure un criterio basato su zone del video predefinite, non solo sullo stato istantaneo delle track.
|
||||
61
aggiornamento-2026-06-03-21-10.md
Normal file
61
aggiornamento-2026-06-03-21-10.md
Normal file
@@ -0,0 +1,61 @@
|
||||
## Aggiornamento 2026-06-03 21:10
|
||||
|
||||
### Obiettivo
|
||||
Introdurre una UI DearPyGUI solo per i componenti esterni:
|
||||
- observer di `flywms_navigation`
|
||||
- server `flywms_wms_server`
|
||||
|
||||
Lasciare invariata la parte intelligente del core:
|
||||
- acquisizione
|
||||
- inferenza
|
||||
- tracking
|
||||
- logica di navigazione e snapshot
|
||||
|
||||
### Baseline salvata
|
||||
- Commit locale: `e86c05a`
|
||||
- Tag locale: `gui-observer-in-opencv`
|
||||
|
||||
Nota: il push su Gitea non e' riuscito per problema di risoluzione DNS dell'host remoto.
|
||||
|
||||
### Documentazione aggiunta
|
||||
- `dearpygui_observer_server_spec.md`
|
||||
|
||||
### Modifiche implementate
|
||||
|
||||
#### `flywms_navigation_observer.py`
|
||||
- aggiunto supporto backend UI:
|
||||
- `dearpygui`
|
||||
- `opencv`
|
||||
- `auto`
|
||||
- mantenuto il protocollo socket esistente con il core
|
||||
- aggiunta UI DearPyGUI con:
|
||||
- preview principale navigazione
|
||||
- preview snapshot
|
||||
- preview crop etichetta
|
||||
- pannello stato
|
||||
- pannello metriche
|
||||
- pannello comandi
|
||||
- mantenuto fallback OpenCV
|
||||
|
||||
#### `flywms_wms_server.py`
|
||||
- aggiunto supporto backend UI:
|
||||
- `dearpygui`
|
||||
- `opencv`
|
||||
- `auto`
|
||||
- lasciata invariata la logica FastAPI/OCR/ACK
|
||||
- aggiunta UI DearPyGUI con:
|
||||
- immagine ricevuta
|
||||
- stato server
|
||||
- payload OCR / WMS
|
||||
- mantenuto fallback OpenCV
|
||||
|
||||
### Verifiche eseguite
|
||||
- `python -m py_compile flywms_navigation_observer.py flywms_wms_server.py`
|
||||
- import e selezione backend:
|
||||
- `flywms_navigation_observer.choose_backend('auto') -> dearpygui`
|
||||
- `flywms_wms_server.choose_ui_backend('auto') -> dearpygui`
|
||||
|
||||
### Stato attuale
|
||||
- core intelligente invariato
|
||||
- observer e server pronti per prova con DearPyGUI
|
||||
- fallback OpenCV ancora disponibile per debug
|
||||
103
dearpygui_observer_server_spec.md
Normal file
103
dearpygui_observer_server_spec.md
Normal file
@@ -0,0 +1,103 @@
|
||||
# DearPyGUI UI Spec - Observer e WMS Server
|
||||
|
||||
## Scopo
|
||||
|
||||
Rendere la demo piu' ordinata e leggibile spostando la presentazione visuale da OpenCV a DearPyGUI nei soli componenti esterni:
|
||||
|
||||
- `flywms_navigation_observer.py`
|
||||
- `flywms_wms_server.py`
|
||||
|
||||
La parte intelligente resta invariata:
|
||||
|
||||
- `flywms_navigation.py`
|
||||
- tracking
|
||||
- snapshot logic
|
||||
- WMS client
|
||||
- logica OCR / FastAPI lato server
|
||||
|
||||
## Principio architetturale
|
||||
|
||||
La UI non entra nel percorso critico.
|
||||
|
||||
### Observer
|
||||
|
||||
- riceve gia' telemetria e preview dal core;
|
||||
- DearPyGUI sostituisce solo le finestre OpenCV;
|
||||
- il protocollo socket e il payload restano invariati.
|
||||
|
||||
### WMS Server
|
||||
|
||||
- continua a ricevere upload e a fare OCR come oggi;
|
||||
- DearPyGUI sostituisce solo le finestre OpenCV di monitoraggio;
|
||||
- nessuna modifica al contratto HTTP.
|
||||
|
||||
## Layout Observer
|
||||
|
||||
### Colonna sinistra
|
||||
|
||||
- preview principale `navigate`
|
||||
|
||||
### Colonna destra superiore
|
||||
|
||||
- snapshot OCR payload
|
||||
- crop etichetta
|
||||
|
||||
### Colonna destra inferiore
|
||||
|
||||
- stato / metriche
|
||||
- comando corrente
|
||||
- lista comandi
|
||||
|
||||
### Metriche minime
|
||||
|
||||
- `frame_id`
|
||||
- `processed_frames` se presente
|
||||
- `loop_fps`
|
||||
- `yolo_fps`
|
||||
- `last_yolo_ms`
|
||||
- `det_count`
|
||||
- `track_count`
|
||||
- `snapshot_counter`
|
||||
- `yolo_mode` se disponibile
|
||||
- `motion_text`
|
||||
|
||||
## Layout WMS Server
|
||||
|
||||
### Colonna sinistra
|
||||
|
||||
- ultima immagine ricevuta dal server
|
||||
|
||||
### Colonna destra
|
||||
|
||||
- payload request corrente
|
||||
- esito OCR
|
||||
- esito ACK/NACK
|
||||
- contatore richieste
|
||||
|
||||
## Requisiti UI
|
||||
|
||||
- finestre ridimensionabili
|
||||
- nessun blocco sul core/server
|
||||
- refresh periodico leggero
|
||||
- fallback a OpenCV se DearPyGUI non e' disponibile
|
||||
|
||||
## Strategia di implementazione
|
||||
|
||||
### Fase 1
|
||||
|
||||
- introdurre backend DearPyGUI opzionale per observer
|
||||
- introdurre backend DearPyGUI opzionale per WMS server
|
||||
- mantenere OpenCV come fallback
|
||||
|
||||
### Fase 2
|
||||
|
||||
- rifinire il layout
|
||||
- eventuali colori/stati piu' chiari
|
||||
- eventuali controlli aggiuntivi
|
||||
|
||||
## Criterio di successo
|
||||
|
||||
1. la demo risulta piu' ordinata;
|
||||
2. il core intelligente non cambia comportamento;
|
||||
3. observer e server possono essere lanciati senza finestre OpenCV;
|
||||
4. il fallback OpenCV resta disponibile in caso di problema.
|
||||
@@ -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 = 3.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,12 +312,38 @@ 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.
|
||||
; Default se non indicato: 15.0
|
||||
yolo_fps = 15.0
|
||||
|
||||
; OBBLIGATORIO: no.
|
||||
; Ruolo: se true, la frequenza YOLO viene adattata allo stato delle track.
|
||||
; Default se non indicato: true
|
||||
adaptive_yolo_enabled = false
|
||||
|
||||
; OBBLIGATORIO: no.
|
||||
; Ruolo: FPS YOLO quando non ci sono track attive.
|
||||
; Default se non indicato: 8.0
|
||||
idle_yolo_fps = 8.0
|
||||
|
||||
; OBBLIGATORIO: no.
|
||||
; Ruolo: FPS YOLO quando ci sono track attive ma nessuna e' ancora critica.
|
||||
; Default se non indicato: 12.0
|
||||
tracking_yolo_fps = 12.0
|
||||
|
||||
; OBBLIGATORIO: no.
|
||||
; Ruolo: FPS YOLO quando una track e' in stato candidate/centered.
|
||||
; Default se non indicato: 15.0
|
||||
critical_yolo_fps = 15.0
|
||||
|
||||
; OBBLIGATORIO: no.
|
||||
; Ruolo: massimo numero di frame da processare. 0 significa tutto il video.
|
||||
; Default se non indicato: 0
|
||||
@@ -153,6 +354,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 +393,61 @@ 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, avvia la pubblicazione dei dati verso un observer esterno su socket locale.
|
||||
; Quando attivo, il core disabilita la UI OpenCV integrata per non rallentare la demo.
|
||||
; Default se non indicato: false
|
||||
observer_enabled = false
|
||||
|
||||
; OBBLIGATORIO: no.
|
||||
; Ruolo: host TCP del publisher observer.
|
||||
; Default se non indicato: 127.0.0.1
|
||||
observer_host = 127.0.0.1
|
||||
|
||||
; OBBLIGATORIO: no.
|
||||
; Ruolo: porta TCP del publisher observer.
|
||||
; Default se non indicato: 8765
|
||||
observer_port = 8765
|
||||
|
||||
; OBBLIGATORIO: no.
|
||||
; Ruolo: FPS massimi della preview inviata all'observer. Tenere basso per non pesare sul core.
|
||||
; Default se non indicato: 4.0
|
||||
observer_preview_fps = 4.0
|
||||
|
||||
; OBBLIGATORIO: no.
|
||||
; Ruolo: larghezza massima della preview inviata all'observer.
|
||||
; Default se non indicato: 960
|
||||
observer_preview_width = 960
|
||||
|
||||
; OBBLIGATORIO: no.
|
||||
; Ruolo: qualita' JPEG delle preview inviate all'observer.
|
||||
; Default se non indicato: 75
|
||||
observer_jpeg_quality = 75
|
||||
|
||||
; OBBLIGATORIO: no.
|
||||
; Ruolo: frequenza della telemetria inviata all'observer.
|
||||
; Default se non indicato: 8.0
|
||||
observer_telemetry_fps = 8.0
|
||||
|
||||
; 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
|
||||
|
||||
1459
flywms_navigation.py
1459
flywms_navigation.py
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
|
||||
|
||||
515
flywms_navigation_observer.py
Normal file
515
flywms_navigation_observer.py
Normal file
@@ -0,0 +1,515 @@
|
||||
import argparse
|
||||
import base64
|
||||
import ctypes
|
||||
import json
|
||||
import socket
|
||||
import time
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
import flywms_navigation as nav
|
||||
|
||||
try:
|
||||
import dearpygui.dearpygui as dpg
|
||||
except ModuleNotFoundError:
|
||||
dpg = None
|
||||
|
||||
|
||||
NAV_TEXTURE = "obs_nav_texture"
|
||||
SNAPSHOT_TEXTURE = "obs_snapshot_texture"
|
||||
LABEL_TEXTURE = "obs_label_texture"
|
||||
NAV_IMAGE = "obs_nav_image"
|
||||
SNAPSHOT_IMAGE = "obs_snapshot_image"
|
||||
LABEL_IMAGE = "obs_label_image"
|
||||
TEXT_STATUS = "obs_status"
|
||||
TEXT_COMMANDS = "obs_commands"
|
||||
TEXT_TIMELINE = "obs_timeline"
|
||||
BADGE_CONNECTION = "obs_badge_connection"
|
||||
BADGE_YOLO = "obs_badge_yolo"
|
||||
BADGE_PHASE = "obs_badge_phase"
|
||||
BADGE_WMS = "obs_badge_wms"
|
||||
METRIC_LOOP_FPS = "obs_metric_loop_fps"
|
||||
METRIC_YOLO_FPS = "obs_metric_yolo_fps"
|
||||
METRIC_YOLO_MS = "obs_metric_yolo_ms"
|
||||
METRIC_SNAPSHOTS = "obs_metric_snapshots"
|
||||
METRIC_FRAME_ID = "obs_metric_frame_id"
|
||||
METRIC_TRACKS = "obs_metric_tracks"
|
||||
METRIC_LABELS = "obs_metric_labels"
|
||||
METRIC_PROGRESS = "obs_metric_progress"
|
||||
NAV_W = 960
|
||||
NAV_H = 540
|
||||
SNAPSHOT_W = 520
|
||||
SNAPSHOT_H = 300
|
||||
LABEL_W = 520
|
||||
LABEL_H = 220
|
||||
THEME_BADGE_OK = "obs_theme_badge_ok"
|
||||
THEME_BADGE_WARN = "obs_theme_badge_warn"
|
||||
THEME_BADGE_ALERT = "obs_theme_badge_alert"
|
||||
THEME_BADGE_IDLE = "obs_theme_badge_idle"
|
||||
|
||||
|
||||
def parse_args():
|
||||
pre = argparse.ArgumentParser(add_help=False)
|
||||
pre.add_argument("--config", default=nav.DEFAULT_CONFIG_PATH, help="File configurazione INI")
|
||||
pre_args, _ = pre.parse_known_args()
|
||||
defaults = nav.load_navigation_config(pre_args.config)
|
||||
|
||||
ap = argparse.ArgumentParser(parents=[pre])
|
||||
ap.add_argument("--host", default=defaults["observer_host"], help="Host publisher observer")
|
||||
ap.add_argument("--port", type=int, default=defaults["observer_port"], help="Porta publisher observer")
|
||||
ap.add_argument("--reconnect-sec", type=float, default=1.0, help="Attesa tra tentativi di reconnessione")
|
||||
ap.add_argument("--ui-backend", choices=["auto", "opencv", "dearpygui"], default="dearpygui",
|
||||
help="Backend UI observer")
|
||||
return ap.parse_args()
|
||||
|
||||
|
||||
def decode_preview(message: dict[str, object]) -> np.ndarray | None:
|
||||
image_b64 = message.get("image_b64")
|
||||
if not isinstance(image_b64, str):
|
||||
return None
|
||||
try:
|
||||
raw = base64.b64decode(image_b64.encode("ascii"))
|
||||
array = np.frombuffer(raw, dtype=np.uint8)
|
||||
return cv2.imdecode(array, cv2.IMREAD_COLOR)
|
||||
except (ValueError, cv2.error):
|
||||
return None
|
||||
|
||||
|
||||
def choose_backend(requested: str) -> str:
|
||||
if requested == "opencv":
|
||||
return "opencv"
|
||||
if requested == "dearpygui":
|
||||
if dpg is None:
|
||||
raise RuntimeError("DearPyGUI non disponibile")
|
||||
return "dearpygui"
|
||||
if dpg is not None:
|
||||
return "dearpygui"
|
||||
return "opencv"
|
||||
|
||||
|
||||
def extract_progress_text(telemetry: dict[str, object]) -> str:
|
||||
pose = telemetry.get("capture_pose") if isinstance(telemetry.get("capture_pose"), dict) else {}
|
||||
route_progress = pose.get("route_progress_ratio")
|
||||
if isinstance(route_progress, (int, float)):
|
||||
return f"{float(route_progress) * 100.0:.1f}%"
|
||||
return "n/d"
|
||||
|
||||
|
||||
def format_commands(telemetry: dict[str, object]) -> str:
|
||||
command_lines = telemetry.get("command_lines") or ["Observer in attesa dati"]
|
||||
if not isinstance(command_lines, list):
|
||||
command_lines = [str(command_lines)]
|
||||
return "\n".join(str(item) for item in command_lines)
|
||||
|
||||
|
||||
def empty_placeholder(width: int, height: int, text: str) -> np.ndarray:
|
||||
canvas = np.full((height, width, 3), 235, dtype=np.uint8)
|
||||
cv2.putText(
|
||||
canvas,
|
||||
text,
|
||||
(24, height // 2),
|
||||
cv2.FONT_HERSHEY_SIMPLEX,
|
||||
0.8,
|
||||
(30, 30, 30),
|
||||
2,
|
||||
cv2.LINE_AA,
|
||||
)
|
||||
return canvas
|
||||
|
||||
|
||||
def frame_to_texture_data(frame: np.ndarray, target_w: int, target_h: int) -> list[float]:
|
||||
src_h, src_w = frame.shape[:2]
|
||||
if src_h <= 0 or src_w <= 0:
|
||||
canvas = np.zeros((target_h, target_w, 3), dtype=np.uint8)
|
||||
else:
|
||||
scale = min(target_w / float(src_w), target_h / float(src_h))
|
||||
draw_w = max(1, int(round(src_w * scale)))
|
||||
draw_h = max(1, int(round(src_h * scale)))
|
||||
resized = cv2.resize(frame, (draw_w, draw_h), interpolation=cv2.INTER_AREA if scale < 1.0 else cv2.INTER_LINEAR)
|
||||
canvas = np.full((target_h, target_w, 3), 18, dtype=np.uint8)
|
||||
x0 = (target_w - draw_w) // 2
|
||||
y0 = (target_h - draw_h) // 2
|
||||
canvas[y0:y0 + draw_h, x0:x0 + draw_w] = resized
|
||||
rgba = cv2.cvtColor(canvas, cv2.COLOR_BGR2RGBA)
|
||||
return (rgba.astype(np.float32).ravel() / 255.0).tolist()
|
||||
|
||||
|
||||
def restore_window_by_title(title: str) -> None:
|
||||
if not hasattr(ctypes, "windll"):
|
||||
return
|
||||
user32 = ctypes.windll.user32
|
||||
hwnd = user32.FindWindowW(None, title)
|
||||
if hwnd:
|
||||
user32.ShowWindow(hwnd, 9) # SW_RESTORE
|
||||
user32.SetForegroundWindow(hwnd)
|
||||
|
||||
|
||||
def classify_phase(telemetry: dict[str, object]) -> str:
|
||||
command_lines = telemetry.get("command_lines")
|
||||
if not isinstance(command_lines, list):
|
||||
command_lines = []
|
||||
joined = " | ".join(str(line) for line in command_lines)
|
||||
if "ATTENDI_WMS" in joined or "ATTENDI_ACK" in joined:
|
||||
return "WAIT WMS"
|
||||
if "SCATTA_FOTO_ETICHETTA" in joined or "INVIA_ROI_REMOTA" in joined:
|
||||
return "CAPTURE"
|
||||
if "CENTRA_ETICHETTA" in joined or "RITORNA_CENTRO_GAYLORD" in joined:
|
||||
return "CENTERING"
|
||||
if "STOP" in joined:
|
||||
return "PAUSED"
|
||||
return "SCANNING"
|
||||
|
||||
|
||||
def classify_wms_status(telemetry: dict[str, object]) -> str:
|
||||
command_lines = telemetry.get("command_lines")
|
||||
if not isinstance(command_lines, list):
|
||||
command_lines = []
|
||||
joined = " | ".join(str(line) for line in command_lines)
|
||||
if "WMS_ACK" in joined or "ACK_RICEVUTO" in joined:
|
||||
return "ACK"
|
||||
if "WMS_NACK" in joined or "NACK_RICEVUTO" in joined:
|
||||
return "NACK"
|
||||
if "WMS_TIMEOUT" in joined or "TIMEOUT" in joined:
|
||||
return "TIMEOUT"
|
||||
if "ATTENDI_WMS" in joined or "ATTENDI_ACK" in joined:
|
||||
return "WAIT"
|
||||
return "IDLE"
|
||||
|
||||
|
||||
class DearPyGuiObserver:
|
||||
def __init__(self, args):
|
||||
if dpg is None:
|
||||
raise RuntimeError("DearPyGUI non disponibile")
|
||||
self.args = args
|
||||
self.latest_navigate: np.ndarray | None = None
|
||||
self.latest_snapshot: np.ndarray | None = None
|
||||
self.latest_label: np.ndarray | None = None
|
||||
self.telemetry: dict[str, object] = {
|
||||
"command_lines": ["Observer in attesa dati"],
|
||||
"motion_text": "MOTO: n/d",
|
||||
}
|
||||
self.sock: socket.socket | None = None
|
||||
self.recv_buffer = ""
|
||||
self.last_connect_attempt = 0.0
|
||||
self.connected = False
|
||||
self.event_history: list[str] = []
|
||||
self.last_command_signature = ""
|
||||
self.last_phase = ""
|
||||
self.last_wms_status = ""
|
||||
|
||||
def run(self) -> int:
|
||||
self._build_ui()
|
||||
try:
|
||||
while dpg.is_dearpygui_running():
|
||||
self._poll_socket()
|
||||
self._refresh_ui()
|
||||
dpg.render_dearpygui_frame()
|
||||
finally:
|
||||
self._close_socket()
|
||||
dpg.destroy_context()
|
||||
return 0
|
||||
|
||||
def _build_ui(self) -> None:
|
||||
dpg.create_context()
|
||||
self._build_themes()
|
||||
with dpg.texture_registry(show=False):
|
||||
self._create_texture(NAV_TEXTURE, frame_to_texture_data(empty_placeholder(NAV_W, NAV_H, "In attesa preview"), NAV_W, NAV_H), NAV_W, NAV_H)
|
||||
self._create_texture(SNAPSHOT_TEXTURE, frame_to_texture_data(empty_placeholder(SNAPSHOT_W, SNAPSHOT_H, "In attesa snapshot"), SNAPSHOT_W, SNAPSHOT_H), SNAPSHOT_W, SNAPSHOT_H)
|
||||
self._create_texture(LABEL_TEXTURE, frame_to_texture_data(empty_placeholder(LABEL_W, LABEL_H, "In attesa etichetta"), LABEL_W, LABEL_H), LABEL_W, LABEL_H)
|
||||
|
||||
with dpg.window(label="FlyWMS Observer", tag="main_window", width=1600, height=980):
|
||||
with dpg.group(horizontal=True):
|
||||
with dpg.child_window(width=980, height=920, border=True):
|
||||
dpg.add_text("Preview navigazione")
|
||||
dpg.add_image(NAV_TEXTURE, tag=NAV_IMAGE)
|
||||
with dpg.child_window(width=580, height=920, border=True):
|
||||
with dpg.group(horizontal=True):
|
||||
dpg.add_button(label="CONN", tag=BADGE_CONNECTION, width=86, height=28, enabled=False)
|
||||
dpg.add_button(label="YOLO", tag=BADGE_YOLO, width=86, height=28, enabled=False)
|
||||
dpg.add_button(label="PHASE", tag=BADGE_PHASE, width=130, height=28, enabled=False)
|
||||
dpg.add_button(label="WMS", tag=BADGE_WMS, width=86, height=28, enabled=False)
|
||||
dpg.add_spacer(height=10)
|
||||
dpg.add_text("Snapshot OCR payload")
|
||||
dpg.add_image(SNAPSHOT_TEXTURE, tag=SNAPSHOT_IMAGE)
|
||||
dpg.add_spacer(height=12)
|
||||
dpg.add_text("Crop etichetta")
|
||||
dpg.add_image(LABEL_TEXTURE, tag=LABEL_IMAGE)
|
||||
dpg.add_spacer(height=12)
|
||||
dpg.add_separator()
|
||||
dpg.add_text("Stato observer")
|
||||
dpg.add_text("Observer in attesa connessione", tag=TEXT_STATUS, wrap=540)
|
||||
dpg.add_separator()
|
||||
dpg.add_text("Metriche")
|
||||
with dpg.table(header_row=False, borders_innerH=True, borders_outerH=True, borders_innerV=True, borders_outerV=True, resizable=False, policy=dpg.mvTable_SizingStretchProp):
|
||||
dpg.add_table_column()
|
||||
dpg.add_table_column()
|
||||
for label, tag in (
|
||||
("Loop FPS", METRIC_LOOP_FPS),
|
||||
("YOLO FPS", METRIC_YOLO_FPS),
|
||||
("YOLO ms", METRIC_YOLO_MS),
|
||||
("Snapshot", METRIC_SNAPSHOTS),
|
||||
("Frame", METRIC_FRAME_ID),
|
||||
("Track attive", METRIC_TRACKS),
|
||||
("Etichette", METRIC_LABELS),
|
||||
("Progresso", METRIC_PROGRESS),
|
||||
):
|
||||
with dpg.table_row():
|
||||
dpg.add_text(label)
|
||||
dpg.add_text("n/d", tag=tag)
|
||||
dpg.add_separator()
|
||||
dpg.add_text("Comandi")
|
||||
dpg.add_input_text(tag=TEXT_COMMANDS, multiline=True, readonly=True, width=540, height=260, default_value="Observer in attesa dati")
|
||||
dpg.add_separator()
|
||||
dpg.add_text("Timeline eventi")
|
||||
dpg.add_input_text(tag=TEXT_TIMELINE, multiline=True, readonly=True, width=540, height=180, default_value="In attesa eventi")
|
||||
|
||||
dpg.create_viewport(title="FlyWMS Observer", width=1600, height=980)
|
||||
dpg.setup_dearpygui()
|
||||
dpg.show_viewport()
|
||||
time.sleep(0.05)
|
||||
restore_window_by_title("FlyWMS Observer")
|
||||
self._set_badge(BADGE_CONNECTION, "DISCONNECTED", THEME_BADGE_ALERT)
|
||||
self._set_badge(BADGE_YOLO, "YOLO OFF", THEME_BADGE_IDLE)
|
||||
self._set_badge(BADGE_PHASE, "IDLE", THEME_BADGE_IDLE)
|
||||
self._set_badge(BADGE_WMS, "WMS IDLE", THEME_BADGE_IDLE)
|
||||
|
||||
def _build_themes(self) -> None:
|
||||
def make_badge_theme(tag: str, button_color: tuple[int, int, int], text_color: tuple[int, int, int] = (255, 255, 255)) -> None:
|
||||
with dpg.theme(tag=tag):
|
||||
with dpg.theme_component(dpg.mvButton):
|
||||
dpg.add_theme_color(dpg.mvThemeCol_Button, button_color)
|
||||
dpg.add_theme_color(dpg.mvThemeCol_ButtonHovered, button_color)
|
||||
dpg.add_theme_color(dpg.mvThemeCol_ButtonActive, button_color)
|
||||
dpg.add_theme_color(dpg.mvThemeCol_Text, text_color)
|
||||
dpg.add_theme_style(dpg.mvStyleVar_FrameRounding, 6)
|
||||
dpg.add_theme_style(dpg.mvStyleVar_FramePadding, 8, 6)
|
||||
|
||||
make_badge_theme(THEME_BADGE_OK, (40, 140, 70))
|
||||
make_badge_theme(THEME_BADGE_WARN, (196, 132, 21))
|
||||
make_badge_theme(THEME_BADGE_ALERT, (176, 52, 52))
|
||||
make_badge_theme(THEME_BADGE_IDLE, (80, 88, 102))
|
||||
|
||||
def _create_texture(self, tag: str, data: list[float], width: int, height: int) -> None:
|
||||
dpg.add_dynamic_texture(width=width, height=height, default_value=data, tag=tag)
|
||||
|
||||
def _set_texture(self, texture_tag: str, frame: np.ndarray, target_w: int, target_h: int) -> None:
|
||||
dpg.set_value(texture_tag, frame_to_texture_data(frame, target_w, target_h))
|
||||
|
||||
def _set_badge(self, tag: str, label: str, theme_tag: str) -> None:
|
||||
dpg.configure_item(tag, label=label)
|
||||
dpg.bind_item_theme(tag, theme_tag)
|
||||
|
||||
def _append_event(self, text: str) -> None:
|
||||
stamp = time.strftime("%H:%M:%S")
|
||||
self.event_history.insert(0, f"{stamp} {text}")
|
||||
self.event_history = self.event_history[:12]
|
||||
|
||||
def _poll_socket(self) -> None:
|
||||
now = time.perf_counter()
|
||||
if self.sock is None:
|
||||
if now - self.last_connect_attempt < max(0.2, self.args.reconnect_sec):
|
||||
return
|
||||
self.last_connect_attempt = now
|
||||
try:
|
||||
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self.sock.connect((self.args.host, self.args.port))
|
||||
self.sock.setblocking(False)
|
||||
self.recv_buffer = ""
|
||||
self.connected = True
|
||||
nav.log("[OBS] connesso al core")
|
||||
self._append_event("Observer connesso al core")
|
||||
except OSError as exc:
|
||||
self._close_socket()
|
||||
self.connected = False
|
||||
dpg.set_value(TEXT_STATUS, f"Observer non connesso: {exc}")
|
||||
return
|
||||
|
||||
try:
|
||||
while True:
|
||||
try:
|
||||
chunk = self.sock.recv(65536)
|
||||
if not chunk:
|
||||
raise ConnectionError("publisher disconnesso")
|
||||
self.recv_buffer += chunk.decode("utf-8", errors="replace")
|
||||
except BlockingIOError:
|
||||
break
|
||||
except OSError as exc:
|
||||
if getattr(exc, "winerror", None) in (10035,):
|
||||
break
|
||||
raise
|
||||
while True:
|
||||
newline_pos = self.recv_buffer.find("\n")
|
||||
if newline_pos < 0:
|
||||
break
|
||||
line = self.recv_buffer[:newline_pos].strip()
|
||||
self.recv_buffer = self.recv_buffer[newline_pos + 1:]
|
||||
if not line:
|
||||
continue
|
||||
try:
|
||||
message = json.loads(line)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
msg_type = message.get("type")
|
||||
if msg_type == "telemetry":
|
||||
self.telemetry = message
|
||||
elif msg_type == "preview":
|
||||
frame = decode_preview(message)
|
||||
stream = message.get("stream")
|
||||
if frame is None:
|
||||
continue
|
||||
if stream == "navigate":
|
||||
self.latest_navigate = frame
|
||||
elif stream == "snapshot":
|
||||
self.latest_snapshot = frame
|
||||
elif stream == "label":
|
||||
self.latest_label = frame
|
||||
except (ConnectionError, OSError) as exc:
|
||||
nav.log(f"[OBS] connessione persa o non disponibile: {exc}")
|
||||
self._close_socket()
|
||||
self.connected = False
|
||||
dpg.set_value(TEXT_STATUS, f"Connessione persa: {exc}")
|
||||
self._append_event(f"Connessione persa: {exc}")
|
||||
|
||||
def _refresh_ui(self) -> None:
|
||||
if self.latest_navigate is not None:
|
||||
self._set_texture(NAV_TEXTURE, self.latest_navigate, NAV_W, NAV_H)
|
||||
if self.latest_snapshot is not None:
|
||||
self._set_texture(SNAPSHOT_TEXTURE, self.latest_snapshot, SNAPSHOT_W, SNAPSHOT_H)
|
||||
if self.latest_label is not None:
|
||||
self._set_texture(LABEL_TEXTURE, self.latest_label, LABEL_W, LABEL_H)
|
||||
state = "connesso" if self.connected else "in attesa connessione"
|
||||
dpg.set_value(TEXT_STATUS, f"Observer {state} su {self.args.host}:{self.args.port}")
|
||||
dpg.set_value(TEXT_COMMANDS, format_commands(self.telemetry))
|
||||
dpg.set_value(METRIC_LOOP_FPS, f"{float(self.telemetry.get('loop_fps', 0.0)):.2f}")
|
||||
dpg.set_value(METRIC_YOLO_FPS, f"{float(self.telemetry.get('yolo_fps', 0.0)):.2f}")
|
||||
dpg.set_value(METRIC_YOLO_MS, f"{float(self.telemetry.get('last_yolo_ms', 0.0)):.1f}")
|
||||
dpg.set_value(METRIC_SNAPSHOTS, str(self.telemetry.get("snapshot_counter", 0)))
|
||||
dpg.set_value(METRIC_FRAME_ID, str(self.telemetry.get("frame_id", "n/d")))
|
||||
dpg.set_value(METRIC_TRACKS, f"{self.telemetry.get('active_track_count', 0)} / {self.telemetry.get('track_count', 0)}")
|
||||
dpg.set_value(METRIC_LABELS, str(self.telemetry.get("label_count", 0)))
|
||||
dpg.set_value(METRIC_PROGRESS, extract_progress_text(self.telemetry))
|
||||
|
||||
self._set_badge(BADGE_CONNECTION, "CONNECTED" if self.connected else "DISCONNECTED", THEME_BADGE_OK if self.connected else THEME_BADGE_ALERT)
|
||||
run_yolo = bool(self.telemetry.get("run_yolo", False))
|
||||
yolo_mode = str(self.telemetry.get("yolo_mode", "idle")).upper()
|
||||
self._set_badge(BADGE_YOLO, f"YOLO {yolo_mode}" if run_yolo else "YOLO IDLE", THEME_BADGE_OK if run_yolo else THEME_BADGE_IDLE)
|
||||
phase = classify_phase(self.telemetry)
|
||||
phase_theme = THEME_BADGE_WARN if phase in ("CAPTURE", "CENTERING", "WAIT WMS", "PAUSED") else THEME_BADGE_OK
|
||||
self._set_badge(BADGE_PHASE, phase, phase_theme)
|
||||
wms_status = classify_wms_status(self.telemetry)
|
||||
wms_theme = THEME_BADGE_IDLE
|
||||
if wms_status == "ACK":
|
||||
wms_theme = THEME_BADGE_OK
|
||||
elif wms_status in ("WAIT",):
|
||||
wms_theme = THEME_BADGE_WARN
|
||||
elif wms_status in ("NACK", "TIMEOUT"):
|
||||
wms_theme = THEME_BADGE_ALERT
|
||||
self._set_badge(BADGE_WMS, f"WMS {wms_status}", wms_theme)
|
||||
|
||||
command_signature = str(self.telemetry.get("command_text") or "")
|
||||
if command_signature and command_signature != self.last_command_signature:
|
||||
self._append_event(command_signature)
|
||||
self.last_command_signature = command_signature
|
||||
if phase != self.last_phase:
|
||||
self._append_event(f"Fase -> {phase}")
|
||||
self.last_phase = phase
|
||||
if wms_status != self.last_wms_status:
|
||||
self._append_event(f"Stato WMS -> {wms_status}")
|
||||
self.last_wms_status = wms_status
|
||||
dpg.set_value(TEXT_TIMELINE, "\n".join(self.event_history) if self.event_history else "In attesa eventi")
|
||||
|
||||
def _close_socket(self) -> None:
|
||||
try:
|
||||
if self.sock is not None:
|
||||
self.sock.close()
|
||||
except OSError:
|
||||
pass
|
||||
self.sock = None
|
||||
self.recv_buffer = ""
|
||||
|
||||
|
||||
def run_opencv_observer(args) -> int:
|
||||
nav.log(f"[OBS] avvio observer {args.host}:{args.port}")
|
||||
|
||||
latest_navigate: np.ndarray | None = None
|
||||
latest_snapshot: np.ndarray | None = None
|
||||
latest_label: np.ndarray | None = None
|
||||
telemetry: dict[str, object] = {
|
||||
"command_lines": ["Observer in attesa dati"],
|
||||
"motion_text": "MOTO: n/d",
|
||||
}
|
||||
|
||||
cv2.namedWindow("flywms observer", cv2.WINDOW_NORMAL)
|
||||
cv2.namedWindow("flywms observer comandi", cv2.WINDOW_NORMAL)
|
||||
cv2.namedWindow("flywms observer snapshot", cv2.WINDOW_NORMAL)
|
||||
cv2.namedWindow("flywms observer etichetta", cv2.WINDOW_NORMAL)
|
||||
|
||||
while True:
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
try:
|
||||
sock.connect((args.host, args.port))
|
||||
nav.log("[OBS] connesso al core")
|
||||
fileobj = sock.makefile("r", encoding="utf-8", newline="\n")
|
||||
while True:
|
||||
line = fileobj.readline()
|
||||
if not line:
|
||||
raise ConnectionError("publisher disconnesso")
|
||||
message = json.loads(line)
|
||||
msg_type = message.get("type")
|
||||
if msg_type == "telemetry":
|
||||
telemetry = message
|
||||
elif msg_type == "preview":
|
||||
frame = decode_preview(message)
|
||||
stream = message.get("stream")
|
||||
if frame is None:
|
||||
continue
|
||||
if stream == "navigate":
|
||||
latest_navigate = frame
|
||||
elif stream == "snapshot":
|
||||
latest_snapshot = frame
|
||||
elif stream == "label":
|
||||
latest_label = frame
|
||||
|
||||
if latest_navigate is not None:
|
||||
cv2.imshow("flywms observer", latest_navigate)
|
||||
if latest_snapshot is not None:
|
||||
cv2.imshow("flywms observer snapshot", latest_snapshot)
|
||||
if latest_label is not None:
|
||||
cv2.imshow("flywms observer etichetta", latest_label)
|
||||
panel = nav.draw_commands_window(
|
||||
[str(item) for item in (telemetry.get("command_lines") or ["Observer attivo"])],
|
||||
str(telemetry.get("motion_text") or "MOTO: n/d"),
|
||||
)
|
||||
cv2.imshow("flywms observer comandi", panel)
|
||||
key = cv2.waitKey(1) & 0xFF
|
||||
if key in (27, ord("q")):
|
||||
nav.log("[OBS] terminato da tastiera")
|
||||
return 0
|
||||
except (ConnectionError, OSError, json.JSONDecodeError) as exc:
|
||||
nav.log(f"[OBS] connessione persa o non disponibile: {exc}")
|
||||
wait_deadline = time.perf_counter() + max(0.2, args.reconnect_sec)
|
||||
while time.perf_counter() < wait_deadline:
|
||||
key = cv2.waitKey(50) & 0xFF
|
||||
if key in (27, ord("q")):
|
||||
nav.log("[OBS] terminato da tastiera")
|
||||
return 0
|
||||
continue
|
||||
finally:
|
||||
try:
|
||||
sock.close()
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
backend = choose_backend(args.ui_backend)
|
||||
nav.log(f"[OBS] backend UI: {backend}")
|
||||
if backend == "dearpygui":
|
||||
return DearPyGuiObserver(args).run()
|
||||
return run_opencv_observer(args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
340
flywms_paddleocr_worker.py
Normal file
340
flywms_paddleocr_worker.py
Normal 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())
|
||||
918
flywms_wms_server.py
Normal file
918
flywms_wms_server.py
Normal file
@@ -0,0 +1,918 @@
|
||||
import argparse
|
||||
import asyncio
|
||||
import configparser
|
||||
import ctypes
|
||||
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
|
||||
|
||||
try:
|
||||
import dearpygui.dearpygui as dpg
|
||||
except ModuleNotFoundError:
|
||||
dpg = None
|
||||
|
||||
|
||||
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("--ui-backend", choices=["auto", "opencv", "dearpygui"], default="dearpygui",
|
||||
help="Backend UI server WMS")
|
||||
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 choose_ui_backend(requested: str) -> str:
|
||||
if requested == "opencv":
|
||||
return "opencv"
|
||||
if requested == "dearpygui":
|
||||
if dpg is None:
|
||||
raise RuntimeError("DearPyGUI non disponibile")
|
||||
return "dearpygui"
|
||||
if dpg is not None:
|
||||
return "dearpygui"
|
||||
return "opencv"
|
||||
|
||||
|
||||
def placeholder_frame(width: int, height: int, text: str) -> np.ndarray:
|
||||
canvas = np.full((height, width, 3), 235, dtype=np.uint8)
|
||||
cv2.putText(
|
||||
canvas,
|
||||
text,
|
||||
(24, height // 2),
|
||||
cv2.FONT_HERSHEY_SIMPLEX,
|
||||
0.8,
|
||||
(30, 30, 30),
|
||||
2,
|
||||
cv2.LINE_AA,
|
||||
)
|
||||
return canvas
|
||||
|
||||
|
||||
def frame_to_texture_data(frame: np.ndarray, target_w: int, target_h: int) -> list[float]:
|
||||
src_h, src_w = frame.shape[:2]
|
||||
if src_h <= 0 or src_w <= 0:
|
||||
canvas = np.zeros((target_h, target_w, 3), dtype=np.uint8)
|
||||
else:
|
||||
scale = min(target_w / float(src_w), target_h / float(src_h))
|
||||
draw_w = max(1, int(round(src_w * scale)))
|
||||
draw_h = max(1, int(round(src_h * scale)))
|
||||
resized = cv2.resize(frame, (draw_w, draw_h), interpolation=cv2.INTER_AREA if scale < 1.0 else cv2.INTER_LINEAR)
|
||||
canvas = np.full((target_h, target_w, 3), 18, dtype=np.uint8)
|
||||
x0 = (target_w - draw_w) // 2
|
||||
y0 = (target_h - draw_h) // 2
|
||||
canvas[y0:y0 + draw_h, x0:x0 + draw_w] = resized
|
||||
rgba = cv2.cvtColor(canvas, cv2.COLOR_BGR2RGBA)
|
||||
return (rgba.astype(np.float32).ravel() / 255.0).tolist()
|
||||
|
||||
|
||||
def restore_window_by_title(title: str) -> None:
|
||||
if not hasattr(ctypes, "windll"):
|
||||
return
|
||||
user32 = ctypes.windll.user32
|
||||
hwnd = user32.FindWindowW(None, title)
|
||||
if hwnd:
|
||||
user32.ShowWindow(hwnd, 9) # SW_RESTORE
|
||||
user32.SetForegroundWindow(hwnd)
|
||||
|
||||
|
||||
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 server_dpg_ui_loop() -> None:
|
||||
if dpg is None:
|
||||
log("UI DearPyGUI non disponibile, impossibile avviare il monitor WMS")
|
||||
with ui_lock:
|
||||
ui_state["stop"] = True
|
||||
return
|
||||
|
||||
image_texture = "wms_image_texture"
|
||||
payload_text = "wms_payload_text"
|
||||
status_text = "wms_status_text"
|
||||
image_w = 720
|
||||
image_h = 420
|
||||
|
||||
def create_texture(tag: str, data: list[float], width: int, height: int) -> None:
|
||||
dpg.add_dynamic_texture(width=width, height=height, default_value=data, tag=tag)
|
||||
|
||||
def set_texture(frame: np.ndarray) -> None:
|
||||
dpg.set_value(image_texture, frame_to_texture_data(frame, image_w, image_h))
|
||||
|
||||
dpg.create_context()
|
||||
with dpg.texture_registry(show=False):
|
||||
create_texture(
|
||||
image_texture,
|
||||
frame_to_texture_data(placeholder_frame(image_w, image_h, "In attesa immagine etichetta"), image_w, image_h),
|
||||
image_w,
|
||||
image_h,
|
||||
)
|
||||
|
||||
with dpg.window(label="FlyWMS WMS Server", width=1450, height=900):
|
||||
with dpg.group(horizontal=True):
|
||||
with dpg.child_window(width=760, height=840, border=True):
|
||||
dpg.add_text("Ultima immagine ricevuta")
|
||||
dpg.add_image(image_texture)
|
||||
with dpg.child_window(width=640, height=840, border=True):
|
||||
dpg.add_text("Stato")
|
||||
dpg.add_text("Server WMS in attesa payload", tag=status_text, wrap=600)
|
||||
dpg.add_separator()
|
||||
dpg.add_text("Payload")
|
||||
dpg.add_input_text(tag=payload_text, multiline=True, readonly=True, width=600, height=720, default_value="In attesa di payload WMS")
|
||||
|
||||
dpg.create_viewport(title="FlyWMS WMS Server", width=1450, height=900)
|
||||
dpg.setup_dearpygui()
|
||||
dpg.show_viewport()
|
||||
time.sleep(0.05)
|
||||
restore_window_by_title("FlyWMS WMS Server")
|
||||
|
||||
try:
|
||||
while dpg.is_dearpygui_running():
|
||||
with ui_lock:
|
||||
stop = bool(ui_state["stop"])
|
||||
image = None if ui_state["image"] is None else ui_state["image"].copy()
|
||||
lines = list(ui_state["payload_lines"])
|
||||
if stop:
|
||||
break
|
||||
if image is not None:
|
||||
set_texture(image)
|
||||
dpg.set_value(status_text, f"Richieste ricevute: {server_state['counter']}")
|
||||
dpg.set_value(payload_text, "\n".join(lines))
|
||||
dpg.render_dearpygui_frame()
|
||||
time.sleep(0.03)
|
||||
finally:
|
||||
dpg.destroy_context()
|
||||
|
||||
|
||||
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()
|
||||
ui_backend = choose_ui_backend(args.ui_backend) if args.ui_enabled else "disabled"
|
||||
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'}")
|
||||
log(f"UI backend: {ui_backend}")
|
||||
ui_thread = None
|
||||
if args.ui_enabled:
|
||||
ui_target = server_dpg_ui_loop if ui_backend == "dearpygui" else server_ui_loop
|
||||
ui_thread = threading.Thread(target=ui_target, 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())
|
||||
293
handoff.md
293
handoff.md
@@ -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.
|
||||
|
||||
5
run_navigator.bat
Normal file
5
run_navigator.bat
Normal file
@@ -0,0 +1,5 @@
|
||||
@echo off
|
||||
title FlyWMS - Navigator
|
||||
cd /d "%~dp0"
|
||||
python flywms_navigation.py --video "testhd2_edit.mp4" --observer-enabled --wms-enabled
|
||||
|
||||
5
run_observer.bat
Normal file
5
run_observer.bat
Normal file
@@ -0,0 +1,5 @@
|
||||
@echo off
|
||||
title FlyWMS - Observer
|
||||
cd /d "%~dp0"
|
||||
python flywms_navigation_observer.py --ui-backend dearpygui
|
||||
|
||||
5
run_wms_server.bat
Normal file
5
run_wms_server.bat
Normal file
@@ -0,0 +1,5 @@
|
||||
@echo off
|
||||
title FlyWMS - WMS Server
|
||||
cd /d "%~dp0"
|
||||
python flywms_wms_server.py --ui-backend dearpygui
|
||||
|
||||
14
startdemo.bat
Normal file
14
startdemo.bat
Normal file
@@ -0,0 +1,14 @@
|
||||
@echo off
|
||||
setlocal
|
||||
|
||||
set "ROOT=%~dp0"
|
||||
set "PY_SCRIPT=%ROOT%startdemo.py"
|
||||
|
||||
if not exist "%PY_SCRIPT%" (
|
||||
echo Script non trovato: "%PY_SCRIPT%"
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
python "%PY_SCRIPT%"
|
||||
set "EXIT_CODE=%ERRORLEVEL%"
|
||||
exit /b %EXIT_CODE%
|
||||
138
startdemo.py
Normal file
138
startdemo.py
Normal file
@@ -0,0 +1,138 @@
|
||||
import os
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
VIDEO = "testhd2_edit.mp4"
|
||||
|
||||
CHILDREN: list[subprocess.Popen] = []
|
||||
STOPPING = False
|
||||
|
||||
|
||||
def kill_tree(pid: int) -> None:
|
||||
try:
|
||||
subprocess.run(
|
||||
["taskkill", "/PID", str(pid), "/T", "/F"],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
check=False,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def pids_listening_on_port(port: int) -> list[int]:
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["netstat", "-ano", "-p", "tcp"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
check=False,
|
||||
)
|
||||
except Exception:
|
||||
return []
|
||||
pids: set[int] = set()
|
||||
suffix = f":{port}"
|
||||
for raw_line in result.stdout.splitlines():
|
||||
line = raw_line.strip()
|
||||
if not line:
|
||||
continue
|
||||
parts = line.split()
|
||||
if len(parts) < 5:
|
||||
continue
|
||||
proto, local_addr, _, state, pid_text = parts[:5]
|
||||
if proto.upper() != "TCP":
|
||||
continue
|
||||
if state.upper() != "LISTENING":
|
||||
continue
|
||||
if not local_addr.endswith(suffix):
|
||||
continue
|
||||
try:
|
||||
pids.add(int(pid_text))
|
||||
except ValueError:
|
||||
continue
|
||||
return sorted(pids)
|
||||
|
||||
|
||||
def preflight_cleanup() -> None:
|
||||
stale_pids = pids_listening_on_port(8088)
|
||||
for pid in stale_pids:
|
||||
print(f"Pulizia preventiva: termino processo in ascolto su 8088 (PID {pid})", flush=True)
|
||||
kill_tree(pid)
|
||||
if stale_pids:
|
||||
time.sleep(0.4)
|
||||
|
||||
|
||||
def stop_all() -> None:
|
||||
global STOPPING
|
||||
if STOPPING:
|
||||
return
|
||||
STOPPING = True
|
||||
print("\nArresto finestre demo...", flush=True)
|
||||
for proc in CHILDREN:
|
||||
if proc.poll() is None:
|
||||
kill_tree(proc.pid)
|
||||
|
||||
|
||||
def on_signal(signum, frame) -> None:
|
||||
stop_all()
|
||||
raise KeyboardInterrupt
|
||||
|
||||
|
||||
def start_window(title: str, launcher_bat: str) -> subprocess.Popen:
|
||||
startupinfo = subprocess.STARTUPINFO()
|
||||
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
||||
startupinfo.wShowWindow = 6 # SW_MINIMIZE
|
||||
return subprocess.Popen(
|
||||
["cmd.exe", "/k", launcher_bat],
|
||||
cwd=str(ROOT),
|
||||
creationflags=subprocess.CREATE_NEW_CONSOLE,
|
||||
startupinfo=startupinfo,
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
signal.signal(signal.SIGINT, on_signal)
|
||||
if hasattr(signal, "SIGTERM"):
|
||||
signal.signal(signal.SIGTERM, on_signal)
|
||||
|
||||
preflight_cleanup()
|
||||
|
||||
try:
|
||||
print("Avvio WMS server...", flush=True)
|
||||
CHILDREN.append(start_window("FlyWMS - WMS Server", "run_wms_server.bat"))
|
||||
time.sleep(0.25)
|
||||
|
||||
print("Avvio observer...", flush=True)
|
||||
CHILDREN.append(start_window("FlyWMS - Observer", "run_observer.bat"))
|
||||
time.sleep(0.25)
|
||||
|
||||
print("Avvio navigator...", flush=True)
|
||||
CHILDREN.append(start_window("FlyWMS - Navigator", "run_navigator.bat"))
|
||||
|
||||
print("\nDemo avviata.", flush=True)
|
||||
for proc in CHILDREN:
|
||||
print(f"- PID {proc.pid}", flush=True)
|
||||
print("\nPremi Ctrl+C in questa finestra per chiudere tutto.\n", flush=True)
|
||||
|
||||
while True:
|
||||
alive = [proc for proc in CHILDREN if proc.poll() is None]
|
||||
if not alive:
|
||||
print("Tutte le finestre demo sono terminate.", flush=True)
|
||||
return 0
|
||||
time.sleep(1.0)
|
||||
except KeyboardInterrupt:
|
||||
print("Interruzione rilevata.", flush=True)
|
||||
return 130
|
||||
finally:
|
||||
stop_all()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
261
step1_core_observer_design.md
Normal file
261
step1_core_observer_design.md
Normal file
@@ -0,0 +1,261 @@
|
||||
# FlyWMS Step 1 - Core/Observer Split
|
||||
|
||||
Data: 2026-05-29
|
||||
|
||||
## Scopo
|
||||
|
||||
Ridurre il tempo totale della demo fino a portarlo vicino alla durata reale del video, evitando che la parte visuale OpenCV rallenti il loop critico di acquisizione, inferenza e logica demo.
|
||||
|
||||
Lo Step 1 non cambia ancora la struttura single-process del core di calcolo, ma separa in modo netto:
|
||||
|
||||
- `core`: acquisizione, YOLO, tracking, macchina a stati, logging prestazioni
|
||||
- `observer`: processo separato per preview e telemetria
|
||||
|
||||
L'obiettivo e' ottenere una demo visibile senza riportare nel loop principale il costo della vecchia UI OpenCV.
|
||||
|
||||
## Motivazione tecnica
|
||||
|
||||
I benchmark gia' disponibili dicono:
|
||||
|
||||
- video reale: `658.3 s`
|
||||
- benchmark headless: `762.13 s`
|
||||
- demo vecchia con UI integrata: molto piu' lenta
|
||||
|
||||
La UI OpenCV dentro al loop ha gia' dimostrato di pesare in modo determinante. La strada piu' pulita e' quindi:
|
||||
|
||||
1. mantenere il core il piu' simile possibile al benchmark
|
||||
2. spostare la visualizzazione in un processo esterno
|
||||
3. accettare che l'observer non mostri tutti i frame
|
||||
|
||||
## Perimetro di Step 1
|
||||
|
||||
Step 1 include:
|
||||
|
||||
- canale di telemetria `core -> observer`
|
||||
- canale preview `core -> observer`
|
||||
- observer separato e opzionale
|
||||
- modalita' `latest update wins`
|
||||
|
||||
Step 1 non include ancora:
|
||||
|
||||
- multiprocessing tra capture e inference
|
||||
- shared memory
|
||||
- zero-copy
|
||||
- nuovo frontend DearPyGUI
|
||||
- protocollo distribuito multi-host
|
||||
|
||||
## Architettura proposta
|
||||
|
||||
### Processo core
|
||||
|
||||
Responsabilita':
|
||||
|
||||
- legge il video o la camera
|
||||
- rispetta i limiti `preview_fps` e `yolo_fps`
|
||||
- esegue YOLO
|
||||
- aggiorna tracking e macchina a stati
|
||||
- salva snapshot e gestisce WMS
|
||||
- scrive `tempistiche.txt`
|
||||
- pubblica telemetria e preview in modo non bloccante
|
||||
|
||||
Il core non deve mai aspettare l'observer.
|
||||
|
||||
### Processo observer
|
||||
|
||||
Responsabilita':
|
||||
|
||||
- ricevere dati dal core
|
||||
- mantenere l'ultimo stato disponibile
|
||||
- mostrare:
|
||||
- preview principale
|
||||
- eventuali preview secondarie
|
||||
- comandi/stato
|
||||
- fps/logica macchina a stati
|
||||
|
||||
L'observer puo' perdere aggiornamenti. Questo e' accettabile.
|
||||
|
||||
## Trasporto dati
|
||||
|
||||
Per Step 1 si usa un trasporto semplice su `localhost`, in modo da ridurre il rischio di complessita' prematura.
|
||||
|
||||
Scelta:
|
||||
|
||||
- socket TCP locale per preview e telemetria
|
||||
|
||||
Motivi:
|
||||
|
||||
- semplice da debuggare
|
||||
- leggibile anche con strumenti esterni
|
||||
- facile da riusare in seguito
|
||||
- non richiede introdurre subito shared memory
|
||||
|
||||
## Formato messaggi
|
||||
|
||||
### Telemetria
|
||||
|
||||
Formato: JSON line-delimited o JSON framed su socket
|
||||
|
||||
Campi minimi previsti:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "telemetry",
|
||||
"frame_id": 123,
|
||||
"timestamp": 1710000000.123,
|
||||
"state": "scan_level",
|
||||
"command_lines": ["...", "..."],
|
||||
"motion_text": "MOTO: stabile ...",
|
||||
"snapshot_counter": 4,
|
||||
"det_count": 2,
|
||||
"label_count": 1,
|
||||
"track_count": 2,
|
||||
"active_track_count": 2,
|
||||
"run_yolo": true,
|
||||
"last_yolo_ms": 28.4,
|
||||
"loop_fps": 25.8,
|
||||
"yolo_fps": 12.6,
|
||||
"video_fps": 30.0,
|
||||
"preview_fps": 30.0,
|
||||
"yolo_target_fps": 15.0
|
||||
}
|
||||
```
|
||||
|
||||
### Preview
|
||||
|
||||
Formato: messaggio con header JSON + payload JPEG.
|
||||
|
||||
Campi minimi header:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "preview",
|
||||
"stream": "navigate",
|
||||
"frame_id": 123,
|
||||
"timestamp": 1710000000.123,
|
||||
"width": 960,
|
||||
"height": 540,
|
||||
"encoding": "jpeg",
|
||||
"jpeg_size": 45231
|
||||
}
|
||||
```
|
||||
|
||||
Il payload binario segue subito dopo l'header.
|
||||
|
||||
## Frequenze observer
|
||||
|
||||
Per non pesare sul core:
|
||||
|
||||
- telemetria: fino a `5-10 Hz`
|
||||
- preview principale: `3-5 fps`
|
||||
- preview snapshot/etichetta: solo su evento o ultima disponibile
|
||||
|
||||
Step 1 non ha l'obiettivo di mostrare una preview fluida. Ha l'obiettivo di restare credibile a livello demo senza penalizzare il core.
|
||||
|
||||
## Strategia "latest update wins"
|
||||
|
||||
Il core non accumula backlog per l'observer.
|
||||
|
||||
Regole:
|
||||
|
||||
- se l'observer e' lento, gli aggiornamenti vecchi possono essere scartati
|
||||
- il core invia solo l'ultimo stato rilevante
|
||||
- nessuna coda lunga per la preview
|
||||
|
||||
Questo vale in particolare per:
|
||||
|
||||
- preview `navigate`
|
||||
- testo comandi
|
||||
- metriche fps
|
||||
|
||||
## Gestione overlay
|
||||
|
||||
Step 1 adotta una soluzione intermedia:
|
||||
|
||||
- il core genera una preview gia' ridotta
|
||||
- il core puo' continuare a disegnare l'overlay minimo necessario sulla preview inviata
|
||||
- l'observer resta semplice e mostra il risultato
|
||||
|
||||
Questo non e' il modello finale ideale, ma riduce il lavoro di riscrittura nello Step 1.
|
||||
|
||||
Evoluzione futura possibile:
|
||||
|
||||
- inviare frame piu' bbox/stato
|
||||
- disegnare overlay interamente nell'observer
|
||||
|
||||
## Modalita' di fallback
|
||||
|
||||
Il sistema deve funzionare anche senza observer.
|
||||
|
||||
Regole:
|
||||
|
||||
- se l'observer non e' avviato, il core continua a lavorare
|
||||
- se il socket non e' disponibile, il core logga e prosegue
|
||||
- se l'observer si scollega, il core non deve bloccarsi
|
||||
|
||||
Questo e' un requisito fondamentale dello Step 1.
|
||||
|
||||
## Parametri di configurazione da introdurre
|
||||
|
||||
Nuovi parametri suggeriti nel file INI:
|
||||
|
||||
- `observer_enabled = true/false`
|
||||
- `observer_host = 127.0.0.1`
|
||||
- `observer_port = 8765`
|
||||
- `observer_preview_fps = 4.0`
|
||||
- `observer_preview_width = 960`
|
||||
- `observer_jpeg_quality = 75`
|
||||
- `observer_telemetry_fps = 8.0`
|
||||
|
||||
## File previsti
|
||||
|
||||
### Nuovo file
|
||||
|
||||
- `flywms_navigation_observer.py`
|
||||
|
||||
Responsabilita':
|
||||
|
||||
- connettersi al core
|
||||
- ricevere messaggi
|
||||
- mantenere ultimo stato per stream
|
||||
- mostrare finestre OpenCV lato observer
|
||||
|
||||
### Modifiche a file esistente
|
||||
|
||||
- `flywms_navigation.py`
|
||||
- aggiunta configurazione observer
|
||||
- serializzazione telemetria
|
||||
- invio preview/telemetria non bloccante
|
||||
- disattivazione progressiva della vecchia UI integrata
|
||||
|
||||
## Piano implementativo di Step 1
|
||||
|
||||
1. aggiungere parametri observer a config e argparse
|
||||
2. creare un piccolo publisher non bloccante nel core
|
||||
3. creare `flywms_navigation_observer.py`
|
||||
4. pubblicare:
|
||||
- preview `navigate`
|
||||
- preview `snapshot`
|
||||
- preview `etichetta`
|
||||
- telemetria/comandi
|
||||
5. rendere opzionale la vecchia UI integrata
|
||||
6. testare:
|
||||
- core senza observer
|
||||
- core con observer
|
||||
- benchmark con observer attivo ma leggero
|
||||
|
||||
## Criteri di successo
|
||||
|
||||
Step 1 e' considerato riuscito se:
|
||||
|
||||
1. il core continua a funzionare senza finestre locali
|
||||
2. l'observer mostra una preview leggibile e lo stato della missione
|
||||
3. l'observer non blocca il core se rallenta o si chiude
|
||||
4. il tempo totale della demo si avvicina al benchmark headless, senza ricadere nella penalizzazione della vecchia UI
|
||||
|
||||
## Limiti noti
|
||||
|
||||
- Step 1 non risolve ancora il costo seriale di `cap.read()` e inferenza
|
||||
- Step 1 non introduce ancora il multiprocessing tra capture e inference
|
||||
- Step 1 non ottimizza ancora l'uso adattivo di YOLO
|
||||
|
||||
Questi temi restano demandati agli Step 2 e 3.
|
||||
104
step2_capture_inference_design.md
Normal file
104
step2_capture_inference_design.md
Normal file
@@ -0,0 +1,104 @@
|
||||
# Step 2 - Disaccoppiamento Capture / Inference
|
||||
|
||||
## Obiettivo
|
||||
|
||||
Ridurre la serializzazione del loop principale separando:
|
||||
|
||||
- acquisizione video e campionamento metadati;
|
||||
- inferenza, tracking e logica demo.
|
||||
|
||||
Lo scopo non e' introdurre ancora una pipeline completa multi-processo stile ROS, ma togliere dal percorso critico almeno la parte `cap.read()` e la preparazione del pacchetto frame.
|
||||
|
||||
## Vincolo architetturale
|
||||
|
||||
Il frame consumato dall'inferenza non puo' essere un'immagine grezza isolata.
|
||||
|
||||
Ogni frame deve essere rappresentato da un pacchetto logico di acquisizione che congela nello stesso istante:
|
||||
|
||||
- `frame_id`
|
||||
- `timestamp`
|
||||
- `video_time_sec`
|
||||
- immagine
|
||||
- posa / stato del drone campionati nel momento di acquisizione
|
||||
|
||||
Questo segue il modello gia' fissato nei documenti di architettura:
|
||||
|
||||
- `capture` non inventa la posa;
|
||||
- `capture` la campiona dalla sorgente di stato disponibile;
|
||||
- `inference` consuma un `CapturedFrame` gia' coerente.
|
||||
|
||||
Nel contesto della demo video attuale non esiste ancora una posa SLAM reale. Quindi il campo posa verra' popolato con una posa simulata minima, ma congelata all'acquisizione, utile a:
|
||||
|
||||
- mantenere il contratto architetturale corretto;
|
||||
- evitare che in futuro frame e stato si disallineino;
|
||||
- propagare i metadati fino agli snapshot.
|
||||
|
||||
## Struttura dati proposta
|
||||
|
||||
```text
|
||||
CapturedFrame
|
||||
- frame_id
|
||||
- timestamp
|
||||
- video_time_sec
|
||||
- frame
|
||||
- pose
|
||||
```
|
||||
|
||||
Dove `pose` e' un dizionario o struttura equivalente con almeno:
|
||||
|
||||
- `mode`
|
||||
- `frame_id`
|
||||
- `video_time_sec`
|
||||
- `scan_direction`
|
||||
- `route_progress_ratio`
|
||||
|
||||
Il campo `pose` potra' essere sostituito piu' avanti da un provider reale SLAM / PX4 / odometria senza cambiare il contratto tra capture e inference.
|
||||
|
||||
## Strategia di esecuzione
|
||||
|
||||
### Capture worker
|
||||
|
||||
Un worker separato:
|
||||
|
||||
- legge da `VideoCapture`
|
||||
- costruisce `CapturedFrame`
|
||||
- pubblica solo l'ultimo frame disponibile
|
||||
|
||||
### Inference / control loop
|
||||
|
||||
Il loop principale:
|
||||
|
||||
- legge l'ultimo `CapturedFrame`
|
||||
- esegue YOLO, tracking e logica demo
|
||||
- usa sempre i metadati congelati nel pacchetto
|
||||
|
||||
## Regola di coda
|
||||
|
||||
La coda deve essere piccola:
|
||||
|
||||
- niente backlog lungo;
|
||||
- meglio perdere frame che accumulare ritardo;
|
||||
- politica `latest frame wins`.
|
||||
|
||||
## Impatto sul codice esistente
|
||||
|
||||
### Nuove entita'
|
||||
|
||||
- `CapturedFrame`
|
||||
- `CaptureWorker`
|
||||
- helper `sample_demo_pose(...)`
|
||||
|
||||
### Oggetti da aggiornare
|
||||
|
||||
- `CandidateSnapshot`
|
||||
- `NavigationSnapshot`
|
||||
- metadata snapshot JSON
|
||||
- observer telemetry, se utile, con riferimento alla posa dell'ultimo frame
|
||||
|
||||
## Criteri di successo
|
||||
|
||||
1. il core continua a produrre lo stesso comportamento funzionale della demo;
|
||||
2. inferenza e snapshot lavorano su frame con metadati coerenti;
|
||||
3. `cap.read()` non sta piu' nel loop principale critico;
|
||||
4. nessun backlog crescente;
|
||||
5. nessun blocco se il capture worker arriva piu' veloce o piu' lento del consumer.
|
||||
55
step3_adaptive_inference_design.md
Normal file
55
step3_adaptive_inference_design.md
Normal file
@@ -0,0 +1,55 @@
|
||||
# Step 3 - Inferenza adattiva
|
||||
|
||||
## Obiettivo
|
||||
|
||||
Ridurre il tempo totale della demo senza cambiare modello o logica di missione, variando la frequenza di YOLO in base al contesto invece di usare un solo valore fisso.
|
||||
|
||||
## Principio
|
||||
|
||||
L'inferenza non deve avere sempre lo stesso ritmo.
|
||||
|
||||
Ci sono tre situazioni principali:
|
||||
|
||||
1. **scansione tranquilla**
|
||||
- nessuna track attiva
|
||||
- nessun target interessante vicino alla zona utile
|
||||
- YOLO puo' girare piu' lentamente
|
||||
|
||||
2. **tracciamento normale**
|
||||
- una o piu' track attive
|
||||
- target presenti ma non ancora in fase critica
|
||||
- YOLO gira a frequenza intermedia
|
||||
|
||||
3. **fase critica / near snapshot**
|
||||
- track in stato `candidate` o `centered`
|
||||
- target vicino alla linea di snapshot
|
||||
- YOLO gira a frequenza alta
|
||||
|
||||
## Regole iniziali
|
||||
|
||||
Versione conservativa:
|
||||
|
||||
- nessuna track attiva -> `idle_yolo_fps`
|
||||
- track attive ma nessuna `candidate/centered` -> `tracking_yolo_fps`
|
||||
- almeno una `candidate/centered` -> `critical_yolo_fps`
|
||||
|
||||
Queste regole usano lo stato gia' prodotto dal tracker e dal `NavigationController`, quindi non introducono nuova logica semantica invasiva.
|
||||
|
||||
## Vincoli
|
||||
|
||||
- la frequenza adattiva non deve scendere troppo, altrimenti si rischia di ritardare il riaggancio del target;
|
||||
- la frequenza critica non deve superare in modo aggressivo la capacita' reale della macchina;
|
||||
- il comportamento deve restare deterministico e leggibile nei log.
|
||||
|
||||
## Configurazione proposta
|
||||
|
||||
- `adaptive_yolo_enabled`
|
||||
- `idle_yolo_fps`
|
||||
- `tracking_yolo_fps`
|
||||
- `critical_yolo_fps`
|
||||
|
||||
## Criterio di successo
|
||||
|
||||
1. ridurre la durata netta della demo;
|
||||
2. non peggiorare visibilmente il comportamento di tracking/snapshot;
|
||||
3. mantenere il codice semplice e facilmente spegnibile.
|
||||
Reference in New Issue
Block a user