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
+23 -15
View File
@@ -34,6 +34,10 @@ struct Known {
crop_px: f32,
}
/// What the catalog holds per face, decoded: photograph, vector, size,
/// quality.
type Decoded = (u64, Vec<f32>, f32, Option<f32>);
fn main() {
let args: Vec<String> = std::env::args().skip(1).collect();
let Some(path) = args.first() else {
@@ -59,10 +63,10 @@ fn main() {
let model = dr_face::ModelId::new(MODEL_ID.to_string());
let stored = faces::embeddings(conn, MODEL_ID).expect("embeddings");
let mut embedding_of = HashMap::new();
for (id, image, blob, crop_px) in stored {
if let Some(e) = dr_face::Embedding::from_f16_bytes(model.clone(), &blob) {
embedding_of.insert(id, (image.0, e.v.to_vec(), crop_px));
let mut embedding_of: HashMap<faces::FaceId, Decoded> = HashMap::new();
for f in stored {
if let Some(e) = dr_face::Embedding::from_f16_bytes(model.clone(), &f.embedding) {
embedding_of.insert(f.face, (f.image.0, e.v.to_vec(), f.crop_px, f.quality));
}
}
println!("faces with embeddings: {}", embedding_of.len());
@@ -82,7 +86,7 @@ fn main() {
if !f.confirmed {
continue;
}
if let Some((image, embedding, crop_px)) = embedding_of.get(&f.id) {
if let Some((image, embedding, crop_px, _)) = embedding_of.get(&f.id) {
mine.push(Known {
image: *image,
person: p.id,
@@ -288,19 +292,22 @@ fn band(label: &str, v: &[f32]) {
/// The whole library through the real clusterer, for the numbers it would
/// actually write.
fn full_library(
embedding_of: &HashMap<faces::FaceId, (u64, Vec<f32>, f32)>,
embedding_of: &HashMap<faces::FaceId, Decoded>,
confirmed: &HashMap<faces::FaceId, u64>,
cal: &dr_face::Calibration,
) {
let mut candidates: Vec<dr_face::Candidate> = embedding_of
.iter()
.map(|(id, (image, embedding, crop_px))| dr_face::Candidate {
face: id.0,
image: *image,
embedding: embedding.clone(),
crop_px: *crop_px,
confirmed_person: confirmed.get(id).copied(),
})
.map(
|(id, (image, embedding, crop_px, quality))| dr_face::Candidate {
face: id.0,
image: *image,
embedding: embedding.clone(),
crop_px: *crop_px,
quality: *quality,
confirmed_person: confirmed.get(id).copied(),
},
)
.collect();
candidates.sort_by_key(|c| c.face);
@@ -317,11 +324,13 @@ fn full_library(
.collect();
let crop_px: Vec<f32> = candidates.iter().map(|c| c.crop_px).collect();
let images: Vec<u64> = candidates.iter().map(|c| c.image).collect();
let gallery: Vec<bool> = candidates.iter().map(|c| c.in_gallery()).collect();
let view = dr_face::neighbours::Faces {
embeddings: &flat,
dim,
crop_px: &crop_px,
images: &images,
gallery: &gallery,
};
let t = std::time::Instant::now();
@@ -335,8 +344,7 @@ fn full_library(
let agglomerate = t.elapsed().as_secs_f64() - scan;
let t = std::time::Instant::now();
let _ =
dr_face::identity_shares(candidates.len(), &clusters, &evidence, dr_face::TOP_MATCHES);
let _ = dr_face::identity_shares(&gallery, &clusters, &evidence, dr_face::TOP_MATCHES);
println!(
" scan {scan:.2}s ({} evidence pairs) · agglomerate {agglomerate:.2}s · score {:.2}s",
evidence.len(),