From 1d4c3348af557bbaf38ef6965d43170f3229b96d Mon Sep 17 00:00:00 2001 From: Duncan Tourolle Date: Fri, 28 Aug 2026 08:30:09 +0200 Subject: [PATCH] Let a 32-pixel face count, and move the blur floor with it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 64 source pixels was too strict: it threw away 70% of everything the detector finds, and plenty of what it took were faces a person could name. Lowering it is not a one-line change, because the two floors are coupled. A face under 112 pixels is *upsampled* to reach the embedder and upsampling invents no edges, so a small face scores low on sharpness however crisp the original was. Re-measured over the reference library with `face_index --quality`: min crop min sharp size cut blur cut kept 32 0.000 44% 0% 56% 32 0.002 44% 3% 53% 32 0.005 44% 8% 48% 32 0.010 44% 16% 40% 32 0.020 44% 27% 30% 64 0.020 70% 7% 23% Holding the blur floor at 0.020 while dropping the size floor to 32 would have rejected a further 27% — for being small rather than for being blurred — and kept only 30%, barely more than the 23% the strict pair kept. Most of the point of lowering the size floor would have gone straight back out through the other gate. 0.005 removes 8% of what the size floor leaves, which is the same job 0.020 was doing at 64 (7%): the large-but-soft face this gate exists for. Together they now keep 48% of what the detector finds, against 23% before. The box pre-filter follows down to 24, staying below what the real floor accepts so it cannot reject a face that would have cleared 32. Co-Authored-By: Claude Opus 5 (1M context) --- core/dr-face/src/detect.rs | 32 ++++++++++++++++++++++++++------ ui/dr-ui/examples/face_index.rs | 11 ++++++----- 2 files changed, 32 insertions(+), 11 deletions(-) diff --git a/core/dr-face/src/detect.rs b/core/dr-face/src/detect.rs index e1d9881..e6727a9 100644 --- a/core/dr-face/src/detect.rs +++ b/core/dr-face/src/detect.rs @@ -46,13 +46,13 @@ pub struct DetectOptions { /// it is measured on the aligned crop rather than the box. This one exists /// only to throw away the obviously hopeless before paying for a warp, so /// it is deliberately set *below* what the real floor will accept: the - /// aligned crop spans roughly 1.3x the box's shorter edge, so 48 here - /// cannot reject a face that would have cleared 64 there. + /// aligned crop spans roughly 1.3x the box's shorter edge, so 24 here + /// cannot reject a face that would have cleared 32 there. pub min_face_px: f32, /// Smallest face the embedder may be given, in **source pixels across the /// aligned crop** — `crop_px` in the catalog. /// - /// The honest statement of "a face must be at least 64x64", because this is + /// The honest statement of "a face must be at least 32x32", because this is /// the number of real pixels behind the 112x112 the model actually sees. /// The box's own size is not that: the ArcFace template reaches past the /// box for forehead and chin, so a 64-pixel box and a 64-pixel crop are @@ -74,6 +74,26 @@ pub struct DetectOptions { /// and a caller cannot enable one gate while forgetting the other. /// /// Zero disables it, which is what a measurement run wants. + /// + /// # It has to move with the size floor + /// + /// The two are coupled, because an upsampled face scores low here whatever + /// its original sharpness. Measured over the reference library, with the + /// size floor at 32 source pixels: + /// + /// | min sharpness | of what the size floor left, this removes | + /// |---|---| + /// | 0.002 | 3% | + /// | 0.005 | 8% | + /// | 0.010 | 16% | + /// | 0.020 | 27% | + /// + /// At a 64-pixel floor, 0.020 removed 7% — the same *kind* of face, the + /// large-but-soft one this gate exists for. Holding 0.020 while dropping + /// the size floor to 32 would have thrown away a quarter of the newly + /// admitted faces for being small rather than for being blurred, undoing + /// most of the point of lowering it. 0.005 removes 8% at 32, which is the + /// same job. pub min_sharpness: f32, } @@ -82,9 +102,9 @@ impl Default for DetectOptions { Self { confidence: 0.5, nms_iou: 0.4, - min_face_px: 48.0, - min_source_px: 64.0, - min_sharpness: 0.020, + min_face_px: 24.0, + min_source_px: 32.0, + min_sharpness: 0.005, } } } diff --git a/ui/dr-ui/examples/face_index.rs b/ui/dr-ui/examples/face_index.rs index 1e9ac70..7c1f4a9 100644 --- a/ui/dr-ui/examples/face_index.rs +++ b/ui/dr-ui/examples/face_index.rs @@ -464,13 +464,14 @@ fn report_quality( println!("{}", "-".repeat(52)); for (min_px, min_sharp) in [ (0.0_f32, 0.0_f32), - (64.0, 0.0), - (0.0, 0.010), - (64.0, 0.005), + (32.0, 0.0), + (32.0, 0.002), + (32.0, 0.005), + (32.0, 0.010), + (32.0, 0.020), + (48.0, 0.005), (64.0, 0.010), (64.0, 0.020), - (80.0, 0.010), - (96.0, 0.010), ] { let by_size = found.iter().filter(|f| f.0 < min_px).count(); let by_blur = found