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(),
};