Face Recognition
The FaceID library's detector and embedder cascade, landmark alignment, threshold selection and limitations.
The FaceID library implements face recognition as a two-stage cascade: a CenterFace detector finds every face in the frame, and a MobileFaceNet embedder converts each aligned face crop into an embedding that is compared against a bank of enrolled faces. The FaceRecognition example, under File > Examples > FaceID, shows a red box with an enrolment button for an unknown face and a green box for a recognised one.
Structure
#include <OV5640_Arduino.h>
#include <PostProcess.h>
#include <n6_face.h>
#include <n6_face_bank.h>
#pragma neuron6 model="centerface-hwc.tflite" name=face_det
#pragma neuron6 model="face4-int8.onnx" name=face_emb
OV5640 camera(CARRIER_TFT);
NEURON6_DECLARE_MODEL(face_det);
NEURON6_DECLARE_MODEL(face_emb);
static n6_face_bank_t bank;
void setup() {
VisionConfig cfg;
cfg.camera = CAMERA_WVGA;
cfg.nn.width = 128; cfg.nn.height = 128; // detector input
cfg.nn.aspect = ASPECT_CROP_CENTER;
cfg.transport = TRANSPORT_LCD;
Vision.begin(cfg);
n6_face_bank_init(&bank);
}
void loop() {
Vision.runWith(&NN_Instance_face_det, n6_face_detector(), OVL_CLEAR);
for (int i = 0; i < n6_face_count(); i++) {
const n6_face_t *f = n6_face_get(i);
Vision.runRoi(n6_face_align_roi(f), &NN_Instance_face_emb,
n6_face_embedder(), OVL_RELOAD_CLEAR_NONE);
if (n6_face_embedding_valid()) {
float sim;
int who = n6_face_bank_match(&bank, n6_face_embedding(), 0.45f, &sim);
// who >= 0: enrolled index. who < 0: unknown
}
}
OVL_Commit();
}CenterFace runs at 128x128 and returns each face as a box with five landmarks: two eyes, the nose and two mouth corners. n6_face_align_roi() builds a region from the landmarks, and runRoi() feeds the aligned crop to MobileFaceNet, which returns a 128-value embedding. n6_face_bank_match() returns the index of the best match above the threshold, or -1.
Enrolment adds the current embedding to the bank under a name. The bank is held in RAM; persistence across a power cycle uses the asset store.
Detector cadence
The detector runs on every frame. Unlike the hand cascade, no tracker is used: people enter and leave independently, and any face may be a new person, so the set of faces is re-established each frame. At 128x128 the detector is inexpensive.
Alignment
The embedding network was trained on ArcFace-aligned crops with the eyes at fixed positions. n6_face_align_roi() reproduces that alignment from the detector's landmarks with a translation and uniform scale, which the axis-aligned cropper can express.
Measured on 120 faces, scoring each crop against the face's reference crop:
| Crop | Mean cosine similarity |
|---|---|
| Detector box, best expansion tried (1.2x) | 0.54 |
| Landmark aligned | 0.79 |
The difference exceeds the gap between same-person and different-person scores, so alignment determines whether recognition functions. It requires no additional inference.
Threshold
Embeddings are L2-normalised, so a match score is a cosine. Measured over 240 faces of 80 identities through this model and feed path, same-person pairs averaged 0.41 and different-person pairs averaged 0.05 with a 99th percentile of 0.33. The example uses 0.45. A higher threshold, towards 0.6, reduces false accepts. A lower threshold, towards 0.35, tolerates poor angles and lighting.
Limitations
Head roll is not corrected, for the same reason the hand crop is not rotated: the cropper is axis aligned. A tilted head embeds less well than an upright one.
The system stores one signature per person, performs no liveness check, and recognises a photograph of a person. It is a demonstration of a two-stage NPU pipeline and is not suitable for access control.
Custom face models
Two properties of a face model are checked before use.
Input layout. A model converted from an NCHW source may carry an input tensor shaped [1,3,128,128]. Under the TFLite NHWC convention the compiler reads this as height 3, width 128 and 128 channels, generates a planar input, and the network receives scrambled pixels. The generate report's input line must read [b:1,h:H,w:W,c:3]. The --inputs-ch-position option does not correct this, because the compiler already treats the last dimension as channels. The core's tools/tflite_input_to_hwc.py moves the graph input past the leading transpose; centerface-hwc.tflite is produced from centerface.tflite this way.
Input quantisation. The recogniser is quantised symmetrically, which Bring Your Own Model warns against for models trained on 0 to 1 inputs. MobileFaceNet is trained on inputs from -1 to 1, and the uint8 re-encoding of a symmetric scale reproduces that convention. The warning applies only to models expecting a 0 to 1 range.