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:
@@ -160,6 +160,26 @@ fn main() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Say it here rather than letting the pass return an empty result and
|
||||
// print "done: 0 image(s)". That is the shape of report this whole change
|
||||
// exists to stop producing.
|
||||
if faces::FACE_TIER.edge() < dr_face::MIN_DETECT_EDGE {
|
||||
println!(
|
||||
"\n--run cannot index from the local store any more.\n\
|
||||
\n\
|
||||
Detection needs a proxy longer than {}px and the store's largest\n\
|
||||
tier is {}px, so every image here would be refused. The measurement\n\
|
||||
behind that floor is docs/faces.md §7c.\n\
|
||||
\n\
|
||||
Index from the app's Identity screen instead: that pass fetches the\n\
|
||||
embedded preview at full size, which is the resolution detection\n\
|
||||
needs.",
|
||||
dr_face::MIN_DETECT_EDGE - 1,
|
||||
faces::FACE_TIER.edge(),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
println!("\nindexing {} image(s)…", audit.ready);
|
||||
let rx = faces::spawn_store_face_sweep(
|
||||
catalog_path,
|
||||
@@ -189,13 +209,16 @@ fn main() {
|
||||
);
|
||||
}
|
||||
}
|
||||
FaceSweepMessage::Failed { images } => seen += images,
|
||||
FaceSweepMessage::Finished {
|
||||
images,
|
||||
faces,
|
||||
failed,
|
||||
too_small,
|
||||
} => {
|
||||
println!(
|
||||
"\ndone: {images} image(s), {faces} face(s), {failed} failed, in {:.0}s",
|
||||
"\ndone: {images} image(s), {faces} face(s), {failed} failed, \
|
||||
{too_small} with no proxy big enough, in {:.0}s",
|
||||
start.elapsed().as_secs_f64()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -50,11 +50,29 @@ pub enum FaceSweepMessage {
|
||||
Total(usize),
|
||||
/// One image finished, with the faces found in it.
|
||||
Indexed { image: ImageId, faces: usize },
|
||||
/// Images the pass gave up on, counted since the last message.
|
||||
///
|
||||
/// **Progress has to advance on these or it does not advance at all.**
|
||||
/// The count moved only on `Indexed`, so a run where every image failed
|
||||
/// sat at 0/169 from the first tick to the last and then reported
|
||||
/// success: the receiver was told the total, told nothing, and told the
|
||||
/// pass had ended. That is exactly what happened on the reference library
|
||||
/// for a day, and it is why the sweep was thought to be hanging when it
|
||||
/// was in fact finishing in fourteen seconds.
|
||||
///
|
||||
/// A batch rather than one message per image, because failures come back
|
||||
/// lane-sized and the interesting number is how many. The reason for each
|
||||
/// is logged where it happens.
|
||||
Failed { images: usize },
|
||||
/// The pass ended.
|
||||
Finished {
|
||||
images: usize,
|
||||
faces: usize,
|
||||
failed: usize,
|
||||
/// Refused for want of a big enough proxy rather than failed; see
|
||||
/// `docs/faces.md` §7c. Counted apart because a library of small
|
||||
/// scans is not a broken network and must not read as one.
|
||||
too_small: usize,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -351,6 +369,7 @@ pub fn spawn_store_face_sweep(
|
||||
images: 0,
|
||||
faces: 0,
|
||||
failed: 0,
|
||||
too_small: 0,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -508,6 +527,7 @@ pub fn spawn_store_face_sweep(
|
||||
images,
|
||||
faces: found,
|
||||
failed,
|
||||
too_small: 0,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -3359,6 +3359,22 @@ pub fn spawn_face_sweep(
|
||||
done += attempted;
|
||||
failed += lane_failed;
|
||||
offline |= lane_offline;
|
||||
// Before the indexed ones, so a screen watching this sees
|
||||
// the count move for work that produced nothing. Images
|
||||
// refused for size are *not* reported here: they are in
|
||||
// `indexed` below, having earned a marker, and counting
|
||||
// them twice would run the progress figure past its total.
|
||||
if lane_failed > 0
|
||||
&& tx
|
||||
.send(FaceSweepMessage::Failed {
|
||||
images: lane_failed,
|
||||
})
|
||||
.is_err()
|
||||
{
|
||||
log::info!("face sweep: cancelled after {images} image(s)");
|
||||
pool.release_all(&*backend).await;
|
||||
return;
|
||||
}
|
||||
for (image_id, faces, edge, keep) in indexed {
|
||||
// Before the detections, so a kill between the two
|
||||
// leaves a proxy with no faces recorded — which the
|
||||
|
||||
Reference in New Issue
Block a user