245 lines
4.5 KiB
Markdown
245 lines
4.5 KiB
Markdown
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<float> 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 tabelle**adattiamo `face-rknn` per produrre i record `faces`
|