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

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à:

  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:

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:

  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 è:

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

  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

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"