galleria_photo_face_rknn_op.../RICONOSCIMENTO_SERVER.md
2026-10-03 12:16:58 +02:00

761 lines
14 KiB
Markdown

# 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"
```