From a1e361e35a8dad90b729a36cfe92693955fedf79 Mon Sep 17 00:00:00 2001 From: Duncan Tourolle Date: Sun, 30 Aug 2026 19:02:58 +0200 Subject: [PATCH] Measure the native path against the one it replaces MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Everything argued for this change so far was read out of a catalog after the fact: crop_px across 18,671 faces the old code had already stored. That is evidence about what the previous implementation did. It is not evidence that the new one does better, and the difference matters because a landmark left in the detector's coordinates, or a box filter with an off-by-one in its source span, would both produce faces that look entirely plausible until somebody counted the pixels behind them. So examples/face_native.rs renders one file and indexes it twice, native and from a 1024 proxy, changing nothing else. Fourteen originals from the reference library, 5472x3648 CR2 and DNG: native 9 faces, mean crop 287px 1024 proxy 5 faces, mean crop 75px Crops 3.8x larger, and across the line that decides whether the crop is photographed or interpolated: 75px is below ALIGNED_EDGE, so the proxy path was upsampling into the embedder on average where this one downsamples into it. Fourteen images and nine faces is enough to show a direction and to catch a wrong scaling; §7b says so rather than quoting the ratio as a library-wide figure. It also corrects something §7b asserted two commits ago. I wrote that detector input resolution cannot affect recall, because §4.1 letterboxes everything to 640. Native found nine faces to the proxy's five, including four on files where the proxy found none, so it plainly can. The two paths differ in their resampling as well as their size, and this experiment does not separate those, so §7b now records the result as evidence for the double-resampling hypothesis rather than as its proof. M4 still owns settling it. The audit-summary test went stale when the ready/to-fetch split was collapsed and is updated to assert the single number, including that the old wording is gone. Co-Authored-By: Claude Opus 5 (1M context) --- docs/faces.md | 27 +++++++++++++++++++++++++++ ui/dr-ui/src/faces.rs | 14 ++++++++------ 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/docs/faces.md b/docs/faces.md index 0a99b95..11a6ec0 100644 --- a/docs/faces.md +++ b/docs/faces.md @@ -556,6 +556,26 @@ as 34 — a 3.3× enlargement — against a mean of 178. An upsampled crop does produces a confident 512-d embedding describing detail that was interpolated rather than photographed, and the damage appears three stages later as clusters that will not separate. +### Measured again, against the implementation + +The figures above are read out of a catalog after the fact, so they describe what the old code did +rather than what the new code does. `examples/face_native.rs` renders one file and indexes it both +ways, so the difference can be attributed to the resolution and nothing else. Fourteen originals +from the reference library — 5472×3648 Canon CR2 and DNG — each rendered once and indexed twice: + +| Path | Faces found | Mean `crop_px` | +|---|---|---| +| Native (this specification) | 9 | **287** | +| Everything from a 1024 proxy | 5 | **75** | + +Crops 3.8× larger, and on the right side of the line that matters: 75 px is *below* [`ALIGNED_EDGE`] +so the proxy path was upsampling into the embedder on average, where the native path downsamples +into it. + +**Sample of fourteen images and nine faces.** Enough to show the direction and to catch a wrong +landmark scaling, which is what it was written for; not enough to quote a ratio as the library-wide +figure. §12's M4 is still where the recall curve gets established. + A second effect, recorded here because it was measured and because the mechanism is **not** established. Grouping the same runs by `face_index.source_edge` — the buffer detection ran against — gives 0.078 faces per image at 1024 or below, against 1.82 at 2048 or better. Controlled @@ -567,6 +587,13 @@ itself a downscale of a larger preview, so the detector sees a twice-resampled i larger buffer is resampled once — but that is a hypothesis, not a finding, and §12's M4 is where it should be settled. **The crop measurement above stands on its own and does not depend on it.** +The A/B is evidence for that hypothesis without settling it. Native found nine faces where the +1024 path found five, on four files where the proxy path found none at all — so detector input +*does* affect recall, which the letterbox says it should not. The two paths differ in their +resampling as well as their size (one box filter and one letterbox against a downscale and a +letterbox), and this experiment does not separate those. Isolating it means holding the chain fixed +and varying only the buffer, which is M4's job. + --- ## 8. Calibration — cosine to probability diff --git a/ui/dr-ui/src/faces.rs b/ui/dr-ui/src/faces.rs index 79f66be..5182445 100644 --- a/ui/dr-ui/src/faces.rs +++ b/ui/dr-ui/src/faces.rs @@ -1395,7 +1395,7 @@ mod tests { } #[test] - fn an_audit_summary_names_both_kinds_of_outstanding() { + fn an_audit_summary_counts_the_outstanding_as_one_number() { let a = IndexAudit { coverage: faces::Coverage { images: 100, @@ -1410,11 +1410,13 @@ mod tests { assert!(s.contains("60/100"), "{s}"); assert!(s.contains("60%"), "{s}"); assert!(!s.contains("60.0%"), "whole numbers above ten percent: {s}"); - assert!(s.contains("30 ready"), "{s}"); - // "to fetch", not "awaiting a proxy": the whole-library pass fetches - // these rather than being blocked by them, and the line must not send - // the user off to run the thumbnail sweep first. - assert!(s.contains("10 to fetch"), "{s}"); + // One number, not two. The split described a pass that could index + // whatever already had a proxy; detection now refuses that proxy's + // size (§7), so both halves cost the same fetch and "30 ready, 10 to + // fetch" would imply a distinction that decides nothing. + assert!(s.contains("40 to index"), "{s}"); + assert!(!s.contains("ready"), "the split should be gone: {s}"); + assert!(!s.contains("to fetch"), "the split should be gone: {s}"); } #[test]