diff --git a/core/dr-face/src/detect.rs b/core/dr-face/src/detect.rs index e6727a9..787ba67 100644 --- a/core/dr-face/src/detect.rs +++ b/core/dr-face/src/detect.rs @@ -14,7 +14,7 @@ use ndarray::Array4; -use crate::{install_backend, FaceError}; +use crate::{install_backend, FaceError, MIN_DETECT_EDGE}; /// The graph's input edge, in pixels. See the module note: not configurable. pub const INPUT_EDGE: usize = 640; @@ -213,6 +213,17 @@ impl Detector { if width == 0 || height == 0 { return Ok(Vec::new()); } + // Refused, not merely unproductive. Returning `Ok(vec![])` here would + // let the caller write a run marker meaning "looked, found nothing", + // which is the state that makes the image permanently invisible to + // every later pass. See [`MIN_DETECT_EDGE`]. + let edge = width.max(height) as u32; + if edge < MIN_DETECT_EDGE { + return Err(FaceError::ProxyTooSmall { + edge, + needed: MIN_DETECT_EDGE, + }); + } if rgb.len() != width * height * 3 { return Err(FaceError::ImageShape { expected: width * height * 3, diff --git a/core/dr-face/src/lib.rs b/core/dr-face/src/lib.rs index 96a72e8..fae958d 100644 --- a/core/dr-face/src/lib.rs +++ b/core/dr-face/src/lib.rs @@ -43,6 +43,31 @@ pub mod embedding; pub mod naming; pub mod neighbours; +/// Smallest proxy long edge detection will accept, in pixels. +/// +/// # Why this exists, and why it is 1025 rather than 1024 +/// +/// A proxy is letterboxed into the detector's 640px input whatever size it +/// arrives at, so a small one does not fail — it silently finds almost +/// nothing, and the +/// caller records a run marker saying the image was examined. That marker is +/// the damage: it is indistinguishable from "examined properly, no faces +/// here", so the photograph is never looked at again. +/// +/// The number is not a guess. Measured over a 23,500 image library, detection +/// against a 1024-edge proxy returned **0.078 faces per image**; the same +/// files — DNGs, 21 MB against 23 MB, so the same photographs in every respect +/// that matters — returned **1.82** against a proxy of 2048 or better. A +/// twenty-three fold difference, on identical source material and identical +/// weights. Ninety percent of the 1024-edge runs found nothing at all. +/// +/// 1024 is exactly `dr_thumbs::ThumbSize::Large`, the tier a proxy is stored +/// at, and it was the tier one of the two sweeps detected on. So the floor has +/// to *exclude* 1024 rather than admit it, and the constant is one past it: +/// `edge < MIN_DETECT_EDGE` is then the whole test, with no off-by-one left at +/// the call sites to get wrong. +pub const MIN_DETECT_EDGE: u32 = 1025; + pub use align::{warp, Aligned112, Similarity, ALIGNED_EDGE, ARCFACE_TEMPLATE}; pub use assign::{identity_shares, RIVAL_FLOOR, TOP_MATCHES}; pub use calibrate::{Calibration, Pairs, ReliabilityBand}; @@ -80,6 +105,14 @@ pub enum FaceError { #[error("image buffer is {got} floats, expected {expected} (RGB, three per pixel)")] ImageShape { expected: usize, got: usize }, + + /// The proxy is too small to detect on, so nothing was attempted. + /// + /// A distinct variant because the caller must treat it differently from a + /// failure: nothing is wrong with the image or the model, and the right + /// response is to find better pixels rather than to retry these. + #[error("proxy long edge is {edge}px, below the {needed}px detection floor")] + ProxyTooSmall { edge: u32, needed: u32 }, } /// Install tract as `ort`'s backend.