pipeline in linea single thread
This commit is contained in:
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
|
||||
@@ -3,7 +3,7 @@
|
||||
; Ruolo: sorgente video usata per simulare la camera del drone.
|
||||
; Se vuoto o "none", usa webcam 0.
|
||||
; Default se non indicato: testhd.mp4
|
||||
video = testhd.mp4
|
||||
video = testhd2_edit.mp4
|
||||
|
||||
; OBBLIGATORIO: si.
|
||||
; Ruolo: modello Ultralytics/YOLO moderno usato per rilevare gaylord ed etichette.
|
||||
@@ -11,9 +11,9 @@ video = testhd.mp4
|
||||
weights = C:\devel\flywms\runs\flywms_yolo11n_quick20\weights\best.pt
|
||||
|
||||
; OBBLIGATORIO: no.
|
||||
; Ruolo: device usato da Ultralytics. Usa "cpu" ora; con GPU compatibile usare "0".
|
||||
; Ruolo: device usato da Ultralytics. Usa "cpu" per forzare CPU; con GPU compatibile usare "0".
|
||||
; Default se non indicato: cpu
|
||||
ultralytics_device = cpu
|
||||
ultralytics_device = 0
|
||||
|
||||
; OBBLIGATORIO: no.
|
||||
; Ruolo: dimensione input YOLO. 640 e' il valore usato nel training rapido.
|
||||
@@ -30,6 +30,12 @@ min_confidence = 0.25
|
||||
; Default se non indicato: gaylord
|
||||
target_class = gaylord
|
||||
|
||||
; OBBLIGATORIO: no.
|
||||
; Ruolo: classe etichetta associata al gaylord centrato.
|
||||
; Il bbox etichetta viene accettato solo se e' contenuto nel bbox del gaylord.
|
||||
; Default se non indicato: etichetta
|
||||
label_class = etichetta
|
||||
|
||||
; OBBLIGATORIO: no.
|
||||
; Ruolo: numero massimo di frame in cui una track puo' non essere vista prima di essere rimossa.
|
||||
; Default se non indicato: 8
|
||||
@@ -89,6 +95,11 @@ edge_margin_ratio = 0.0
|
||||
; Default se non indicato: 0.03
|
||||
ocr_payload_pad_ratio = 0.03
|
||||
|
||||
; OBBLIGATORIO: no.
|
||||
; Ruolo: padding aggiunto al bbox etichetta prima di salvare il crop inviato all'OCR remoto.
|
||||
; Default se non indicato: 0.20
|
||||
label_payload_pad_ratio = 0.20
|
||||
|
||||
; OBBLIGATORIO: no.
|
||||
; Ruolo: trend minimo dell'area bbox negli ultimi frame. Valori negativi tollerano leggera uscita.
|
||||
; Default se non indicato: -0.35
|
||||
@@ -106,15 +117,168 @@ snapshot_window_frames = 1
|
||||
snapshot_output_dir = navigate_snapshots
|
||||
|
||||
; OBBLIGATORIO: no.
|
||||
; Ruolo: tempo simulato con cui il drone attende OCR remoto + verifica WMS.
|
||||
; Ruolo: tempo massimo con cui il drone attende OCR remoto + verifica WMS.
|
||||
; Con PaddleOCR il primo avvio del worker puo' richiedere alcuni secondi.
|
||||
; Default se non indicato: 2.0
|
||||
remote_ack_timeout_sec = 2.0
|
||||
remote_ack_timeout_sec = 10.0
|
||||
|
||||
; OBBLIGATORIO: no.
|
||||
; Ruolo: risposta remota simulata. Valori: always-ack, always-nack, alternate.
|
||||
; Default se non indicato: always-ack
|
||||
remote_ack_mode = always-ack
|
||||
|
||||
; OBBLIGATORIO: no.
|
||||
; Ruolo: se true, invia realmente il payload etichetta al server WMS demo via HTTP.
|
||||
; Se false, mantiene la risposta remota simulata locale.
|
||||
; Default se non indicato: false
|
||||
wms_enabled = false
|
||||
|
||||
; OBBLIGATORIO: no.
|
||||
; Ruolo: endpoint HTTP del server WMS demo, anche su altro PC.
|
||||
; Esempio rete: http://192.168.1.50:8088/api/v1/navigation-snapshot
|
||||
; Default se non indicato: http://127.0.0.1:8088/api/v1/navigation-snapshot
|
||||
wms_server_url = http://127.0.0.1:8088/api/v1/navigation-snapshot
|
||||
|
||||
; OBBLIGATORIO: no.
|
||||
; Ruolo: identificativo del client/drone demo mandato nei metadati.
|
||||
; Default se non indicato: flywms-demo-01
|
||||
wms_client_id = flywms-demo-01
|
||||
|
||||
; OBBLIGATORIO: no.
|
||||
; Ruolo: timeout HTTP del worker WMS, in secondi.
|
||||
; Deve essere maggiore del tempo OCR server; con PaddleOCR fast caldo circa 3s, primo avvio circa 8-10s.
|
||||
; Default se non indicato: 2.0
|
||||
wms_timeout_sec = 12.0
|
||||
|
||||
; OBBLIGATORIO: no.
|
||||
; Ruolo: numero massimo di snapshot in coda verso il WMS.
|
||||
; Default se non indicato: 8
|
||||
wms_queue_max_size = 8
|
||||
|
||||
; OBBLIGATORIO: no.
|
||||
; Ruolo: se true, oltre al crop etichetta invia anche il frame/debug gaylord.
|
||||
; Default se non indicato: true
|
||||
wms_send_gaylord_debug = true
|
||||
|
||||
; OBBLIGATORIO: no.
|
||||
; Ruolo: host su cui il server WMS demo FastAPI resta in ascolto.
|
||||
; 0.0.0.0 consente connessioni da altri PC della rete.
|
||||
; Default se non indicato: 0.0.0.0
|
||||
wms_server_host = 0.0.0.0
|
||||
|
||||
; OBBLIGATORIO: no.
|
||||
; Ruolo: porta del server WMS demo FastAPI.
|
||||
; Default se non indicato: 8088
|
||||
wms_server_port = 8088
|
||||
|
||||
; OBBLIGATORIO: no.
|
||||
; Ruolo: directory in cui il server salva immagini e metadati ricevuti.
|
||||
; Default se non indicato: wms_received
|
||||
wms_received_dir = wms_received
|
||||
|
||||
; OBBLIGATORIO: no.
|
||||
; Ruolo: risposta simulata server. Valori: always-ack, always-nack, alternate, random.
|
||||
; Default se non indicato: always-ack
|
||||
wms_fake_ack_mode = always-ack
|
||||
|
||||
; OBBLIGATORIO: no.
|
||||
; Ruolo: ritardo finto del server per simulare OCR/WMS.
|
||||
; Default se non indicato: 0.5
|
||||
wms_fake_processing_sec = 0.5
|
||||
|
||||
; OBBLIGATORIO: no.
|
||||
; Ruolo: se true, il server WMS demo apre le finestre OpenCV immagine/payload.
|
||||
; Default se non indicato: true
|
||||
wms_ui_enabled = true
|
||||
|
||||
; OBBLIGATORIO: no.
|
||||
; Ruolo: se true, il server prova OCR reale sul crop etichetta.
|
||||
; Default se non indicato: true
|
||||
wms_ocr_enabled = true
|
||||
|
||||
; OBBLIGATORIO: no.
|
||||
; Ruolo: motore OCR server. Valori: paddleocr, easyocr, tesseract, fake.
|
||||
; Default se non indicato: paddleocr
|
||||
wms_ocr_mode = paddleocr
|
||||
|
||||
; OBBLIGATORIO: no.
|
||||
; Ruolo: Python del virtualenv isolato usato dal worker PaddleOCR.
|
||||
; Default se non indicato: C:\devel\yolo-ocr\.venv-paddle-cpu\Scripts\python.exe
|
||||
wms_paddle_python = C:\devel\yolo-ocr\.venv-paddle-cpu\Scripts\python.exe
|
||||
|
||||
; OBBLIGATORIO: no.
|
||||
; Ruolo: script worker PaddleOCR persistente.
|
||||
; Default se non indicato: flywms_paddleocr_worker.py nella cartella flywms.
|
||||
wms_paddle_worker_script = C:\devel\flywms\flywms_paddleocr_worker.py
|
||||
|
||||
; OBBLIGATORIO: no.
|
||||
; Ruolo: altezze a cui ridimensionare il crop etichetta prima dell'OCR.
|
||||
; Per demo 96 e' la configurazione leggera. Per debug: 64,96,128.
|
||||
; Default se non indicato: 96
|
||||
wms_paddle_target_heights = 96
|
||||
|
||||
; OBBLIGATORIO: no.
|
||||
; Ruolo: quante varianti preprocessing usare nel worker PaddleOCR.
|
||||
; Valori: fast, balanced, full.
|
||||
; fast usa originale, CLAHE, CLAHE+sharpen ed e' il default per il server demo.
|
||||
; Default se non indicato: fast
|
||||
wms_paddle_variant_set = fast
|
||||
|
||||
; OBBLIGATORIO: no.
|
||||
; Ruolo: numero di cifre atteso nel codice UDC.
|
||||
; Default se non indicato: 6
|
||||
wms_paddle_expected_digits = 6
|
||||
|
||||
; OBBLIGATORIO: no.
|
||||
; Ruolo: numero minimo di varianti OCR che devono concordare sul codice.
|
||||
; Default se non indicato: 2
|
||||
wms_paddle_min_votes = 2
|
||||
|
||||
; OBBLIGATORIO: no.
|
||||
; Ruolo: confidenza minima del candidato PaddleOCR scelto.
|
||||
; Default se non indicato: 0.70
|
||||
wms_paddle_min_confidence = 0.70
|
||||
|
||||
; OBBLIGATORIO: no.
|
||||
; Ruolo: prefisso del codice fallback se OCR reale non legge nulla.
|
||||
; Default se non indicato: UDC
|
||||
wms_fake_ocr_prefix = UDC
|
||||
|
||||
; OBBLIGATORIO: no.
|
||||
; Ruolo: testo restituito quando OCR non determina il codice reale.
|
||||
; Default se non indicato: udc non determinato
|
||||
wms_undetermined_code_text = udc non determinato
|
||||
|
||||
; OBBLIGATORIO: no.
|
||||
; Ruolo: ritardo finto OCR/fallback in secondi.
|
||||
; Default se non indicato: 0.2
|
||||
wms_fake_ocr_delay_sec = 0.2
|
||||
|
||||
; OBBLIGATORIO: no.
|
||||
; Ruolo: operatore/drone simulato registrato nel payload WMS.
|
||||
; Default se non indicato: udrone
|
||||
wms_operator = udrone
|
||||
|
||||
; OBBLIGATORIO: no.
|
||||
; Ruolo: sito/magazzino simulato registrato nel payload WMS.
|
||||
; Default se non indicato: demo-magazzino
|
||||
wms_site = demo-magazzino
|
||||
|
||||
; OBBLIGATORIO: no.
|
||||
; Ruolo: durata simulata del movimento drone dal centro gaylord al centro etichetta.
|
||||
; Default se non indicato: 3.0
|
||||
label_move_sec = 3.0
|
||||
|
||||
; OBBLIGATORIO: no.
|
||||
; Ruolo: attesa simulata per stabilizzare la foto ravvicinata dell'etichetta.
|
||||
; Default se non indicato: 2.0
|
||||
label_stabilization_sec = 2.0
|
||||
|
||||
; OBBLIGATORIO: no.
|
||||
; Ruolo: durata simulata del ritorno dal centro etichetta al centro gaylord.
|
||||
; Default se non indicato: 3.0
|
||||
label_return_sec = 3.0
|
||||
|
||||
; OBBLIGATORIO: no.
|
||||
; Ruolo: direzione simulata di ripartenza dopo ACK. Valori: destra, sinistra.
|
||||
; Default se non indicato: destra
|
||||
@@ -125,6 +289,17 @@ scan_direction = destra
|
||||
; Default se non indicato: 1280
|
||||
preview_width = 1280
|
||||
|
||||
; OBBLIGATORIO: no.
|
||||
; Ruolo: profilo benchmark senza finestre OpenCV.
|
||||
; Se true forza no_display e usa benchmark_preview_fps come target di cattura/preview.
|
||||
; Default se non indicato: false
|
||||
benchmark_mode = false
|
||||
|
||||
; OBBLIGATORIO: no.
|
||||
; Ruolo: FPS target di cattura/preview per il profilo benchmark.
|
||||
; Default se non indicato: 30.0
|
||||
benchmark_preview_fps = 30.0
|
||||
|
||||
; OBBLIGATORIO: no.
|
||||
; Ruolo: se true, il video di test viene riprodotto rispettando il framerate originale.
|
||||
; Default se non indicato: true
|
||||
@@ -137,6 +312,12 @@ realtime_playback = true
|
||||
; Default se non indicato: 24.0
|
||||
preview_fps = 24.0
|
||||
|
||||
; OBBLIGATORIO: no.
|
||||
; Ruolo: se true, usa FP16/half precision per YOLO quando gira su GPU.
|
||||
; Su CPU viene ignorato automaticamente.
|
||||
; Default se non indicato: true
|
||||
yolo_half = true
|
||||
|
||||
; OBBLIGATORIO: no.
|
||||
; Ruolo: FPS massimo per inferenza YOLO. 0 esegue YOLO su ogni frame di preview.
|
||||
; Nei frame intermedi la preview continua usando l'ultimo stato di tracking disponibile.
|
||||
@@ -153,6 +334,21 @@ max_frames = 0
|
||||
; Default se non indicato: 2.0
|
||||
stats_interval = 2.0
|
||||
|
||||
; OBBLIGATORIO: no.
|
||||
; Ruolo: file di log delle tempistiche dettagliate per frame.
|
||||
; Default se non indicato: tempistiche.txt
|
||||
perf_log_path = tempistiche.txt
|
||||
|
||||
; OBBLIGATORIO: no.
|
||||
; Ruolo: intervallo massimo tra due flush del file di tempistiche.
|
||||
; Default se non indicato: 2.0
|
||||
perf_log_flush_interval_sec = 2.0
|
||||
|
||||
; OBBLIGATORIO: no.
|
||||
; Ruolo: numero di righe bufferizzate prima del flush del file di tempistiche.
|
||||
; Default se non indicato: 120
|
||||
perf_log_flush_lines = 120
|
||||
|
||||
; OBBLIGATORIO: no.
|
||||
; Ruolo: ogni quanti frame aggiornare il moto apparente stimato dalle track.
|
||||
; Default se non indicato: 5
|
||||
@@ -177,3 +373,25 @@ flash_alpha = 0.70
|
||||
; Ruolo: se true, disabilita tutte le finestre video. Usarlo solo per test headless.
|
||||
; Default se non indicato: false
|
||||
no_display = false
|
||||
|
||||
; OBBLIGATORIO: no.
|
||||
; Ruolo: se true, posiziona e ridimensiona le finestre OpenCV usando i valori sotto.
|
||||
; Formato finestre: x,y,width,height
|
||||
; Default se non indicato: true
|
||||
window_layout_enabled = true
|
||||
|
||||
; OBBLIGATORIO: no.
|
||||
; Ruolo: posizione finestra principale con tracking gaylord/etichetta.
|
||||
navigate_window = 20,40,1100,620
|
||||
|
||||
; OBBLIGATORIO: no.
|
||||
; Ruolo: posizione finestra comandi e indicatori movimento.
|
||||
commands_window = 1140,40,760,520
|
||||
|
||||
; OBBLIGATORIO: no.
|
||||
; Ruolo: posizione finestra snapshot gaylord.
|
||||
snapshot_window = 1140,590,520,360
|
||||
|
||||
; OBBLIGATORIO: no.
|
||||
; Ruolo: posizione finestra crop etichetta.
|
||||
label_window = 1140,980,520,260
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -53,13 +53,15 @@ class NavigationDemoEngine:
|
||||
def __init__(self, args):
|
||||
self.args = args
|
||||
nav.require_file(args.weights, "modello Ultralytics")
|
||||
self.detector = nav.UltralyticsDetector(args.weights, args.ultralytics_device)
|
||||
self.detector = nav.UltralyticsDetector(args.weights, args.ultralytics_device, args.yolo_half)
|
||||
self.cap, self.source_name = nav.open_capture(args.video)
|
||||
if not self.cap.isOpened():
|
||||
raise RuntimeError(f"impossibile aprire sorgente video: {self.source_name}")
|
||||
|
||||
video_fps = self.cap.get(cv2.CAP_PROP_FPS)
|
||||
preview_fps = args.preview_fps if args.preview_fps and args.preview_fps > 0 else video_fps
|
||||
self.video_fps = video_fps
|
||||
self.preview_fps = preview_fps
|
||||
if args.preview_fps and args.preview_fps > 0 and (args.video is None or str(args.video).isdigit()):
|
||||
self.cap.set(cv2.CAP_PROP_FPS, float(args.preview_fps))
|
||||
self.frame_delay = (
|
||||
@@ -82,6 +84,7 @@ class NavigationDemoEngine:
|
||||
self.next_yolo_time = self.start_time
|
||||
self.last_yolo_ms = 0.0
|
||||
self.gaylords: list[nav.Detection] = []
|
||||
self.labels: list[nav.Detection] = []
|
||||
self.tracks: list[nav.Track] = []
|
||||
self.stop_reason = ""
|
||||
|
||||
@@ -123,6 +126,10 @@ class NavigationDemoEngine:
|
||||
det for det in detections
|
||||
if det.class_name.strip().lower() == self.args.target_class.strip().lower()
|
||||
]
|
||||
self.labels = [
|
||||
det for det in detections
|
||||
if det.class_name.strip().lower() == self.args.label_class.strip().lower()
|
||||
]
|
||||
self.tracks = self.tracker.update(self.gaylords, self.frame_id, frame.shape[1])
|
||||
|
||||
if (
|
||||
@@ -140,6 +147,7 @@ class NavigationDemoEngine:
|
||||
frame,
|
||||
self.frame_id,
|
||||
timestamp,
|
||||
self.labels,
|
||||
)
|
||||
if snapshot is not None:
|
||||
new_snapshots.append(snapshot)
|
||||
@@ -149,17 +157,19 @@ class NavigationDemoEngine:
|
||||
|
||||
elapsed = max(time.perf_counter() - self.start_time, 0.001)
|
||||
fps_text = (
|
||||
f"frame={self.frame_id} fps={self.frame_id / elapsed:.1f} "
|
||||
f"yolo_fps={self.yolo_cycles / elapsed:.1f} yolo={self.last_yolo_ms:.0f}ms "
|
||||
f"det={len(self.gaylords)} tracks={len(self.tracks)} "
|
||||
f"frame={self.frame_id} src_fps={nav.format_fps_value(self.video_fps)} "
|
||||
f"preview_target={nav.format_fps_value(self.preview_fps)} fps={self.frame_id / elapsed:.1f} "
|
||||
f"yolo_target={nav.format_fps_value(self.args.yolo_fps)} yolo_fps={self.yolo_cycles / elapsed:.1f} "
|
||||
f"yolo={self.last_yolo_ms:.0f}ms det={len(self.gaylords)} labels={len(self.labels)} "
|
||||
f"tracks={len(self.tracks)} "
|
||||
f"snap={self.navigator.snapshot_counter}"
|
||||
)
|
||||
display = nav.draw_navigation_debug(
|
||||
frame,
|
||||
self.tracks,
|
||||
self.args,
|
||||
self.navigator.last_command_text,
|
||||
fps_text,
|
||||
self.labels,
|
||||
self.navigator.label_movement_arrow,
|
||||
)
|
||||
|
||||
return DemoFrameState(
|
||||
@@ -335,7 +345,7 @@ class NavigationDemoGui:
|
||||
|
||||
|
||||
def bgr_to_rgba_float(frame: np.ndarray) -> np.ndarray:
|
||||
rgba = cv2.cvtColor(frame, cv2.COLOR_BGR2RGBA)
|
||||
rgba = nav.cuda_cvt_color(frame, cv2.COLOR_BGR2RGBA)
|
||||
return np.asarray(rgba, dtype=np.float32).ravel() / 255.0
|
||||
|
||||
|
||||
|
||||
340
flywms_paddleocr_worker.py
Normal file
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())
|
||||
789
flywms_wms_server.py
Normal file
789
flywms_wms_server.py
Normal file
@@ -0,0 +1,789 @@
|
||||
import argparse
|
||||
import asyncio
|
||||
import configparser
|
||||
import json
|
||||
import random
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
import uvicorn
|
||||
from fastapi import FastAPI, File, Form, UploadFile
|
||||
|
||||
|
||||
DEFAULT_CONFIG_PATH = "flywms_navigation.ini"
|
||||
CUDA_MIN_PIXELS = 640 * 360
|
||||
|
||||
|
||||
def opencv_cuda_available() -> bool:
|
||||
try:
|
||||
return hasattr(cv2, "cuda") and cv2.cuda.getCudaEnabledDeviceCount() > 0
|
||||
except cv2.error:
|
||||
return False
|
||||
|
||||
|
||||
OPENCV_CUDA_AVAILABLE = opencv_cuda_available()
|
||||
|
||||
|
||||
def cuda_resize(
|
||||
image: np.ndarray,
|
||||
size: tuple[int, int],
|
||||
interpolation: int = cv2.INTER_LINEAR,
|
||||
min_pixels: int = CUDA_MIN_PIXELS,
|
||||
) -> np.ndarray:
|
||||
if not OPENCV_CUDA_AVAILABLE or image.size < min_pixels:
|
||||
return cv2.resize(image, size, interpolation=interpolation)
|
||||
try:
|
||||
gpu = cv2.cuda_GpuMat()
|
||||
gpu.upload(image)
|
||||
return cv2.cuda.resize(gpu, size, interpolation=interpolation).download()
|
||||
except cv2.error:
|
||||
return cv2.resize(image, size, interpolation=interpolation)
|
||||
|
||||
|
||||
def cuda_cvt_color(
|
||||
image: np.ndarray,
|
||||
code: int,
|
||||
min_pixels: int = CUDA_MIN_PIXELS,
|
||||
) -> np.ndarray:
|
||||
if not OPENCV_CUDA_AVAILABLE or image.size < min_pixels:
|
||||
return cv2.cvtColor(image, code)
|
||||
try:
|
||||
gpu = cv2.cuda_GpuMat()
|
||||
gpu.upload(image)
|
||||
return cv2.cuda.cvtColor(gpu, code).download()
|
||||
except cv2.error:
|
||||
return cv2.cvtColor(image, code)
|
||||
|
||||
app = FastAPI(title="FlyWMS Demo WMS Server")
|
||||
server_state = {
|
||||
"received_dir": "wms_received",
|
||||
"fake_ack_mode": "always-ack",
|
||||
"fake_processing_sec": 0.5,
|
||||
"ui_enabled": True,
|
||||
"ocr_enabled": True,
|
||||
"ocr_mode": "paddleocr",
|
||||
"tesseract_cmd": "",
|
||||
"paddle_python": r"C:\devel\yolo-ocr\.venv-paddle-cpu\Scripts\python.exe",
|
||||
"paddle_worker_script": str(Path(__file__).with_name("flywms_paddleocr_worker.py")),
|
||||
"paddle_target_heights": "96",
|
||||
"paddle_variant_set": "fast",
|
||||
"paddle_expected_digits": 6,
|
||||
"paddle_min_votes": 2,
|
||||
"paddle_min_confidence": 0.70,
|
||||
"fake_ocr_prefix": "UDC",
|
||||
"undetermined_code_text": "udc non determinato",
|
||||
"fake_ocr_delay_sec": 0.2,
|
||||
"operator": "udrone",
|
||||
"site": "demo-magazzino",
|
||||
"counter": 0,
|
||||
}
|
||||
state_lock = threading.Lock()
|
||||
ui_lock = threading.Lock()
|
||||
ui_state = {
|
||||
"image": None,
|
||||
"payload_lines": ["In attesa di payload WMS"],
|
||||
"updated": False,
|
||||
"stop": False,
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class OcrServerResult:
|
||||
text: str
|
||||
raw_text: str
|
||||
confidence: float
|
||||
backend: str
|
||||
fallback_used: bool
|
||||
votes: int = 0
|
||||
variant: str = ""
|
||||
|
||||
|
||||
class PaddleOcrWorker:
|
||||
def __init__(
|
||||
self,
|
||||
python_path: str,
|
||||
script_path: str,
|
||||
target_heights: str,
|
||||
variant_set: str,
|
||||
expected_digits: int,
|
||||
min_votes: int,
|
||||
min_confidence: float,
|
||||
) -> None:
|
||||
self.python_path = python_path
|
||||
self.script_path = script_path
|
||||
self.target_heights = target_heights
|
||||
self.variant_set = variant_set
|
||||
self.expected_digits = expected_digits
|
||||
self.min_votes = min_votes
|
||||
self.min_confidence = min_confidence
|
||||
self.lock = threading.Lock()
|
||||
self.process: subprocess.Popen[str] | None = None
|
||||
|
||||
def close(self) -> None:
|
||||
with self.lock:
|
||||
process = self.process
|
||||
self.process = None
|
||||
if process is None:
|
||||
return
|
||||
try:
|
||||
if process.stdin:
|
||||
process.stdin.write("__quit__\n")
|
||||
process.stdin.flush()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
process.wait(timeout=2.0)
|
||||
except subprocess.TimeoutExpired:
|
||||
process.kill()
|
||||
|
||||
def predict(self, image_path: Path) -> dict[str, object]:
|
||||
with self.lock:
|
||||
process = self._ensure_process()
|
||||
if process.stdin is None or process.stdout is None:
|
||||
raise RuntimeError("worker stdio non disponibile")
|
||||
request = {
|
||||
"image_path": str(image_path),
|
||||
"target_heights": self.target_heights,
|
||||
"variant_set": self.variant_set,
|
||||
"expected_digits": self.expected_digits,
|
||||
"min_votes": self.min_votes,
|
||||
"min_confidence": self.min_confidence,
|
||||
}
|
||||
process.stdin.write(json.dumps(request, ensure_ascii=True) + "\n")
|
||||
process.stdin.flush()
|
||||
line = process.stdout.readline()
|
||||
if not line:
|
||||
self.process = None
|
||||
raise RuntimeError("worker PaddleOCR terminato")
|
||||
return json.loads(line)
|
||||
|
||||
def _ensure_process(self) -> subprocess.Popen[str]:
|
||||
if self.process is not None and self.process.poll() is None:
|
||||
return self.process
|
||||
python_path = Path(self.python_path)
|
||||
script_path = Path(self.script_path)
|
||||
if not python_path.exists():
|
||||
raise RuntimeError(f"python PaddleOCR non trovato: {python_path}")
|
||||
if not script_path.exists():
|
||||
raise RuntimeError(f"worker PaddleOCR non trovato: {script_path}")
|
||||
self.process = subprocess.Popen(
|
||||
[str(python_path), str(script_path)],
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.DEVNULL,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
creationflags=subprocess.CREATE_NO_WINDOW if hasattr(subprocess, "CREATE_NO_WINDOW") else 0,
|
||||
)
|
||||
assert self.process.stdout is not None
|
||||
ready_line = self.process.stdout.readline()
|
||||
if not ready_line:
|
||||
raise RuntimeError("worker PaddleOCR non avviato")
|
||||
ready = json.loads(ready_line)
|
||||
if not ready.get("ready"):
|
||||
raise RuntimeError(f"worker PaddleOCR risposta inattesa: {ready}")
|
||||
return self.process
|
||||
|
||||
|
||||
_paddle_worker: PaddleOcrWorker | None = None
|
||||
_paddle_worker_lock = threading.Lock()
|
||||
|
||||
|
||||
def log(msg: str) -> None:
|
||||
print(f"[{time.strftime('%H:%M:%S')}] {msg}", flush=True)
|
||||
|
||||
|
||||
def load_server_config(path_str: str) -> dict[str, object]:
|
||||
defaults: dict[str, object] = {
|
||||
"wms_server_host": "0.0.0.0",
|
||||
"wms_server_port": 8088,
|
||||
"wms_received_dir": "wms_received",
|
||||
"wms_fake_ack_mode": "always-ack",
|
||||
"wms_fake_processing_sec": 0.5,
|
||||
"wms_ui_enabled": True,
|
||||
"wms_ocr_enabled": True,
|
||||
"wms_ocr_mode": "paddleocr",
|
||||
"wms_tesseract_cmd": "",
|
||||
"wms_paddle_python": r"C:\devel\yolo-ocr\.venv-paddle-cpu\Scripts\python.exe",
|
||||
"wms_paddle_worker_script": str(Path(__file__).with_name("flywms_paddleocr_worker.py")),
|
||||
"wms_paddle_target_heights": "96",
|
||||
"wms_paddle_variant_set": "fast",
|
||||
"wms_paddle_expected_digits": 6,
|
||||
"wms_paddle_min_votes": 2,
|
||||
"wms_paddle_min_confidence": 0.70,
|
||||
"wms_fake_ocr_prefix": "UDC",
|
||||
"wms_undetermined_code_text": "udc non determinato",
|
||||
"wms_fake_ocr_delay_sec": 0.2,
|
||||
"wms_operator": "udrone",
|
||||
"wms_site": "demo-magazzino",
|
||||
}
|
||||
|
||||
path = Path(path_str)
|
||||
if not path.exists():
|
||||
return defaults
|
||||
|
||||
parser = configparser.ConfigParser()
|
||||
parser.read(path, encoding="utf-8")
|
||||
section = parser["navigation"] if parser.has_section("navigation") else {}
|
||||
for key, default_value in defaults.items():
|
||||
if key not in section:
|
||||
continue
|
||||
if isinstance(default_value, bool):
|
||||
defaults[key] = parser.getboolean("navigation", key, fallback=default_value)
|
||||
elif isinstance(default_value, int):
|
||||
defaults[key] = parser.getint("navigation", key, fallback=default_value)
|
||||
elif isinstance(default_value, float):
|
||||
defaults[key] = parser.getfloat("navigation", key, fallback=default_value)
|
||||
else:
|
||||
defaults[key] = section.get(key, str(default_value)).strip()
|
||||
return defaults
|
||||
|
||||
|
||||
def parse_args():
|
||||
pre = argparse.ArgumentParser(add_help=False)
|
||||
pre.add_argument("--config", default=DEFAULT_CONFIG_PATH, help="File configurazione INI")
|
||||
pre_args, _ = pre.parse_known_args()
|
||||
defaults = load_server_config(pre_args.config)
|
||||
|
||||
ap = argparse.ArgumentParser(parents=[pre])
|
||||
ap.add_argument("--host", default=defaults["wms_server_host"], help="Host bind server")
|
||||
ap.add_argument("--port", type=int, default=defaults["wms_server_port"], help="Porta server")
|
||||
ap.add_argument("--received-dir", default=defaults["wms_received_dir"], help="Directory upload ricevuti")
|
||||
ap.add_argument(
|
||||
"--fake-ack-mode",
|
||||
choices=["always-ack", "always-nack", "alternate", "random"],
|
||||
default=defaults["wms_fake_ack_mode"],
|
||||
help="Modalita risposta WMS demo",
|
||||
)
|
||||
ap.add_argument(
|
||||
"--fake-processing-sec",
|
||||
type=float,
|
||||
default=defaults["wms_fake_processing_sec"],
|
||||
help="Ritardo elaborazione finta",
|
||||
)
|
||||
ap.add_argument("--ui-enabled", action="store_true", default=defaults["wms_ui_enabled"], help="Mostra finestre OpenCV server")
|
||||
ap.add_argument("--no-ui", action="store_false", dest="ui_enabled", help="Disabilita finestre OpenCV server")
|
||||
ap.add_argument("--ocr-enabled", action="store_true", default=defaults["wms_ocr_enabled"], help="Esegue OCR reale se disponibile")
|
||||
ap.add_argument("--no-ocr", action="store_false", dest="ocr_enabled", help="Disabilita OCR reale e usa fallback demo")
|
||||
ap.add_argument("--ocr-mode", choices=["paddleocr", "easyocr", "tesseract", "fake"], default=defaults["wms_ocr_mode"], help="Motore OCR server")
|
||||
ap.add_argument("--tesseract-cmd", default=defaults["wms_tesseract_cmd"], help="Percorso esplicito tesseract.exe")
|
||||
ap.add_argument("--paddle-python", default=defaults["wms_paddle_python"], help="Python del virtualenv PaddleOCR")
|
||||
ap.add_argument("--paddle-worker-script", default=defaults["wms_paddle_worker_script"], help="Script worker PaddleOCR")
|
||||
ap.add_argument("--paddle-target-heights", default=defaults["wms_paddle_target_heights"], help="Altezze resize PaddleOCR, es. 96 oppure 64,96,128")
|
||||
ap.add_argument("--paddle-variant-set", choices=["fast", "balanced", "full"], default=defaults["wms_paddle_variant_set"], help="Numero varianti preprocessing PaddleOCR")
|
||||
ap.add_argument("--paddle-expected-digits", type=int, default=defaults["wms_paddle_expected_digits"], help="Numero cifre atteso codice UDC")
|
||||
ap.add_argument("--paddle-min-votes", type=int, default=defaults["wms_paddle_min_votes"], help="Consenso minimo varianti PaddleOCR")
|
||||
ap.add_argument("--paddle-min-confidence", type=float, default=defaults["wms_paddle_min_confidence"], help="Confidenza minima PaddleOCR")
|
||||
ap.add_argument("--fake-ocr-prefix", default=defaults["wms_fake_ocr_prefix"], help="Prefisso fallback OCR demo")
|
||||
ap.add_argument("--undetermined-code-text", default=defaults["wms_undetermined_code_text"], help="Testo usato se OCR non determina il codice")
|
||||
ap.add_argument("--fake-ocr-delay-sec", type=float, default=defaults["wms_fake_ocr_delay_sec"], help="Ritardo OCR demo/fallback")
|
||||
ap.add_argument("--operator", default=defaults["wms_operator"], help="Operatore WMS demo")
|
||||
ap.add_argument("--site", default=defaults["wms_site"], help="Sito/magazzino demo")
|
||||
return ap.parse_args()
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
def health() -> dict[str, str]:
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@app.post("/api/v1/navigation-snapshot")
|
||||
async def receive_navigation_snapshot(
|
||||
metadata: str = Form(...),
|
||||
label_image: UploadFile = File(...),
|
||||
gaylord_image: UploadFile | None = File(None),
|
||||
) -> dict[str, object]:
|
||||
started = time.perf_counter()
|
||||
try:
|
||||
meta = json.loads(metadata)
|
||||
except json.JSONDecodeError:
|
||||
meta = {"raw_metadata": metadata}
|
||||
|
||||
with state_lock:
|
||||
server_state["counter"] = int(server_state["counter"]) + 1
|
||||
counter = int(server_state["counter"])
|
||||
|
||||
request_id = str(meta.get("request_id") or f"server-{counter:06d}")
|
||||
snapshot_id = meta.get("snapshot_id", counter)
|
||||
received_dir = Path(str(server_state["received_dir"]))
|
||||
request_dir = received_dir / f"{counter:06d}_{safe_name(request_id)}"
|
||||
request_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
label_path = request_dir / safe_upload_name(label_image.filename, "label.jpg")
|
||||
label_bytes = await label_image.read()
|
||||
label_path.write_bytes(label_bytes)
|
||||
|
||||
gaylord_path = None
|
||||
gaylord_bytes = b""
|
||||
if gaylord_image is not None:
|
||||
gaylord_path = request_dir / safe_upload_name(gaylord_image.filename, "gaylord.jpg")
|
||||
gaylord_bytes = await gaylord_image.read()
|
||||
gaylord_path.write_bytes(gaylord_bytes)
|
||||
|
||||
(request_dir / "metadata.json").write_text(
|
||||
json.dumps(meta, indent=2, ensure_ascii=True),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
image = cv2.imdecode(np.frombuffer(label_bytes, dtype=np.uint8), cv2.IMREAD_COLOR)
|
||||
ocr_result = await asyncio.to_thread(run_server_ocr, image, int(snapshot_id), label_path)
|
||||
|
||||
fake_processing_sec = float(server_state["fake_processing_sec"])
|
||||
if fake_processing_sec > 0:
|
||||
await asyncio.sleep(fake_processing_sec)
|
||||
|
||||
status = choose_status(str(server_state["fake_ack_mode"]), counter)
|
||||
udc_code = ocr_result.text if status == "ACK" else ""
|
||||
message = (
|
||||
f"codice valido su WMS: {udc_code}"
|
||||
if status == "ACK"
|
||||
else f"codice non riconosciuto: {ocr_result.text}"
|
||||
)
|
||||
processing_ms = (time.perf_counter() - started) * 1000.0
|
||||
received_at = datetime.now(timezone.utc).isoformat()
|
||||
wms_payload = {
|
||||
"request_id": request_id,
|
||||
"client_id": meta.get("client_id", ""),
|
||||
"site": server_state["site"],
|
||||
"operator": server_state["operator"],
|
||||
"received_at": received_at,
|
||||
"snapshot_id": snapshot_id,
|
||||
"position": meta.get("simulated_position", ""),
|
||||
"track_id": meta.get("track_id", ""),
|
||||
"udc_code": udc_code,
|
||||
"ocr_raw_text": ocr_result.raw_text,
|
||||
"ocr_confidence": ocr_result.confidence,
|
||||
"ocr_backend": ocr_result.backend,
|
||||
"ocr_fallback_used": ocr_result.fallback_used,
|
||||
"ocr_votes": ocr_result.votes,
|
||||
"ocr_variant": ocr_result.variant,
|
||||
"validation_status": status,
|
||||
"validation_message": message,
|
||||
"label_bbox": meta.get("label_bbox", []),
|
||||
"gaylord_bbox": meta.get("gaylord_bbox", []),
|
||||
"movement_vector_px": meta.get("movement_vector_px", {}),
|
||||
"label_image_path": str(label_path),
|
||||
"gaylord_image_path": str(gaylord_path) if gaylord_path else "",
|
||||
}
|
||||
(request_dir / "wms_payload.json").write_text(
|
||||
json.dumps(wms_payload, indent=2, ensure_ascii=True),
|
||||
encoding="utf-8",
|
||||
)
|
||||
update_ui_state(image, wms_payload)
|
||||
|
||||
log(
|
||||
f"received request_id={request_id} snapshot={snapshot_id} "
|
||||
f"label_bytes={len(label_bytes)} gaylord_bytes={len(gaylord_bytes)} "
|
||||
f"ocr='{ocr_result.text}' status={status}"
|
||||
)
|
||||
return {
|
||||
"status": status,
|
||||
"request_id": request_id,
|
||||
"message": message,
|
||||
"ocr_text": udc_code,
|
||||
"ocr_raw_text": ocr_result.raw_text,
|
||||
"ocr_confidence": ocr_result.confidence,
|
||||
"ocr_backend": ocr_result.backend,
|
||||
"ocr_fallback_used": ocr_result.fallback_used,
|
||||
"ocr_votes": ocr_result.votes,
|
||||
"ocr_variant": ocr_result.variant,
|
||||
"processing_ms": round(processing_ms, 1),
|
||||
"saved_dir": str(request_dir),
|
||||
"wms_payload_path": str(request_dir / "wms_payload.json"),
|
||||
"label_image_path": str(label_path),
|
||||
"gaylord_image_path": str(gaylord_path) if gaylord_path else "",
|
||||
}
|
||||
|
||||
|
||||
def run_server_ocr(image: np.ndarray | None, snapshot_id: int, label_path: Path | None = None) -> OcrServerResult:
|
||||
fake_delay = float(server_state["fake_ocr_delay_sec"])
|
||||
if fake_delay > 0:
|
||||
time.sleep(fake_delay)
|
||||
fallback = str(server_state["undetermined_code_text"])
|
||||
if image is None:
|
||||
return OcrServerResult(fallback, "", 0.0, "fallback", True)
|
||||
if not bool(server_state["ocr_enabled"]) or str(server_state["ocr_mode"]) == "fake":
|
||||
return OcrServerResult(fallback, fallback, 1.0, "fake", True)
|
||||
if str(server_state["ocr_mode"]) == "paddleocr":
|
||||
if label_path is None:
|
||||
return OcrServerResult(fallback, "", 0.0, "paddleocr-missing-path", True)
|
||||
return run_paddleocr_ocr(label_path, fallback)
|
||||
if str(server_state["ocr_mode"]) == "tesseract":
|
||||
return run_tesseract_ocr(image, fallback)
|
||||
|
||||
try:
|
||||
reader = get_easyocr_reader()
|
||||
ocr_input = preprocess_label_for_ocr(image)
|
||||
results = reader.readtext(
|
||||
ocr_input,
|
||||
allowlist="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-",
|
||||
detail=1,
|
||||
paragraph=False,
|
||||
)
|
||||
except Exception as exc:
|
||||
log(f"OCR fallback: {exc}")
|
||||
return OcrServerResult(fallback, "", 0.0, "easyocr-error", True)
|
||||
|
||||
raw_parts: list[str] = []
|
||||
best_text = ""
|
||||
best_conf = 0.0
|
||||
for item in results:
|
||||
text = str(item[1]).strip()
|
||||
conf = float(item[2]) if len(item) > 2 else 0.0
|
||||
if text:
|
||||
raw_parts.append(text)
|
||||
candidate = normalize_ocr_code(text)
|
||||
if candidate and conf >= best_conf:
|
||||
best_text = candidate
|
||||
best_conf = conf
|
||||
|
||||
raw_text = " ".join(raw_parts)
|
||||
if not best_text:
|
||||
digits = re.sub(r"\D+", "", raw_text)
|
||||
if digits:
|
||||
best_text = digits
|
||||
best_conf = max(best_conf, 0.01)
|
||||
if best_text:
|
||||
return OcrServerResult(best_text, raw_text, best_conf, "easyocr", False)
|
||||
return OcrServerResult(fallback, raw_text, best_conf, "easyocr-fallback", True)
|
||||
|
||||
|
||||
def get_paddle_worker() -> PaddleOcrWorker:
|
||||
global _paddle_worker
|
||||
with _paddle_worker_lock:
|
||||
if _paddle_worker is None:
|
||||
_paddle_worker = PaddleOcrWorker(
|
||||
python_path=str(server_state["paddle_python"]),
|
||||
script_path=str(server_state["paddle_worker_script"]),
|
||||
target_heights=str(server_state["paddle_target_heights"]),
|
||||
variant_set=str(server_state["paddle_variant_set"]),
|
||||
expected_digits=int(server_state["paddle_expected_digits"]),
|
||||
min_votes=int(server_state["paddle_min_votes"]),
|
||||
min_confidence=float(server_state["paddle_min_confidence"]),
|
||||
)
|
||||
return _paddle_worker
|
||||
|
||||
|
||||
def close_paddle_worker() -> None:
|
||||
global _paddle_worker
|
||||
with _paddle_worker_lock:
|
||||
worker = _paddle_worker
|
||||
_paddle_worker = None
|
||||
if worker is not None:
|
||||
worker.close()
|
||||
|
||||
|
||||
def run_paddleocr_ocr(label_path: Path, fallback: str) -> OcrServerResult:
|
||||
try:
|
||||
response = get_paddle_worker().predict(label_path)
|
||||
except Exception as exc:
|
||||
log(f"PaddleOCR fallback: {exc}")
|
||||
close_paddle_worker()
|
||||
return OcrServerResult(fallback, "", 0.0, "paddleocr-error", True)
|
||||
|
||||
text = str(response.get("text") or "")
|
||||
best_text = str(response.get("best_text") or text)
|
||||
raw_text = str(response.get("raw_text") or best_text)
|
||||
confidence = float(response.get("confidence") or 0.0)
|
||||
votes = int(response.get("votes") or 0)
|
||||
variant = str(response.get("variant") or "")
|
||||
if response.get("ok") and text:
|
||||
return OcrServerResult(text, raw_text, confidence, "paddleocr", False, votes=votes, variant=variant)
|
||||
reason = str(response.get("reason") or "fallback")
|
||||
raw_with_reason = f"{raw_text} [{reason}]".strip()
|
||||
return OcrServerResult(fallback, raw_with_reason, confidence, "paddleocr-fallback", True, votes=votes, variant=variant)
|
||||
|
||||
|
||||
def run_tesseract_ocr(image: np.ndarray, fallback: str) -> OcrServerResult:
|
||||
try:
|
||||
import pytesseract
|
||||
except Exception as exc:
|
||||
log(f"Tesseract fallback: pytesseract non disponibile: {exc}")
|
||||
return OcrServerResult(fallback, "", 0.0, "tesseract-unavailable", True)
|
||||
|
||||
tesseract_cmd = str(server_state.get("tesseract_cmd") or "").strip()
|
||||
if tesseract_cmd:
|
||||
if not Path(tesseract_cmd).exists():
|
||||
log(f"Tesseract fallback: binario non trovato: {tesseract_cmd}")
|
||||
return OcrServerResult(fallback, "", 0.0, "tesseract-missing", True)
|
||||
pytesseract.pytesseract.tesseract_cmd = tesseract_cmd
|
||||
elif shutil.which("tesseract") is None:
|
||||
log("Tesseract fallback: tesseract.exe non trovato nel PATH")
|
||||
return OcrServerResult(fallback, "", 0.0, "tesseract-missing", True)
|
||||
|
||||
variants = build_tesseract_variants(image)
|
||||
candidates: list[tuple[str, float, str]] = []
|
||||
config = "--psm 7 --oem 3 -c tessedit_char_whitelist=0123456789"
|
||||
for name, variant in variants:
|
||||
try:
|
||||
data = pytesseract.image_to_data(
|
||||
variant,
|
||||
config=config,
|
||||
output_type=pytesseract.Output.DICT,
|
||||
)
|
||||
except Exception as exc:
|
||||
log(f"Tesseract fallback su variante {name}: {exc}")
|
||||
return OcrServerResult(fallback, "", 0.0, "tesseract-error", True)
|
||||
|
||||
texts = [text.strip() for text in data.get("text", []) if text and text.strip()]
|
||||
confs: list[float] = []
|
||||
for conf in data.get("conf", []):
|
||||
try:
|
||||
value = float(conf)
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
if value >= 0:
|
||||
confs.append(value / 100.0)
|
||||
raw = "".join(texts)
|
||||
digits = re.sub(r"\D+", "", raw)
|
||||
confidence = sum(confs) / len(confs) if confs else 0.0
|
||||
if digits:
|
||||
candidates.append((digits, confidence, name))
|
||||
|
||||
if not candidates:
|
||||
return OcrServerResult(fallback, "", 0.0, "tesseract-fallback", True)
|
||||
|
||||
votes: dict[str, list[float]] = {}
|
||||
for digits, confidence, _ in candidates:
|
||||
votes.setdefault(digits, []).append(confidence)
|
||||
best_digits, best_confs = max(
|
||||
votes.items(),
|
||||
key=lambda item: (len(item[1]), sum(item[1]) / max(len(item[1]), 1)),
|
||||
)
|
||||
consensus_count = len(best_confs)
|
||||
avg_conf = sum(best_confs) / consensus_count
|
||||
raw_text = "; ".join(f"{digits}@{conf:.2f}/{name}" for digits, conf, name in candidates)
|
||||
if consensus_count < 2 or avg_conf < 0.45:
|
||||
return OcrServerResult(fallback, raw_text, avg_conf, "tesseract-ambiguous", True)
|
||||
return OcrServerResult(best_digits, raw_text, avg_conf, "tesseract", False)
|
||||
|
||||
|
||||
def build_tesseract_variants(image: np.ndarray) -> list[tuple[str, np.ndarray]]:
|
||||
h, w = image.shape[:2]
|
||||
scale = 4.0 if w < 500 else 2.0
|
||||
resized = cuda_resize(image, (int(w * scale), int(h * scale)), interpolation=cv2.INTER_CUBIC)
|
||||
gray = cuda_cvt_color(resized, cv2.COLOR_BGR2GRAY)
|
||||
gray = cv2.bilateralFilter(gray, 7, 45, 45)
|
||||
sharpen = cv2.addWeighted(gray, 1.6, cv2.GaussianBlur(gray, (0, 0), 1.2), -0.6, 0)
|
||||
_, otsu = cv2.threshold(sharpen, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
|
||||
adaptive = cv2.adaptiveThreshold(
|
||||
sharpen,
|
||||
255,
|
||||
cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
|
||||
cv2.THRESH_BINARY,
|
||||
31,
|
||||
7,
|
||||
)
|
||||
return [
|
||||
("gray", gray),
|
||||
("sharpen", sharpen),
|
||||
("otsu", otsu),
|
||||
("adaptive", adaptive),
|
||||
("otsu_inv", cv2.bitwise_not(otsu)),
|
||||
]
|
||||
|
||||
|
||||
_easyocr_reader = None
|
||||
_easyocr_lock = threading.Lock()
|
||||
|
||||
|
||||
def get_easyocr_reader():
|
||||
global _easyocr_reader
|
||||
with _easyocr_lock:
|
||||
if _easyocr_reader is None:
|
||||
import easyocr
|
||||
|
||||
_easyocr_reader = easyocr.Reader(["en"], gpu=False, verbose=False)
|
||||
return _easyocr_reader
|
||||
|
||||
|
||||
def preprocess_label_for_ocr(image: np.ndarray) -> np.ndarray:
|
||||
h, w = image.shape[:2]
|
||||
scale = 3.0 if w < 450 else 2.0
|
||||
resized = cuda_resize(image, (int(w * scale), int(h * scale)), interpolation=cv2.INTER_CUBIC)
|
||||
gray = cuda_cvt_color(resized, cv2.COLOR_BGR2GRAY)
|
||||
gray = cv2.bilateralFilter(gray, 7, 45, 45)
|
||||
return cuda_cvt_color(gray, cv2.COLOR_GRAY2BGR)
|
||||
|
||||
|
||||
def normalize_ocr_code(text: str) -> str:
|
||||
compact = re.sub(r"[^0-9A-Za-z-]+", "", text).strip("-")
|
||||
digits = re.sub(r"\D+", "", compact)
|
||||
return digits if digits else compact
|
||||
|
||||
|
||||
def update_ui_state(image: np.ndarray | None, payload: dict[str, object]) -> None:
|
||||
if not bool(server_state["ui_enabled"]):
|
||||
return
|
||||
lines = [
|
||||
"ULTIMO PAYLOAD WMS",
|
||||
f"request_id: {payload.get('request_id', '')}",
|
||||
f"client_id: {payload.get('client_id', '')}",
|
||||
f"site: {payload.get('site', '')}",
|
||||
f"operator: {payload.get('operator', '')}",
|
||||
f"received_at: {payload.get('received_at', '')}",
|
||||
f"snapshot_id: {payload.get('snapshot_id', '')}",
|
||||
f"position: {payload.get('position', '')}",
|
||||
f"track_id: {payload.get('track_id', '')}",
|
||||
f"udc_code: {payload.get('udc_code', '')}",
|
||||
f"ocr_raw: {payload.get('ocr_raw_text', '')}",
|
||||
f"ocr_backend: {payload.get('ocr_backend', '')}",
|
||||
f"ocr_fallback: {payload.get('ocr_fallback_used', '')}",
|
||||
f"ocr_votes: {payload.get('ocr_votes', '')}",
|
||||
f"ocr_variant: {payload.get('ocr_variant', '')}",
|
||||
f"status: {payload.get('validation_status', '')}",
|
||||
f"label_bbox: {payload.get('label_bbox', '')}",
|
||||
f"movement: {payload.get('movement_vector_px', '')}",
|
||||
]
|
||||
with ui_lock:
|
||||
ui_state["image"] = None if image is None else image.copy()
|
||||
ui_state["payload_lines"] = lines
|
||||
ui_state["updated"] = True
|
||||
|
||||
|
||||
def choose_status(mode: str, counter: int) -> str:
|
||||
if mode == "always-nack":
|
||||
return "NACK"
|
||||
if mode == "alternate":
|
||||
return "ACK" if counter % 2 == 1 else "NACK"
|
||||
if mode == "random":
|
||||
return "ACK" if random.random() >= 0.5 else "NACK"
|
||||
return "ACK"
|
||||
|
||||
|
||||
def safe_upload_name(filename: str | None, fallback: str) -> str:
|
||||
name = Path(filename or fallback).name
|
||||
return safe_name(name) or fallback
|
||||
|
||||
|
||||
def safe_name(value: str) -> str:
|
||||
allowed = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789._-"
|
||||
return "".join(ch if ch in allowed else "_" for ch in value)[:160]
|
||||
|
||||
|
||||
def server_ui_loop() -> None:
|
||||
try:
|
||||
cv2.namedWindow("wms immagine ricevuta", cv2.WINDOW_NORMAL)
|
||||
cv2.namedWindow("wms payload", cv2.WINDOW_NORMAL)
|
||||
except cv2.error as exc:
|
||||
log(f"UI OpenCV disabilitata: highgui non disponibile ({exc})")
|
||||
with ui_lock:
|
||||
ui_state["stop"] = True
|
||||
return
|
||||
cv2.resizeWindow("wms immagine ricevuta", 640, 360)
|
||||
cv2.resizeWindow("wms payload", 900, 560)
|
||||
cv2.moveWindow("wms immagine ricevuta", 40, 40)
|
||||
cv2.moveWindow("wms payload", 720, 40)
|
||||
while True:
|
||||
with ui_lock:
|
||||
if ui_state["stop"]:
|
||||
break
|
||||
image = None if ui_state["image"] is None else ui_state["image"].copy()
|
||||
lines = list(ui_state["payload_lines"])
|
||||
if image is None:
|
||||
image_display = np.full((360, 640, 3), 240, dtype=np.uint8)
|
||||
cv2.putText(
|
||||
image_display,
|
||||
"In attesa immagine etichetta",
|
||||
(30, 180),
|
||||
cv2.FONT_HERSHEY_SIMPLEX,
|
||||
0.9,
|
||||
(0, 0, 0),
|
||||
2,
|
||||
cv2.LINE_AA,
|
||||
)
|
||||
else:
|
||||
image_display = resize_preview(image, 640)
|
||||
cv2.imshow("wms immagine ricevuta", image_display)
|
||||
cv2.imshow("wms payload", draw_payload_window(lines))
|
||||
key = cv2.waitKey(100) & 0xFF
|
||||
if key in (27, ord("q")):
|
||||
with ui_lock:
|
||||
ui_state["stop"] = True
|
||||
break
|
||||
cv2.destroyWindow("wms immagine ricevuta")
|
||||
cv2.destroyWindow("wms payload")
|
||||
|
||||
|
||||
def resize_preview(frame: np.ndarray, max_width: int) -> np.ndarray:
|
||||
h, w = frame.shape[:2]
|
||||
if max_width <= 0 or w <= max_width:
|
||||
return frame
|
||||
scale = max_width / float(w)
|
||||
return cuda_resize(frame, (int(w * scale), int(h * scale)), interpolation=cv2.INTER_LINEAR)
|
||||
|
||||
|
||||
def draw_payload_window(lines: list[str]) -> np.ndarray:
|
||||
canvas = np.full((560, 900, 3), 245, dtype=np.uint8)
|
||||
y = 42
|
||||
for idx, line in enumerate(lines[:18]):
|
||||
color = (0, 0, 160) if idx == 0 else (0, 0, 0)
|
||||
scale = 0.9 if idx == 0 else 0.63
|
||||
thickness = 2 if idx == 0 else 1
|
||||
cv2.putText(
|
||||
canvas,
|
||||
str(line)[:110],
|
||||
(24, y),
|
||||
cv2.FONT_HERSHEY_SIMPLEX,
|
||||
scale,
|
||||
color,
|
||||
thickness,
|
||||
cv2.LINE_AA,
|
||||
)
|
||||
y += 34 if idx == 0 else 30
|
||||
return canvas
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
server_state["received_dir"] = args.received_dir
|
||||
server_state["fake_ack_mode"] = args.fake_ack_mode
|
||||
server_state["fake_processing_sec"] = args.fake_processing_sec
|
||||
server_state["ui_enabled"] = args.ui_enabled
|
||||
server_state["ocr_enabled"] = args.ocr_enabled
|
||||
server_state["ocr_mode"] = args.ocr_mode
|
||||
server_state["tesseract_cmd"] = args.tesseract_cmd
|
||||
server_state["paddle_python"] = args.paddle_python
|
||||
server_state["paddle_worker_script"] = args.paddle_worker_script
|
||||
server_state["paddle_target_heights"] = args.paddle_target_heights
|
||||
server_state["paddle_variant_set"] = args.paddle_variant_set
|
||||
server_state["paddle_expected_digits"] = args.paddle_expected_digits
|
||||
server_state["paddle_min_votes"] = args.paddle_min_votes
|
||||
server_state["paddle_min_confidence"] = args.paddle_min_confidence
|
||||
server_state["fake_ocr_prefix"] = args.fake_ocr_prefix
|
||||
server_state["undetermined_code_text"] = args.undetermined_code_text
|
||||
server_state["fake_ocr_delay_sec"] = args.fake_ocr_delay_sec
|
||||
server_state["operator"] = args.operator
|
||||
server_state["site"] = args.site
|
||||
Path(args.received_dir).mkdir(parents=True, exist_ok=True)
|
||||
log(
|
||||
f"FlyWMS WMS demo server host={args.host} port={args.port} "
|
||||
f"mode={args.fake_ack_mode} received_dir={args.received_dir}"
|
||||
)
|
||||
log(f"OpenCV CUDA: {'attivo' if OPENCV_CUDA_AVAILABLE else 'non disponibile'}")
|
||||
ui_thread = None
|
||||
if args.ui_enabled:
|
||||
ui_thread = threading.Thread(target=server_ui_loop, name="wms-server-ui", daemon=True)
|
||||
ui_thread.start()
|
||||
try:
|
||||
uvicorn.run(app, host=args.host, port=args.port, log_level="info")
|
||||
finally:
|
||||
close_paddle_worker()
|
||||
with ui_lock:
|
||||
ui_state["stop"] = True
|
||||
if ui_thread is not None:
|
||||
ui_thread.join(timeout=2.0)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user