# Riconoscimento facciale nel server s141js ## Scopo del documento Questo documento descrive il funzionamento attuale del riconoscimento facciale nel server `s141js`. La soluzione usa due percorsi paralleli: - Python produce il risultato ufficiale e lo salva nel database; - OpenCL sulla GPU Mali-G610 ricalcola lo stesso risultato in modalità shadow e verifica la coerenza. La modalità shadow non sostituisce ancora il risultato Python e non modifica le valutazioni ufficiali. ## Legenda dei segnaposto Il file usa segnaposto al posto dei caratteri Markdown che possono essere alterati durante la copia in chat o nel terminale. ```text # titolo livello 1, da convertire in # ## titolo livello 2, da convertire in ## ### titolo livello 3, da convertire in ### #### titolo livello 4, da convertire in #### __FENCE__ apertura o chiusura di un blocco di codice __FENCE_TEXT__ apertura di un blocco text __FENCE_BASH__ apertura di un blocco bash __FENCE_JSON__ apertura di un blocco json __FENCE_JS__ apertura di un blocco javascript ` apice rovescio singolo ``` ## Architettura generale ```text Fotografia | v SCRFD tramite RKNN e NPU | v Bounding box e cinque landmark | v Allineamento del volto a 112 x 112 | v SFace tramite RKNN e NPU | v Embedding float32[128] normalizzato L2 | +-------------------------------+ | | v v Pipeline Python ufficiale kNN OpenCL shadow | | v v Valutazioni salvate Confronto di coerenza nel database Python contro OpenCL | | +---------------+---------------+ | v Report e stato del job ``` ## Componenti principali ### Face engine RKNN Il face engine usa la NPU del dispositivo RK3588 per: - rilevare i volti con SCRFD; - calcolare confidence, bounding box e landmark; - allineare il volto; - generare un embedding SFace; - normalizzare l'embedding con norma L2. File principali: ```text include/face_config.h include/face_engine.h src/face_engine.cc src/face_recognition.cc src/face_scan.cc ``` Ogni embedding ha il seguente formato: ```text Dimensione: 128 componenti Tipo: float32 Dimensione binaria: 512 byte Ordine byte: little-endian Normalizzazione: L2 ``` Poiché gli embedding sono normalizzati, la cosine similarity coincide con il prodotto scalare. ### Motore kNN nativo Il motore nativo è installato in: ```text native/face-knn/ ``` File principali: ```text native/face-knn/bin/face_knn native/face-knn/kernels/knn_float4_topk.cl native/face-knn/run-face-knn.sh native/face-knn/config.json native/face-knn/SHA256SUMS ``` Il launcher `run-face-knn.sh` imposta la directory di lavoro corretta prima di avviare il binario. Questo permette a `face_knn` di trovare il kernel tramite il percorso relativo: ```text kernels/knn_float4_topk.cl ``` Il motore supporta due backend: ```text cpu opencl ``` Il backend CPU è il riferimento numerico e il fallback. Il backend OpenCL usa la GPU Mali-G610. ### Runtime Node.js Il modulo: ```text tools/faceKnnRuntime.js ``` ha queste responsabilità: 1. legge la configurazione da `native/face-knn/config.json`; 2. recupera gli embedding dal database SQLite; 3. preserva l'ordine delle query prodotto dall'esperimento Python; 4. rilegge i prototype attivi ordinati per `selection_order`; 5. verifica che ogni embedding sia un Buffer da 512 byte; 6. crea file binari temporanei per query e prototype; 7. genera la self-map per il leave-one-out; 8. avvia il binario nativo; 9. analizza il JSON prodotto; 10. elimina i file biometrici temporanei. I file temporanei vengono creati sotto: ```text var/face-knn/run-* ``` Durante l'esecuzione normale vengono rimossi automaticamente. ### Shadow runner Il comando indipendente: ```text tools/runFaceKnnShadow.js ``` confronta il risultato Python con quello del backend nativo. Confronta per ogni query: - prototype migliore; - best similarity; - media dei primi tre score; - decisione recognized; - esclusione leave-one-out. Il report contiene solo metadati e risultati numerici. Non contiene gli embedding. ## Database e tabelle utilizzate Il database attivo è: ```text api_v1/database.sqlite ``` Le tabelle principali sono: ### faces Contiene i volti e gli embedding da 512 byte. ### person_face_prototypes Contiene i prototype attivi di ogni persona. L'ordine è determinato da: ```text selection_order ``` La corrispondenza con gli indici OpenCL è: ```text selection_order 1 -> indice OpenCL 0 selection_order 2 -> indice OpenCL 1 selection_order 3 -> indice OpenCL 2 selection_order 4 -> indice OpenCL 3 selection_order 5 -> indice OpenCL 4 ``` ### face_prototype_jobs Contiene i job di ricalcolo dei prototype. Stati principali: ```text pending processing done error superseded ``` ### person_face_recognition_evaluations Contiene le valutazioni ufficiali prodotte dalla pipeline Python. Campi principali: ```text face_id prototype_job_id prototype_count best_similarity top3_mean best_prototype_face_id recognition_threshold recognized evaluated_at ``` ### face_recognition_index_state Tiene traccia dello stato dell'indice persistente. Stato attuale validato: ```text user: Fabio status: dirty dirty: 1 version: 0 prototype_count: 5 index_path: null metadata_path: null built_at: null last_error: null ``` Lo stato `dirty` indica che i prototype sono stati ricalcolati, ma l'indice persistente non è ancora stato costruito. ## Come nasce un job di ricalcolo Il router è montato con il prefisso: ```text /face-review ``` L'endpoint manuale è: ```text POST /face-review/persons/:personId/prototypes/recalculate ``` La richiesta richiede: ```text Authorization: Bearer TOKEN Content-Type: application/json ``` Esempio di body: ```json { "reason": "opencl_shadow_validation" } ``` La funzione `queuePrototypeRefresh()`: - crea un job pending se non ne esiste uno; - aggiorna il job pending esistente per evitare duplicati; - salva reason, requested_by e timestamp. Il ricalcolo viene accodato anche automaticamente quando: - viene assegnato un cluster a una persona; - un volto viene rimosso da una persona; - un volto noise viene assegnato; - un volto viene riassegnato; - più volti noise vengono assegnati insieme. ## Elaborazione del job Il worker è: ```text tools/runPrototypeWorker.js ``` Esecuzione manuale: ```bash node tools/runPrototypeWorker.js \ --persist \ --job-id JOB_ID ``` Il flusso è il seguente. ### 1. Acquisizione del job Il worker cerca un job con stato `pending`, lo porta a `processing` e incrementa `attempts`. ### 2. Firma degli input Il worker calcola una firma SHA-256 basata sui volti e sulle esclusioni di riferimento. Se la firma cambia durante il lavoro, il job viene marcato `superseded` e viene creato un job sostitutivo. ### 3. Esperimento Python Il worker avvia: ```text tools/prototype_experiment.py ``` Parametri attuali: ```text noise_prototypes: 2 greedy_additions: 2 min_detector_score: 0.60 ``` L'esperimento produce: ```text prototype_sets.greedy_gain evaluations.greedy_gain.queries evaluations.greedy_gain.summary ``` Ogni query Python contiene: ```text face_id photo_name is_prototype best_similarity best_prototype_face_id top3_mean ``` ### 4. Selezione dei prototype Il worker: 1. applica le esclusioni manuali; 2. mantiene i prototype bloccati; 3. disattiva i prototype automatici precedenti; 4. salva i nuovi prototype; 5. assegna `selection_order` a partire da 1. Per Jessica l'ordine validato è: ```text 0 IMG_0145.JPG cluster_medoid 1 IMG_0141.JPG cluster_medoid 2 IMG_0193.JPG cluster_medoid 3 IMG_0143.JPG greedy_leave_one_out_gain 4 IMG_0146.JPG greedy_leave_one_out_gain ``` Il numero a sinistra è l'indice kNN, cioè `selection_order - 1`. ### 5. Valutazioni Python ufficiali La soglia attuale è: ```text 0.50 ``` Le valutazioni precedenti vengono sostituite atomicamente nella transazione SQLite. Il risultato Python viene salvato in `person_face_recognition_evaluations` ed è ancora il risultato ufficiale. ### 6. Aggiornamento dello stato indice Dopo il ricalcolo dei prototype: ```text status = dirty dirty = 1 prototype_count = numero dei prototype attivi last_error = null ``` ### 7. Chiusura della transazione `persistResult()` conclude la transazione prima dell'avvio OpenCL. Questo evita di mantenere un lock SQLite durante: - creazione dei file binari; - inizializzazione OpenCL; - compilazione del kernel; - esecuzione del kNN; - scrittura del report shadow. ### 8. Shadow OpenCL Dopo il commit della transazione, il worker chiama `runFaceKnnShadow()`. La sequenza è: ```text Risultato Python già salvato | v Recupero embedding query nello stesso ordine Python | v Recupero prototype ordinati per selection_order | v Generazione queries.bin Generazione prototypes.bin Generazione self-map.txt | v face_knn --backend opencl | v Confronto Python contro OpenCL | v Report shadow JSON ``` ## Leave-one-out Se una query è anche un prototype, il confronto con sé stessa deve essere escluso. Il runtime costruisce una mappa tra `face_id` e indice del prototype. La self-map contiene: ```text -1 -> nessuna esclusione 0 -> escludi prototype indice 0 1 -> escludi prototype indice 1 2 -> escludi prototype indice 2 ... ``` Il kernel calcola il TOPK completo. L'esclusione viene applicata sul risultato host. ## Output del motore nativo Il binario mantiene separati i flussi: ```text stdout -> JSON valido stderr -> diagnostica OpenCL e messaggi del driver Mali ``` Il runtime Mali può scrivere direttamente: ```text arm_release_ver: g24p0-00eac0, rk_so_ver: 10 ``` La CLI devia temporaneamente stdout verso stderr durante OpenCL e lo ripristina prima di scrivere il JSON. ## Controlli shadow Per ogni query vengono confrontati: - prototype migliore Python e OpenCL; - best similarity; - media dei primi tre score; - decisione recognized; - leave-one-out. Tolleranza configurata: ```text 0.00001 ``` Campi aggregati del report: ```text query_count prototype_count top1_matches decision_matches best_score_matches top3_mean_matches maximum_score_error maximum_top3_mean_error mismatch_count ``` I report automatici vengono scritti in: ```text var/face-knn/shadow-JOB_ID.json ``` Il report non contiene embedding. ## Gestione degli errori ### Errore Python o persistenza ```text Il job viene marcato error. last_error viene valorizzato. Il worker restituisce exit code non zero. ``` ### Errore OpenCL shadow ```text Il job Python resta done. Le valutazioni ufficiali restano salvate. L'errore viene registrato nel log shadow. Non viene effettuato rollback. ``` ### Mismatch Python/OpenCL ```text Il job Python resta done. Il report contiene mismatch_count maggiore di zero. OpenCL non viene promosso a motore ufficiale. ``` ## Configurazione attuale File: ```text native/face-knn/config.json ``` Configurazione validata: ```json { "enabled": true, "mode": "shadow", "backend": "opencl", "fallbackBackend": "cpu", "dimension": 128, "topK": 5, "recognitionThreshold": 0.5, "scoreTolerance": 0.00001, "autoBackendMinimumComparisons": 100000, "launcher": "native/face-knn/run-face-knn.sh", "indexDirectory": "api_v1/face-recognition-index", "runtimeDirectory": "var/face-knn" } ``` ## Validazione completata Persona validata: ```text User: Fabio Persona: Jessica Person ID: 77456100-d7b2-4c36-b3d6-09f8c1c9d7e7 Query: 21 Prototype: 5 ``` Risultato funzionale: ```text Riconosciuti: 20 Sotto soglia: 1 Copertura: 95.2% Caso sotto soglia: IMG_0183.JPG Score: 0.4664280414581299 Miglior prototype: IMG_0143.JPG Indice prototype: 3 ``` Risultato shadow automatico: ```text Top-1 identici: 21 / 21 Decisioni identiche: 21 / 21 Best score entro tolleranza: 21 / 21 Top-3 mean entro tolleranza: 21 / 21 Mismatch: 0 Errore massimo best score: 1.7869088742017425e-7 Errore massimo top-3 mean: 1.3911677043676463e-7 ``` Job validato: ```text Job ID: 7741c033-dae9-4b6f-b00f-bc082f08b8a1 Stato: done Tentativi: 1 Reason: opencl_shadow_validation Last error: null ``` ## Stato attuale della produzione ```text Python: - seleziona i prototype; - calcola le valutazioni; - salva il risultato ufficiale. OpenCL: - ricalcola il kNN dopo il commit; - confronta il risultato; - genera il report shadow; - non modifica il risultato ufficiale. ``` ## Prossimi passi 1. creare il builder dell'indice persistente; 2. generare atomicamente embeddings e metadati; 3. portare `face_recognition_index_state` da dirty a ready; 4. mantenere la modalità shadow per più job reali; 5. definire criteri di promozione OpenCL; 6. mantenere il backend CPU come fallback; 7. passare a OpenCL ufficiale solo dopo validazioni sufficienti. ## Comandi operativi utili ### Controllare i job pending ```bash sqlite3 -header -column \ api_v1/database.sqlite \ " SELECT id, user, person_id, status, reason, requested_at FROM face_prototype_jobs WHERE status = 'pending' ORDER BY requested_at; " ``` ### Eseguire un job specifico ```bash node tools/runPrototypeWorker.js \ --persist \ --job-id "JOB_ID" ``` ### Validare un report shadow ```bash python3 -m json.tool \ "var/face-knn/shadow-JOB_ID.json" \ > /dev/null \ && echo "OK: report shadow valido" ``` ### Controllare le valutazioni ufficiali ```bash sqlite3 -header -column \ api_v1/database.sqlite \ " SELECT COUNT(*) AS evaluations, SUM(CASE WHEN recognized = 1 THEN 1 ELSE 0 END) AS recognized_faces, SUM(CASE WHEN recognized = 0 THEN 1 ELSE 0 END) AS below_threshold FROM person_face_recognition_evaluations; " ``` ### Controllare lo stato dell'indice ```bash sqlite3 -header -column \ api_v1/database.sqlite \ " SELECT user, status, dirty, version, prototype_count, index_path, metadata_path, built_at, last_error FROM face_recognition_index_state WHERE user = 'Fabio'; " ``` ## Conversione in Markdown standard Per generare un file Markdown normale: ```bash sed \ -e 's/^__H4__[[:space:]]*/#### /' \ -e 's/^__H3__[[:space:]]*/### /' \ -e 's/^__H2__[[:space:]]*/## /' \ -e 's/^__H1__[[:space:]]*/# /' \ -e 's/^__FENCE_TEXT__$/```text/' \ -e 's/^__FENCE_BASH__$/```bash/' \ -e 's/^__FENCE_JSON__$/```json/' \ -e 's/^__FENCE_JS__$/```javascript/' \ -e 's/^__FENCE__$/```/' \ -e 's/`/`/g' \ RICONOSCIMENTO_SERVER.src.md \ > RICONOSCIMENTO_SERVER.md ``` Verifica che non siano rimasti segnaposto: ```bash grep -nE \ '__H[1-4]__|__FENCE|`' \ RICONOSCIMENTO_SERVER.md \ || echo "OK: segnaposto convertiti" ```