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:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user