761 lines
14 KiB
Markdown
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"
|
|
```
|