#pragma once /// TRACES: AR-001 | SR-002 #include "config.hpp" #include "inference/face_detector.hpp" #include #include #include #include // ── FaceDetectorFunc ────────────────────────────────────────────────────────── // KPN node: runs SCRFD-500MF to detect ALL faces in a frame. // // The inference backend (ONNX Runtime or raw TensorRT) is selected at compile // time; this node talks only to IFaceDetector via make_face_detector(cfg). struct FaceDetectorFunc { static constexpr std::string_view label() { return "face_detector"; } explicit FaceDetectorFunc(const Config& cfg) : detector_(make_face_detector(cfg)) , max_faces_(cfg.max_faces) , min_face_px_(cfg.min_face_px) {} /// TRACES: AR-002 | SR-002 // Drop faces below the minimum size — too small for reliable ArcFace // alignment, and below the resolution where identification still holds // (VR-013 measured the knee end to end). // // The minimum is expressed in ORIGINAL video resolution, which is what makes // it a property of the footage rather than of a throughput knob. When // dense_scale downscaled the frame the detector's boxes are in downscaled // space, and `bbox_upscale` is what maps them back; dividing the threshold by // it rather than multiplying every box keeps the comparison on the detector's // own numbers and the physical cutoff constant across scales. // // Strictly less-than: a face exactly at the minimum is admissible, which is // what a "minimum of 40x40" means. static void drop_undersized(std::vector& faces, float min_face_px, float bbox_upscale) { const float min_px = (bbox_upscale > 0.f) ? min_face_px / bbox_upscale : min_face_px; faces.erase( std::remove_if(faces.begin(), faces.end(), [&](const DetectedFace& d) { return d.bbox.width < min_px || d.bbox.height < min_px; }), faces.end()); } SceneFrame operator()(Frame f) { if (f.eof) return {std::move(f), {}}; auto faces = detector_->detect(f.image); drop_undersized(faces, min_face_px_, f.bbox_upscale); // Sort largest-first so max_faces_ keeps the most informative detections std::sort(faces.begin(), faces.end(), [](const DetectedFace& a, const DetectedFace& b) { return a.bbox.area() > b.bbox.area(); }); // TRACES: AR-003 | SR-002 // Largest-first ordering is kept regardless: it is load-bearing for // deterministic association, since the Hungarian solver tie-breaks on // index order (see the replay determinism test). if (max_faces_ > 0 && static_cast(faces.size()) > max_faces_) faces.resize(max_faces_); return {std::move(f), std::move(faces)}; } private: std::unique_ptr detector_; int max_faces_{10}; float min_face_px_{40.f}; };