Keep each face's quality, and never compare against a poor one

The embedder's raw output has a length, and the length is a reading of
how recognisable the crop was: a blur, an occlusion or a hard profile
comes out short. Normalising threw it away. A short vector sits near
the middle of the sphere and matches a little of everyone, which is how
one bad crop bridges two people in a grouping pass.

So the length is kept — the store now holds the raw vector, re-normalised
on load, with the length beside it as `faces.quality` — and a face under
MIN_GALLERY_QUALITY (14) is a probe: measured against the gallery and
placed where it fits, but never what another face is measured against.
Two probes are never paired, and a probe is nobody's evidence for a
confidence. The People screen shows the number as "Quality 17.3", dimmed
below the floor.

Faces indexed before this stored unit vectors and have no reading; they
are admitted to the gallery, and schema V14 forgets the run marker of
every image holding one so the next indexing pass measures them. A
peer's unmeasured shard faces are not adopted, or a sync would write
that marker back.
This commit is contained in:
2026-09-11 21:50:12 +02:00
parent a87139b838
commit 8b3abdb787
22 changed files with 1070 additions and 149 deletions
+17 -12
View File
@@ -306,7 +306,7 @@ pub fn index_proxy(
continue;
}
let embedding = embedder.embed(&aligned)?;
let embedded = embedder.embed(&aligned)?;
out.push(DetectedFace {
x: d.bbox.0 / long_edge,
@@ -315,8 +315,9 @@ pub fn index_proxy(
h: d.height() / long_edge,
landmarks: normalise_landmarks(&d.landmarks, long_edge),
confidence: d.confidence,
embedding: embedding.to_f16_bytes(),
embedding: embedded.to_f16_bytes(),
crop_px: aligned.source_px(),
quality: Some(embedded.quality),
model_id: embedder.model().as_str().to_string(),
// Cut here, while the buffer is still in hand. This is the only
// moment in the whole pipeline where the pixels are free.
@@ -419,7 +420,7 @@ pub fn index_native(
continue;
}
let embedding = embedder.embed(&aligned)?;
let embedded = embedder.embed(&aligned)?;
let (bx, by) = (d.bbox.0 * sx, d.bbox.1 * sy);
let (bw, bh) = (d.width() * sx, d.height() * sy);
@@ -430,8 +431,9 @@ pub fn index_native(
h: bh / long_edge,
landmarks: normalise_landmarks(&landmarks, long_edge),
confidence: d.confidence,
embedding: embedding.to_f16_bytes(),
embedding: embedded.to_f16_bytes(),
crop_px: aligned.source_px(),
quality: Some(embedded.quality),
model_id: embedder.model().as_str().to_string(),
crop: cut_crop_native(native, width, height, (bx, by, bw, bh)).unwrap_or_default(),
});
@@ -837,19 +839,20 @@ impl Population {
let model = ModelId::new(model_id.to_string());
let mut candidates = Vec::with_capacity(stored.len());
let mut ids = Vec::with_capacity(stored.len());
for (face_id, image_id, blob, crop_px) in stored {
let Some(emb) = dr_face::Embedding::from_f16_bytes(model.clone(), &blob) else {
log::warn!("face {face_id:?} has a malformed embedding, skipped");
for f in stored {
let Some(emb) = dr_face::Embedding::from_f16_bytes(model.clone(), &f.embedding) else {
log::warn!("face {:?} has a malformed embedding, skipped", f.face);
continue;
};
candidates.push(dr_face::Candidate {
face: face_id.0,
image: image_id.0,
face: f.face.0,
image: f.image.0,
embedding: emb.v.to_vec(),
crop_px,
confirmed_person: anchors.get(&face_id).map(|p| p.0),
crop_px: f.crop_px,
quality: f.quality,
confirmed_person: anchors.get(&f.face).map(|p| p.0),
});
ids.push(face_id);
ids.push(f.face);
}
Ok(Self {
@@ -1516,6 +1519,7 @@ mod tests {
landmarks: [(0.0, 0.0); 5],
confidence: 0.9,
crop_px: 120.0,
quality: None,
model_id: "w600k_mbf".into(),
person: None,
probability: 0.0,
@@ -1633,6 +1637,7 @@ mod tests {
confidence: 0.9,
embedding: embedding(identity, cosine),
crop_px: 150.0,
quality: None,
model_id: TEST_MODEL.to_string(),
crop: Vec::new(),
};
+73 -5
View File
@@ -97,6 +97,10 @@ pub struct FaceCell {
/// Source pixels across the aligned crop. Small faces embed worse, and the
/// user deserves to know which of a bad suggestion's causes is in play.
pub crop_px: f32,
/// The model's own reading of how recognisable the crop was — the length
/// of its raw embedding (`dr_face::MIN_GALLERY_QUALITY`). `None` for a
/// face indexed before it was kept.
pub quality: Option<f32>,
}
impl FaceCell {
@@ -115,6 +119,31 @@ impl FaceCell {
format!("{:.0}% likely", self.probability * 100.0)
}
}
/// The quality as text — "Quality 17.3", or "Quality —" where it was never
/// measured.
///
/// Called *quality* and not *norm* on the screen, because that is what the
/// number is used as and what the user can act on: a low one is the model
/// saying it could not make the face out, and the fix is a better
/// photograph. One decimal, because the gate sits at a whole number and
/// the faces worth a second look are the ones just either side of it.
pub fn quality_label(&self) -> String {
match self.quality {
Some(q) => format!("Quality {q:.1}"),
None => "Quality —".into(),
}
}
/// Whether this face is good enough to be compared *against*.
///
/// What the screen dims the quality for: a face below the floor is still
/// somebody and still placed, but it vouches for no one else, and a user
/// wondering why a person's group did not gather the rest of them should
/// be able to see that none of its members can.
pub fn in_gallery(&self) -> bool {
dr_face::in_gallery(self.quality)
}
}
/// Put a grouping preview into words.
@@ -254,6 +283,7 @@ pub fn load_faces(
confirmed: f.confirmed,
probability: f.probability,
crop_px: f.crop_px,
quality: f.quality,
});
}
@@ -615,23 +645,24 @@ pub fn preview_split(
let model = dr_face::ModelId::new(model_id.to_string());
let stored: std::collections::HashMap<_, _> = faces::embeddings(conn, model_id)?
.into_iter()
.map(|(id, image, blob, crop_px)| (id, (image, blob, crop_px)))
.map(|e| (e.face, e))
.collect();
let mut candidates = Vec::with_capacity(cells.len());
let mut order = Vec::with_capacity(cells.len());
for c in &cells {
let Some((image, blob, crop_px)) = stored.get(&c.face) else {
let Some(e) = stored.get(&c.face) else {
continue;
};
let Some(emb) = dr_face::Embedding::from_f16_bytes(model.clone(), blob) else {
let Some(emb) = dr_face::Embedding::from_f16_bytes(model.clone(), &e.embedding) else {
continue;
};
candidates.push(dr_face::Candidate {
face: c.face.0,
image: image.0,
image: e.image.0,
embedding: emb.v.to_vec(),
crop_px: *crop_px,
crop_px: e.crop_px,
quality: e.quality,
confirmed_person: None,
});
order.push(c.clone());
@@ -755,6 +786,7 @@ mod tests {
confidence: 0.9,
embedding: vec![seed; 1024],
crop_px: 180.0,
quality: Some(f32::from(seed) + 10.0),
model_id: "w600k_mbf".into(),
crop: Vec::new(),
}
@@ -802,6 +834,7 @@ mod tests {
confirmed: false,
probability: 0.87,
crop_px: 120.0,
quality: Some(17.26),
};
assert_eq!(cell.confidence_label(), "87% likely");
@@ -812,6 +845,41 @@ mod tests {
assert_eq!(confirmed.confidence_label(), "Confirmed");
}
/// The number is the embedding's length, and the screen calls it what it
/// is used as. A face from before it was kept says so rather than showing
/// a zero that would read as the worst face in the library.
#[test]
fn the_quality_is_labelled_as_such_and_dimmed_below_the_floor() {
let cell = FaceCell {
face: FaceId(1),
image: ImageId(1),
crop: None,
confirmed: false,
probability: 0.5,
crop_px: 120.0,
quality: Some(17.26),
};
assert_eq!(cell.quality_label(), "Quality 17.3");
assert!(cell.in_gallery());
let poor = FaceCell {
quality: Some(9.4),
..cell.clone()
};
assert_eq!(poor.quality_label(), "Quality 9.4");
assert!(!poor.in_gallery());
let unmeasured = FaceCell {
quality: None,
..cell
};
assert_eq!(unmeasured.quality_label(), "Quality —");
assert!(
unmeasured.in_gallery(),
"an unmeasured face is not a poor one"
);
}
#[test]
fn the_people_rail_reports_the_unassigned_pool() {
let c = catalog();
+2
View File
@@ -478,6 +478,8 @@ fn push_faces(window: &AppWindow, ctl: &IdentityController, cells: &[FaceCell])
confirmed: c.confirmed,
confidence: c.confidence_label().into(),
crop_px: c.crop_px as i32,
quality: c.quality_label().into(),
in_gallery: c.in_gallery(),
picked: picked.contains(&c.face),
})
.collect();
+3
View File
@@ -5937,6 +5937,7 @@ mod tests {
confidence: 0.9,
embedding: vec![0u8; 1024],
crop_px: 120.0,
quality: None,
crop: Vec::new(),
model_id: "w600k_mbf".into(),
};
@@ -5986,6 +5987,7 @@ mod tests {
confidence: 0.9,
embedding: vec![0u8; 1024],
crop_px: 120.0,
quality: None,
crop: Vec::new(),
model_id: "w600k_mbf".into(),
};
@@ -7012,6 +7014,7 @@ mod tests {
confidence: 0.9,
embedding: vec![0u8; 1024],
crop_px: 120.0,
quality: None,
crop: Vec::new(),
model_id: "w600k_mbf".into(),
};