Detector: YuNet
File:face_detection_yunet.onnx · Source: OpenCV Zoo · License: Apache-2.0
Runs through OpenCV’s cv::FaceDetectorYN, which wraps both the ONNX graph
and YuNet’s anchor-decoding + NMS post-processing — we don’t reimplement
that by hand. Output per detected face: a bounding box, 5 landmarks
(right eye, left eye, nose tip, right mouth corner, left mouth corner), and
a confidence score.
Embedder: ArcFace (w600k_r50)
File: arcface_w600k_r50.onnx · Source: InsightFace buffalo_l · License: MIT
InferenceEngine::initialize reads the input/output tensor names and
shape dynamically from the graph rather than hardcoding them — the
values above are what that introspection actually returns for this model,
confirmed via onnx/onnxruntime in Python before being trusted in the
C++ code.
What was verified, and how
1
I/O contract
Loaded the model with the
onnx and onnxruntime Python packages and
printed the real input/output names, shapes, and dtypes — the table
above. Confirmed the C++ code’s dynamic-shape-reading logic matches
reality rather than assuming NCHW/float32/512-d from a spec sheet.2
Alignment template
FaceAligner’s standard InsightFace 112×112 5-point template is
correct for this model (it’s the same model family/lineage). Confirmed
two ways: visually inspecting aligned 112×112 crops (clean, centered,
consistent eyebrow-to-chin framing across every test image), and
mathematically verifying estimateAffinePartial2D maps detected
landmarks to within ~1.5px of the reference template.3
Detection at full resolution
Found and fixed the YuNet full-resolution bug described above — this
was initially mistaken for a possible alignment problem, since garbage
detections produce garbage-looking aligned crops too. Drawing the raw
detector bbox/landmarks directly onto the pixel grid OpenCV actually
sees (not a viewer-rendered preview, which can apply its own
EXIF-rotation handling and mislead you) was what isolated it to
detection, not alignment.
4
Similarity threshold
See below — this took two iterations to get a
trustworthy reference number.
Models that were tried and discarded
Documenting these because the failure modes are instructive, not just for posterity.”VirtuoTuring” embedder (discarded)
An earlier embedder this service was originally built against. Its published I/O contract (NCHW,[-1,1] normalization, 512-d output) turned
out to be entirely accurate — but the model itself doesn’t discriminate
identity. On a manual test set of 10 photos (45 pairwise comparisons),
same-person and different-person pairs both landed in a 0.88–0.98
cosine-similarity band with no separation (mean 0.934, std 0.023).
A noise-image sanity check made the root cause clear: two unrelated
random-noise images scored 0.9999 similarity with each other under this
model, while genuinely same-person photos scored lower on average
(0.926) than cross-identity pairs (0.935). Detection, alignment, and
normalization were all independently confirmed correct by this point — the
model itself was the problem, most likely reflecting its small
(~23,660-image), unbenchmarked, low-adoption training provenance.
ONNX Model Zoo arcfaceresnet100-8.onnx (discarded)
The first replacement candidate tried, from the official
onnx/models repo — a high-trust
source, but an old export (opset 8, ~2019). The same noise-image sanity
check caught a different failure mode: this model executes as a
near-constant function regardless of input under current ONNX Runtime —
black vs. white images scored 0.988 similarity, i.e. the graph isn’t
meaningfully processing its input at all (likely an execution
compatibility issue between this old opset-8 export and modern ONNX
Runtime, not a training-quality issue like VirtuoTuring’s).
Similarity threshold
COMPARE_THRESHOLD defaults to 0.4 (see Configuration).
This came from running all pairwise comparisons across a 10-photo manual
test set (45 pairs — 4 genuine same-person, 41 cross-identity) through the
current w600k_r50 embedder:
0.4 sits in the gap between those two clusters with margin on both sides.
Sanity-checked against degenerate inputs on the same model:
Liveness heuristic
POST /v1/liveness (see API Reference) does
not use a dedicated liveness model — there isn’t one in this service.
It combines three signals already available from the detector pass:
Measured performance
CPU-only (Apple Silicon, arm64, no CUDA), sequential single-client requests,POST /v1/embed:
The same measurement taken inside the Docker image running under
amd64 emulation (QEMU, on the same Apple Silicon host) showed p50
~1026 ms — that’s emulation overhead, not a real regression. A native
x86_64 host should perform close to the numbers above. See
Deployment for the full Docker validation notes.Concurrency
Sequential-request latency (above) doesn’t tell you throughput under real concurrent load — and the first time this was actually measured, it uncovered a real bug, not just a capacity limit. This wasn’t found by reasoning about the code — it was found by actually running a concurrent load test (POST /v1/embed, increasing concurrency,
12s per level) against the native build on a 10-core Apple Silicon Mac:
Before the fix, throughput didn’t just plateau under load — it
collapsed, with corrupted detections (surfacing as spurious
no_face_detected 422s) and connections hanging for 30+ seconds. The
giveaway in the server logs was an OpenCV DNN warning that has nothing to
do with the actual input images:
setInputSize() calls colliding on the shared
detector — one request’s 640×640 image being processed with another
request’s 40×40 internal state.
After the fix, throughput scales cleanly with concurrency up to the
10-core ceiling (peaking at concurrency≈16), then latency grows gracefully
under further load instead of erroring — p50 was 987ms at concurrency=32,
not a timeout.