Software/Arduino Core/Neural Networks

Face Recognition

The FaceID library's detector and embedder cascade, landmark alignment, threshold selection and limitations.

advanced3 min read

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

cpp
#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:

CropMean cosine similarity
Detector box, best expansion tried (1.2x)0.54
Landmark aligned0.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.