core: Add ArcFace embedder implementation
Last real-model piece of the pipeline. Takes a 112×112 RGB8 chip produced
by `align_face`, runs a single ONNX forward pass through the ArcFace
glintr100 model, and returns a 512-d L2-normalised embedding ready for
the matching layer in M4.
Substantially simpler than the SCRFD detector: single input, single
output, no anchor decoding, no NMS. Preprocessing differs from SCRFD's
in one detail — ArcFace's training used `(pixel − 127.5) / 127.5`
(range [-1, 1]) where SCRFD used `/ 128.0` — and the embedder is strict
about input dimensions: 112×112 only, no rescaling on input. Wrong shapes
surface as `InvalidInputShape` before any inference runs.
The 512-d output goes straight through `Embedding::normalized` so cosine
similarity reduces to a dot product in M4's matching layer. The output
dimension is enforced after inference; a model returning the wrong
dimension surfaces as a Backend error rather than producing a malformed
embedding.
# Tests
4 pure unit tests covering the preprocessor (NCHW shape, the −1/+1
boundary mappings, per-channel separation into NCHW planes).
5 hardware-gated tests covering the embedder end-to-end via the real
ArcFace model:
- Non-RGB8 input is rejected before inference
- Wrong dimensions are rejected before inference
- Solid gray chip produces a 512-d unit-magnitude embedding
- Repeated inference is bit-for-bit deterministic (default CPU EP)
- Different inputs produce non-identical embeddings (sanity check)
Combined hardware suite now runs in ~16s (V4L2 capture dominates at ~11s;
all 5 ArcFace tests take ~5s combined including model load).