Name segmented people from the faces already recognised in them

The segmenter knows it found a person; the face index knows which person.
Joining them turns "person" in the mask list into "Anna", which is the
difference between a vocabulary of eighty COCO classes and one that
includes the user's family. Selecting a subject in a group photograph
stops being a guessing game between three identical rows.

Containment, not IoU. A face is a small part of the person it belongs to,
so a correct pairing has an IoU near zero and anything IoU-based would
reject every true match.

Confirmed names only. A suggestion is the system's guess, and printing a
guessed name onto a mask region would launder it into a fact.

Writing the tests corrected the design once: a tight head-and-shoulders
portrait, where the face fills most of the person box, is the case where
naming is most certain, not least. An earlier guard rejected exactly that
and has been removed, with the reasoning left as a test because it is
easy to get backwards a second time.

The names hang on the develop session, set when the image opens because
that is the one moment the catalog and the image id are both in reach.
Every segmentation run afterwards picks them up for free, and a library
with no face indexing behaves exactly as it did before.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-26 23:10:46 +02:00
co-authored by Claude Opus 5
parent d562ceaaf4
commit b55812812a
7 changed files with 590 additions and 22 deletions
+32 -1
View File
@@ -1685,6 +1685,10 @@ pub fn run(paths: Vec<PathBuf>) -> Result<()> {
let redraw = redraw.clone();
let rows = rows.clone();
let gpu = gpu.clone();
// Cloned for the timer closure: the outer callback is an `Fn` and
// may run again for the next photograph.
let library = library.clone();
let path = path.clone();
let sidecar_rx = Rc::new(sidecar_rx);
let timer = Rc::new(slint::Timer::default());
let held = timer.clone();
@@ -1726,8 +1730,35 @@ pub fn run(paths: Vec<PathBuf>) -> Result<()> {
w.set_exposure(describe_exposure(&l.meta).into());
w.set_dimensions(format!("{} × {}", l.width, l.height).into());
match l.session {
Some(s) => {
Some(mut s) => {
w.set_adjust_enabled(true);
// TRACES: FR-CULL-10
// Who is in this photograph, so a
// segmentation run can say "Anna" where
// the model can only say "person". Read
// here because this is the one moment the
// catalog and the image id are both in
// reach; every run afterwards gets them
// for free.
if let Some(id) = library.image_id_for_path(&path) {
let cat = library.catalog();
let borrowed = cat.borrow();
if let Some(c) = borrowed.as_ref() {
match crate::identity::named_boxes_normalised(c, id) {
Ok(n) if !n.is_empty() => {
log::info!(
"{} known face(s) in this photograph",
n.len()
);
s.set_face_names(n);
}
Ok(_) => {}
Err(e) => {
log::debug!("reading known faces: {e}")
}
}
}
}
*session.borrow_mut() = Some(s);
// TRACES: FR-CAT-8
// The stored edit, if it has landed. It