#pragma once /// TRACES: AR-001 | SR-002 #include "config.hpp" #include "inference/face_detector.hpp" #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) {} SceneFrame operator()(Frame f) { if (f.eof) return {std::move(f), {}}; auto faces = detector_->detect(f.image); // Drop faces below minimum pixel size (too small for reliable ArcFace // alignment). Note: when dense_scale downscaled the frame, both the // detection coords and min_face_px are in downscaled space — so scale // the threshold down to match, keeping the physical size cutoff constant. const float min_px = (f.bbox_upscale != 1.f) ? min_face_px_ / f.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()); // 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}; };