facedetect — buffalo_l face pipeline (face-detect.cpp port)¶
Face detection, recognition, attribute analysis, anti-spoofing, and dense landmarks from a single self-contained GGUF — a pure-Zig port of mudler/face-detect.cpp running the insightface buffalo_l pack on Fucina's CPU runtime:
- SCRFD det_10g detector (boxes + 5-point landmarks),
- ArcFace IResNet-50 recognizer (512-d embeddings, verification),
- GenderAge MobileNet-0.25 head,
- MiniFASNet ×2 anti-spoof ensemble,
- 2d106det / 1k3d68 dense landmarks (separate GGUF).
Outputs match the reference CLI: detect/analyze JSON is byte-identical,
embeddings agree at cosine ≥ 0.999999, anti-spoof probabilities are exact.
Weights¶
Not bundled. Fetch the GGUFs from Hugging Face
mudler/face-detect-gguf
and place (or symlink) them under models/:
buffalo_l.gguf— detector + recognizer + genderage + anti-spoof,landmarks-2d106-1k3d68.gguf— the dense-landmark heads.
hf download mudler/face-detect-gguf buffalo_l.gguf --local-dir models
hf download mudler/face-detect-gguf landmarks-2d106-1k3d68.gguf --local-dir models
(hf comes from pip install -U huggingface_hub — see
docs/RUNNING-MODELS.md.)
Run commands from the repo root; the CLI examples below and the test suite
both resolve models/ relative to it. The model-dependent parity tests
skip when the GGUFs are absent — a green zig build test without them has
not exercised the parity gates.
The insightface model weights carry their own (non-commercial) license
terms — see docs/THIRD-PARTY-NOTICES.md.
Usage¶
zig build facedetect -Doptimize=ReleaseFast -- info models/buffalo_l.gguf
# detect all faces -> reference-format JSON (boxes, scores, 5-point landmarks)
zig build facedetect -Doptimize=ReleaseFast -- detect --model models/buffalo_l.gguf --input face.png
# 512-d ArcFace embedding of the largest face (detect -> align -> embed);
# plain %.6f floats by default, --json for {"dim":..,"embedding":[..]}
zig build facedetect -Doptimize=ReleaseFast -- embed --model models/buffalo_l.gguf --input face.png --json
# 1:1 verification of two images (cosine distance; default threshold 0.35).
# --anti-spoof adds the MiniFASNet liveness veto: after a positive match, a
# face judged fake (or no face found) in either image flips verified to false.
zig build facedetect -Doptimize=ReleaseFast -- verify --model models/buffalo_l.gguf --a a.png --b b.png [--threshold T] [--anti-spoof]
# gender + age of the largest face
zig build facedetect -Doptimize=ReleaseFast -- analyze --model models/buffalo_l.gguf --input face.png
# per-mode CPU benchmark (model load + image decode outside the timed loop,
# one untimed warmup pass, arithmetic mean over N)
zig build facedetect -Doptimize=ReleaseFast -- bench --model models/buffalo_l.gguf --input face.png --mode pipeline|recognizer|detect|analyze --n 20
No photos needed for a first run — goldens/ ships the reference
fixtures' decoded pixels (align-src-{a,b,c}.bin) as FDR1, which the CLI
reads directly:
zig build facedetect -Doptimize=ReleaseFast -- detect --model models/buffalo_l.gguf --input examples/facedetect/goldens/align-src-a.bin
zig build facedetect -Doptimize=ReleaseFast -- verify --model models/buffalo_l.gguf --a examples/facedetect/goldens/align-src-a.bin --b examples/facedetect/goldens/align-src-b.bin
Notes:
- Inputs are PNG (8-bit, non-interlaced, grayscale/RGB/RGBA — palette
PNGs are not decoded) or FDR1 (a raw-pixel dump format, magic
FDR1; the goldens use it to pin reference-decoded pixels). JPEG is not decoded — convert first (sips -s format png face.jpg --out face.png). --threads Ncaps the worker team (default:min(cores, 8), the same default as the reference CLI).FUCINA_MAX_THREADSdoes the same via env.bench --mode recognizerexpects a pre-aligned 112×112 crop as input (e.g.examples/facedetect/goldens/crop-a.bin), mirroring the reference's protocol; the other modes take a full source image.- The dense-landmark nets (2d106det / 1k3d68) are exercised by the parity
gates under
zig build test(GGUF graph replay vsgoldens/lm-*crops and points; skipped whenmodels/landmarks-2d106-1k3d68.ggufis absent). ThelandmarksCLI subcommand is not wired end-to-end and prints a not-implemented notice.
Structure¶
recognizer.zig / scrfd.zig / genderage.zig hand-map their nets onto the
public tagged Tensor facade (channel-last [h,w,c]; conv2d, pool2d, prelu,
channelAffine, upsample). Weights load once into Model structs (GGUF
dequant + layout repack + BatchNorm folding happen at load; forwards are pure
compute). The two interpreter-driven nets (anti-spoof, landmarks) replay a
GGUF-embedded node list through graph.zig — an app-level compiled
dispatcher over ExecContext ops, not a general ONNX runtime. Decision-
critical control paths (cv2-exact letterbox, umeyama align, NMS decode) are
verbatim scalar ports.
Tests run under zig build test (a facedetect test root); the parity
gates compare against goldens/ (see goldens/README.md for what each
golden pins and how to regenerate them from the pinned reference).
FUCINA_TEST_VERBOSE=1 prints the measured per-case margins (cosines,
pixel errors, real-probs) on passing runs; by default passing tests are
silent so zig build test stderr stays clean.
Measured (ms/image, matched 8 threads, n=20)¶
| mode | Fucina | reference (same machine, production config) |
|---|---|---|
| M1 Max recognizer | ~35 | ~765 |
| M1 Max detect | ~84 | ~113 |
| M1 Max pipeline (detect+align+embed) | ~116 | ~872 |
| i9-13950HX recognizer | ~49 | ~56–61 |
| i9-13950HX detect | ~70 | ~53–59 |
| i9-13950HX pipeline | ~116–121 | ~115–123 |
The reference's fast conv kernels are x86-only (scalar fallback on ARM),
which is why the gap differs so much by platform; see the note in
goldens/README.md before quoting numbers.
2026-07-10, prepared Winograd weights (load-time transform planes instead of per-call, i9, same protocol): recognizer 54.4 → 50.4 ms (−7.2%), detect 72.5 → 71.6 ms (−1.2%) — matching the transform's predicted share of winograd byte traffic (~40% recognizer, ~4% detect). The remaining x86 detect gap is the fused tile-blocked winograd lever, not weight preparation.