This page exists because “trust the model card” turned out not to be enough — several assumptions here were wrong until checked against the actual weights. Everything below is either read directly from the ONNX graph or confirmed empirically against a manual test set, not copied from documentation.

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.
Requires OpenCV ≥ 4.8.0. The 2023mar version of this model fails at runtime (not build time) on older OpenCV with Layer with requested id=-1 not found in function 'getLayerData' — a documented upstream incompatibility (opencv_zoo#172). This is why the Docker image builds its own OpenCV instead of using Ubuntu’s apt package — see Deployment.
Degrades badly on raw phone-camera resolutions. Feeding YuNet a 2300px+ image directly (no resize) produced zero detections at the production score threshold, or garbage/mislocated boxes below it, during validation. InferenceEngine::detectFaces works around this by detecting on a 640px-longest-side copy and mapping results back to full-resolution coordinates — see Architecture → detectFaces.

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).
The general lesson: before trusting any new embedding model, run it against a black image, a white image, and two independent random-noise images. A healthy model should show real separation between all of these (see the buffalo_l numbers in the sanity-check table below). A model that scores everything ~0.9+ regardless of input content is either undertrained or broken — the noise check tells you which failure mode you’re looking at faster than staring at real photos does.

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:
This is one small manual test set, not a calibrated FAR/FRR curve. Validate COMPARE_THRESHOLD against a larger labeled dataset representative of your actual traffic before trusting it in a security-sensitive production path.

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:
None of these floors are calibrated against a labeled spoof/live dataset. They’re reasonable starting points, not the result of the same kind of empirical validation the similarity threshold below received. This endpoint will pass a well-lit, sharp, close-up printed photo — it is not a substitute for real presentation-attack detection (depth sensing, motion/challenge-response, or a certified liveness SDK) before relying on it in a security-sensitive production path. Treat it as a stopgap that gives calling systems a real (if weak) signal to build retry-lockout and audit logic against today, swappable for a stronger implementation later without changing the API contract.

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.
cv::FaceDetectorYN is not safe to share across threads the way Ort::Session is, despite an earlier version of this codebase asserting otherwise. InferenceEngine::detectFaces() calls setInputSize() — which mutates state on the detector object — directly before detect(). A single shared detector instance lets concurrent requests race on that state. Fixed by giving each worker thread its own detector instance (thread_local in threadLocalDetector()) — see Architecture → InferenceEngine.
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:
Two concurrent requests’ 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.
The general lesson: a comment asserting an object is “safe for concurrent use” is a claim, not a fact — verify it with an actual concurrent load test before trusting it, especially for third-party library objects with any per-call mutable state (like setInputSize() here). Sequential-request latency numbers, however carefully measured, cannot surface this class of bug.

Expected throughput

Extrapolating from the 10-core measurement above (CPU-bound inference, worker threads = core count by default):
This is extrapolation from one 10-core Apple Silicon Mac, not a measurement on real cloud hardware. Cloud x86_64 vCPUs — especially shared/burstable instance types — often perform worse per-core than this. Re-run the same load test against your actual target instance type before using these numbers for capacity planning. Horizontal scaling (more stateless replicas behind a load balancer) is the intended way to grow past a single instance’s ceiling.