Specify face indexing at native resolution, and say what the proxy cost

FR-CULL-8 said detection runs against the thumbnail or proxy tier and
never a full decode, and faces.md §5 said the aligned crop is sampled
from that same proxy. Both are wrong in the same place: they treat
detection and cropping as one resolution problem when they are two, with
opposite answers.

Detection does not care. §4.1 fixes the graph's input at 640x640 and
letterboxes whatever arrives, so a face filling 2% of the frame reaches
the model at 12px whether the buffer handed over is 1024px or 6000px.
Every pixel above the detector's own input is discarded before inference.

The crop cares about nothing else. §5's warp produces the fixed 112x112
ArcFace sees, so source resolution converts directly into whether those
112 pixels were photographed or interpolated. Reading crop_px across the
18,671 faces the proxy-tier implementation stored: 47.3% were upsampled
to reach the embedder, 314 of them by more than 2x, the smallest from 34
source pixels. An upsampled crop does not fail loudly -- it yields a
confident embedding of detail that was never there, and the damage
appears three stages later as clusters that will not separate.

So FR-CULL-8 now specifies four stages with the resolutions named
separately: render native through FR-EXP-9's pipeline, downscale for the
detector, map boxes and landmarks back to native, crop and align from
the native render. The affordability the old rule bought is met instead
by when the pass runs -- background, preempted, resumable -- and the
requirement says plainly what it now costs on a remote library: the
original rather than FR-NC-3's byte range, 412 GB across the reference
library's 19,107 images, so a whole-library pass is a transfer under
FR-NC-6 rather than something that may start on its own.

MIN_CROP_EDGE replaces the MIN_DETECT_EDGE this branch briefly had. Same
number, guarding the quantity that turned out to matter.

faces.md §7b records both measurements, and marks the second as
unexplained rather than dressing it as a finding. Grouped by the buffer
detection ran against, faces per image was 0.078 at 1024 or below and
1.82 at 2048 or better, controlled for file type and size. That gap is
real and reproducible and I cannot account for it, because the letterbox
above says detector input should not matter. M4 is where it gets
settled. The crop measurement does not depend on it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-30 19:41:20 +02:00
co-authored by Claude Opus 5
parent 144d2e4e84
commit e7b526c550
9 changed files with 259 additions and 49 deletions
+60 -9
View File
@@ -43,6 +43,14 @@ pub struct IdentityController {
/// Images visited so far in the current sweep, and the total it announced.
progress: std::cell::Cell<(usize, usize)>,
faces_found: std::cell::Cell<usize>,
/// Images the running sweep could not get through, and images it refused
/// for want of a big enough proxy.
///
/// On screen because a pass that fails everything used to be
/// indistinguishable from one that succeeded at everything: both ended
/// with a tidy "0 face(s) in 0 image(s)".
sweep_failed: std::cell::Cell<usize>,
sweep_too_small: std::cell::Cell<usize>,
/// Whether opening this screen left the library rather than develop.
///
/// Recorded so leaving puts the user back where they were. The screen is
@@ -1035,6 +1043,8 @@ pub fn wire<S, M, P>(
ctl.progress.set((0, 0));
ctl.faces_found.set(0);
ctl.sweep_failed.set(0);
ctl.sweep_too_small.set(0);
*ctl.activity.borrow_mut() =
Some(activity.begin(crate::activity::Kind::Index, "Indexing faces"));
*ctl.sweep.borrow_mut() = Some(crate::library::spawn_face_sweep(
@@ -1079,19 +1089,42 @@ pub fn wire<S, M, P>(
ctl_tick.progress.set((seen + 1, total));
ctl_tick.faces_found.set(ctl_tick.faces_found.get() + faces);
}
FaceSweepMessage::Finished { .. } => done = true,
FaceSweepMessage::Failed { images } => {
// Advances the same counter. Work that
// failed is still work the pass got
// through, and a bar that only moves on
// success reports a run of pure failure as
// no progress at all.
let (seen, total) = ctl_tick.progress.get();
ctl_tick.progress.set((seen + images, total));
ctl_tick
.sweep_failed
.set(ctl_tick.sweep_failed.get() + images);
}
FaceSweepMessage::Finished {
failed, too_small, ..
} => {
// The sweep's own tallies win over the
// running ones: a cancelled or offline
// pass can end without every batch having
// been reported.
ctl_tick.sweep_failed.set(failed);
ctl_tick.sweep_too_small.set(too_small);
done = true;
}
}
}
}
let (seen, total) = ctl_tick.progress.get();
w.set_identity_indexing_status(
format!(
"indexing {seen}/{total} — {} face(s) found",
ctl_tick.faces_found.get()
)
.into(),
let mut status = format!(
"indexing {seen}/{total} — {} face(s) found",
ctl_tick.faces_found.get()
);
if ctl_tick.sweep_failed.get() > 0 {
status.push_str(&format!(", {} failed", ctl_tick.sweep_failed.get()));
}
w.set_identity_indexing_status(status.into());
if let Some(a) = ctl_tick.activity.borrow().as_ref() {
a.progress(seen, total);
}
@@ -1099,10 +1132,28 @@ pub fn wire<S, M, P>(
if done {
*ctl_tick.sweep.borrow_mut() = None;
if let Some(a) = ctl_tick.activity.borrow_mut().take() {
a.finish(format!(
// Say what did not work. A pass that failed every
// image used to finish with the same sentence as
// one that succeeded at every image, which is how
// 169 consecutive failures went unnoticed for a
// day.
let mut msg = format!(
"{} face(s) in {seen} image(s)",
ctl_tick.faces_found.get()
));
);
if ctl_tick.sweep_failed.get() > 0 {
msg.push_str(&format!(
", {} could not be read",
ctl_tick.sweep_failed.get()
));
}
if ctl_tick.sweep_too_small.get() > 0 {
msg.push_str(&format!(
", {} with no proxy big enough",
ctl_tick.sweep_too_small.get()
));
}
a.finish(msg);
}
w.set_identity_indexing(false);
// Newly indexed faces belong to nobody until they are