AR-003 — max_faces defaults to 0, meaning no cap. A fixed cap discards the SMALLEST faces first, which are exactly the background cast X-Ray still credits with scene membership, so the pipeline was systematically losing the people it is supposed to find in crowded scenes. This is only safe now that AR-004 landed. Previously an uncapped frame would have pushed more work into channels that dropped on overflow, trading a visible cap for silent loss. With backpressure the producer slows instead, so per-frame cost is contained rather than discarded. The matcher's kMaxFaces used to throw above 32, which made it an accidental second cap. It sizes the similarity engine's preallocated buffer, so it bounds memory rather than face count — the frame is now scored in batches of that size. Memory stays bounded; faces do not. Largest-first ordering is kept even without the cap, and the comment now says why: the Hungarian solver tie-breaks on index order, so that ordering is load-bearing for the replay determinism test rather than a leftover of the cap. Verified end to end on a real clip: identical output to the capped run (385 frames, 693 faces), which is expected since that footage peaks at 4 faces per frame — the point is the absence of a regression. The committed fixtures remain byte-identical and valid. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> TRACES: AR-003 | SR-002
62 lines
2.4 KiB
C++
62 lines
2.4 KiB
C++
#pragma once
|
|
/// TRACES: AR-001 | SR-002
|
|
#include "config.hpp"
|
|
#include "inference/face_detector.hpp"
|
|
|
|
#include <algorithm>
|
|
#include <memory>
|
|
#include <string>
|
|
|
|
// ── 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<int>(faces.size()) > max_faces_)
|
|
faces.resize(max_faces_);
|
|
|
|
return {std::move(f), std::move(faces)};
|
|
}
|
|
|
|
private:
|
|
std::unique_ptr<IFaceDetector> detector_;
|
|
int max_faces_{10};
|
|
float min_face_px_{40.f};
|
|
};
|