4.5 KiB
Perfetto. Ho guardato direttamente il repository e adesso possiamo progettare l'integrazione sul codice reale, non su ipotesi.
Il repository conferma che la pipeline produce esattamente ciò che ci serve: SCRFD → landmark → alignment 112×112 → SFace → 128 float L2-normalizzati. La Face attuale contiene già score, bbox, 5 landmark e std::vector embedding.
La cosa più importante
Non cambierei il modello RKNN.
Il formato ideale per il DB è già quello che abbiamo:
float embedding[128]
Il codice attuale fa anche già la L2 normalization prima di restituire l'embedding.
Quindi il flusso diventa:
photos.id │ ▼ SCRFD │ ├── bbox ├── confidence └── 5 landmarks │ ▼ alignment │ ▼ SFace │ ▼ float[128] │ ▼ faces │ ├── HDBSCAN │ └── recognition
E farei una piccola modifica all'architettura del repository
Attualmente face_recognition.cc è un programma di test: prende una/due immagini e stampa i risultati. Il main() stesso conferma che l'attuale uso è principalmente il confronto fra immagini.
Non lo trasformerei direttamente nel processo DB.
Creerei invece qualcosa del genere:
face-rknn/ ├── src/ │ ├── face_recognition.cc ← lasciamo il test attuale │ ├── face_engine.cc │ ├── face_engine.h │ └── face_scan.cc ← nuovo scanner DB
dove FaceEngine espone qualcosa tipo:
struct FaceResult { float score; float x1, y1, x2, y2; Point kps[5]; std::array<float, 128> embedding; };
class FaceEngine { public: bool init( const char* scrfd_model, const char* sface_model );
std::vector<FaceResult> process(
const Image& image
);
};
Così il codice RKNN resta riutilizzabile.
DB: ora possiamo definirlo con precisione
Dato che sei su SQLite/better-sqlite3, userei:
faces.embedding → BLOB
con esattamente:
128 * sizeof(float) = 512 byte
per volto.
E non salverei FP16.
Il modello restituisce FP16, ma il codice attuale li converte già in float e poi normalizza.
Quindi salviamo il risultato finale:
float32[128]
che è esattamente quello che poi il tuo OpenCL può consumare.
Per 350.000 volti:
350000 × 512 ≈ 179.2 MB
assolutamente gestibile.
Una correzione rispetto a quello che ti avevo detto prima
Avendo visto il codice vero, non introdurrei subito prototypes nel DB.
Prima farei funzionare questa pipeline:
photos ↓ faces ↓ OpenCL kNN ↓ HDBSCAN ↓ clusters ↓ utente assegna persona ↓ persons
Poi, quando implementiamo il riconoscimento incrementale, decidiamo se i prototype debbano essere:
una tabella persistente;
un subset di faces;
oppure un rappresentante/calcolo derivato.
Non voglio aggiungere complessità prima che serva.
E c'è un dettaglio molto utile
Il repository usa già:
cosine(a, b)
su embedding L2-normalizzati.
Quindi il tuo benchmark OpenCL che stiamo facendo è perfettamente allineato:
cosine(a,b)
dot(a,b)
per vettori normalizzati.
Questa è una buona notizia: non dobbiamo cambiare metrica.
Il tuo OpenCL può quindi ricevere direttamente i BLOB float32[128] e fare il dot product.
Quindi io procederei così
Passo 1 — DB
Aggiungiamo a init.js:
faces face_clusters persons
senza toccare photos.
Passo 2 — FaceEngine
Separiamo detector + SFace dal main() di test.
Passo 3 — scanner
Prende:
photos
e per ogni foto non ancora processata:
image ↓ faces ↓ embedding ↓ INSERT faces
Passo 4 — GPU
Solo quando abbiamo gli embedding nel DB:
SQLite ↓ float32 array ↓ OpenCL ↓ kNN TOPK=32
Passo 5 — HDBSCAN
Usiamo quel kNN per costruire:
core distance mutual reachability MST condensed tree cluster
Passo 6 — UI
L'utente vede i cluster e assegna:
cluster 123 → "Mario"
che significa:
faces.cluster_id = 123 faces.person_id = Mario
Poi il sistema incrementale può riconoscere direttamente Mario senza rieseguire HDBSCAN globale.
A questo punto non ti chiederei altro codice alla cieca: abbiamo già DB + backend + face engine + formato embedding.
Il prossimo passo concreto è che , compatibile con il tuo better-sqlite3, e subito dopo invece di fare soltanto il test da riga di comando. ti preparo la modifica di db/init.js per le tre tabelleadattiamo face-rknn per produrre i record faces