14 KiB
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.
# 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
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:
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:
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:
native/face-knn/
File principali:
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:
kernels/knn_float4_topk.cl
Il motore supporta due backend:
cpu
opencl
Il backend CPU è il riferimento numerico e il fallback. Il backend OpenCL usa la GPU Mali-G610.
Runtime Node.js
Il modulo:
tools/faceKnnRuntime.js
ha queste responsabilità:
- legge la configurazione da
native/face-knn/config.json; - recupera gli embedding dal database SQLite;
- preserva l'ordine delle query prodotto dall'esperimento Python;
- rilegge i prototype attivi ordinati per
selection_order; - verifica che ogni embedding sia un Buffer da 512 byte;
- crea file binari temporanei per query e prototype;
- genera la self-map per il leave-one-out;
- avvia il binario nativo;
- analizza il JSON prodotto;
- elimina i file biometrici temporanei.
I file temporanei vengono creati sotto:
var/face-knn/run-*
Durante l'esecuzione normale vengono rimossi automaticamente.
Shadow runner
Il comando indipendente:
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 è:
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:
selection_order
La corrispondenza con gli indici OpenCL è:
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:
pending
processing
done
error
superseded
person_face_recognition_evaluations
Contiene le valutazioni ufficiali prodotte dalla pipeline Python.
Campi principali:
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:
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:
/face-review
L'endpoint manuale è:
POST /face-review/persons/:personId/prototypes/recalculate
La richiesta richiede:
Authorization: Bearer TOKEN
Content-Type: application/json
Esempio di body:
{
"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 è:
tools/runPrototypeWorker.js
Esecuzione manuale:
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:
tools/prototype_experiment.py
Parametri attuali:
noise_prototypes: 2
greedy_additions: 2
min_detector_score: 0.60
L'esperimento produce:
prototype_sets.greedy_gain
evaluations.greedy_gain.queries
evaluations.greedy_gain.summary
Ogni query Python contiene:
face_id
photo_name
is_prototype
best_similarity
best_prototype_face_id
top3_mean
4. Selezione dei prototype
Il worker:
- applica le esclusioni manuali;
- mantiene i prototype bloccati;
- disattiva i prototype automatici precedenti;
- salva i nuovi prototype;
- assegna
selection_ordera partire da 1.
Per Jessica l'ordine validato è:
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 è:
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:
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 è:
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:
-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:
stdout -> JSON valido
stderr -> diagnostica OpenCL e messaggi del driver Mali
Il runtime Mali può scrivere direttamente:
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:
0.00001
Campi aggregati del report:
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:
var/face-knn/shadow-JOB_ID.json
Il report non contiene embedding.
Gestione degli errori
Errore Python o persistenza
Il job viene marcato error.
last_error viene valorizzato.
Il worker restituisce exit code non zero.
Errore OpenCL shadow
Il job Python resta done.
Le valutazioni ufficiali restano salvate.
L'errore viene registrato nel log shadow.
Non viene effettuato rollback.
Mismatch Python/OpenCL
Il job Python resta done.
Il report contiene mismatch_count maggiore di zero.
OpenCL non viene promosso a motore ufficiale.
Configurazione attuale
File:
native/face-knn/config.json
Configurazione validata:
{
"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:
User: Fabio
Persona: Jessica
Person ID: 77456100-d7b2-4c36-b3d6-09f8c1c9d7e7
Query: 21
Prototype: 5
Risultato funzionale:
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:
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:
Job ID: 7741c033-dae9-4b6f-b00f-bc082f08b8a1
Stato: done
Tentativi: 1
Reason: opencl_shadow_validation
Last error: null
Stato attuale della produzione
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
- creare il builder dell'indice persistente;
- generare atomicamente embeddings e metadati;
- portare
face_recognition_index_stateda dirty a ready; - mantenere la modalità shadow per più job reali;
- definire criteri di promozione OpenCL;
- mantenere il backend CPU come fallback;
- passare a OpenCL ufficiale solo dopo validazioni sufficienti.
Comandi operativi utili
Controllare i job pending
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
node tools/runPrototypeWorker.js \
--persist \
--job-id "JOB_ID"
Validare un report shadow
python3 -m json.tool \
"var/face-knn/shadow-JOB_ID.json" \
> /dev/null \
&& echo "OK: report shadow valido"
Controllare le valutazioni ufficiali
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
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:
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:
grep -nE \
'__H[1-4]__|__FENCE|`' \
RICONOSCIMENTO_SERVER.md \
|| echo "OK: segnaposto convertiti"