//! TRACES: FR-CULL-10 | NFR-P9 //! Which of a person's faces stand for them in a grouping pass. //! //! # Why not all of them //! //! Every face the user has ruled on enters [`crate::cluster`] as an anchor, //! and the pass compares every face against every other //! ([`crate::neighbours`] is exhaustive by design). So a person with 750 //! confirmed faces costs 750 comparisons against each of the library's other //! faces, and the cost of naming a library well grows with how well it is //! named: a fully confirmed library of 25,000 faces spends almost the whole //! scan re-comparing faces whose identity is already settled against each //! other. //! //! Most of those comparisons say nothing new. A person's confirmed faces are //! heavily redundant — thirty frames from one afternoon are one point of //! view, not thirty — and a new face that matches one of them matches the //! others too. What a new face needs to be measured against is the person's //! *range*: the angles, ages and lights they have been photographed in, each //! represented once. //! //! # The choice: the most diverse of the good ones //! //! Two rules, in order. //! //! **Good enough to vouch.** Only faces whose raw embedding was at least //! [`MIN_REFERENCE_QUALITY`] long are eligible — a stricter floor than the //! gallery's ([`crate::embedding::MIN_GALLERY_QUALITY`]), because a reference //! is asked to speak *for* a person rather than merely be admitted to the //! comparison. A face whose length was never recorded is admitted, as it is //! everywhere else: a rule that cannot be checked admits rather than excludes. //! //! **As far apart as possible.** From the eligible pool, up to //! [`MAX_REFERENCES`] faces are chosen to maximise the volume they span — //! the determinant of their Gram matrix — greedily: start from the longest //! vector, and at each step add the face with the largest component //! orthogonal to everything chosen so far. That is Gram–Schmidt with a //! pivot, and the product of the squared residuals it picks *is* the //! determinant, so the greedy step is the exact greedy on the objective. //! The effect is that a near-duplicate of a chosen face has almost no //! residual and is passed over, while the one profile shot among two //! hundred frontal frames is taken early. //! //! What is not chosen still belongs to the person. Those faces keep their //! confirmations and are not touched by the pass; they are simply not //! compared, which is the whole saving. /// The most faces that stand for one person. /// /// A hundred is far more points of view than a person has. What it bounds /// is the cost: with every person at the cap, a scan against the named part /// of a library is `people × 100` comparisons per face rather than /// `confirmations`, and the two part company as soon as a library is used. pub const MAX_REFERENCES: usize = 100; /// The shortest raw embedding that may stand for a person. /// /// One above the gallery floor: a reference vouches for someone, and the /// margin keeps the faces that only just cleared the gallery — the ones /// nearest the middle of the sphere — out of the set that speaks for a /// person. pub const MIN_REFERENCE_QUALITY: f32 = 15.0; /// Whether a face of this quality may stand for a person. /// /// `None` is "never measured" and is admitted, as in /// [`crate::embedding::in_gallery`]. pub fn eligible(quality: Option) -> bool { quality.is_none_or(|q| q >= MIN_REFERENCE_QUALITY) } /// Choose which of one person's faces stand for them. /// /// `embeddings` and `quality` are one entry per face, the embeddings unit /// length and all of one dimension. Returns the indices chosen, in the order /// chosen — the first is the longest eligible vector, and each after it is /// the one furthest from the span of those before. Every eligible face is /// returned when there are `max` or fewer of them, so a person under the /// cap loses nothing. /// /// Deterministic: equal residuals break on the longer vector, then the lower /// index, so two devices holding the same faces choose the same references /// and group the same way (`cluster::clustering_is_deterministic`). pub fn select(embeddings: &[&[f32]], quality: &[Option], max: usize) -> Vec { debug_assert_eq!(embeddings.len(), quality.len()); let mut pool: Vec = (0..embeddings.len()) .filter(|&i| eligible(quality[i])) .collect(); if pool.len() <= max { return pool; } // Longest first, so the seed is the pool's front and a tie on residual // resolves to the earlier position. A missing reading ranks below any // measured one for this purpose only: it is admitted, but a face that // was measured and found long is the better seed. pool.sort_by(|&a, &b| { let qa = quality[a].unwrap_or(0.0); let qb = quality[b].unwrap_or(0.0); qb.total_cmp(&qa).then(a.cmp(&b)) }); // Residuals: what remains of each pool vector outside the span of the // chosen ones. Copied, since they are rewritten in place. let mut residual: Vec> = pool.iter().map(|&i| embeddings[i].to_vec()).collect(); let mut taken = vec![false; pool.len()]; let mut chosen = Vec::with_capacity(max); while chosen.len() < max { // The face with the most left outside the span. The seed is the // pool's front by construction: every unit vector has the same // residual before anything is chosen, up to rounding, and rounding // is not a reason to prefer one. After that `> best` and not `>=`, // so a genuine tie keeps the earlier (longer) candidate. let mut pick = None; let mut best = 0.0_f32; if chosen.is_empty() { pick = Some(0); best = residual[0].iter().map(|x| x * x).sum(); } else { for (k, r) in residual.iter().enumerate() { if taken[k] { continue; } let n2: f32 = r.iter().map(|x| x * x).sum(); if n2 > best { best = n2; pick = Some(k); } } } // Nothing left outside the span: every remaining face is a // combination of the chosen ones and adds no volume. let Some(k) = pick.filter(|_| best > 1e-6) else { break; }; taken[k] = true; chosen.push(pool[k]); // Project the chosen direction out of every remaining residual. let inv = best.sqrt().recip(); let q: Vec = residual[k].iter().map(|x| x * inv).collect(); for (j, r) in residual.iter_mut().enumerate() { if taken[j] { continue; } let d: f32 = r.iter().zip(&q).map(|(a, b)| a * b).sum(); for (x, y) in r.iter_mut().zip(&q) { *x -= d * y; } } } chosen } #[cfg(test)] mod tests { use super::*; fn unit(v: &[f32]) -> Vec { let n = v.iter().map(|x| x * x).sum::().sqrt(); v.iter().map(|x| x / n).collect() } #[test] fn a_person_under_the_cap_keeps_every_eligible_face() { let e = [unit(&[1.0, 0.0]), unit(&[0.0, 1.0]), unit(&[1.0, 1.0])]; let refs: Vec<&[f32]> = e.iter().map(Vec::as_slice).collect(); let q = [Some(20.0), None, Some(16.0)]; assert_eq!(select(&refs, &q, 100), vec![0, 1, 2]); } #[test] fn a_short_vector_never_stands_for_a_person() { let e = [unit(&[1.0, 0.0]), unit(&[0.0, 1.0])]; let refs: Vec<&[f32]> = e.iter().map(Vec::as_slice).collect(); let q = [Some(20.0), Some(MIN_REFERENCE_QUALITY - 0.01)]; assert_eq!(select(&refs, &q, 100), vec![0]); } /// Two hundred frames from one afternoon and one profile shot: the /// profile is the second choice, not the two-hundred-and-first. #[test] fn the_odd_one_out_is_chosen_before_any_duplicate() { let mut e: Vec> = Vec::new(); let mut q = Vec::new(); for i in 0..200 { // Near-duplicates of one direction, with a little noise. let t = (i as f32) * 1e-3; e.push(unit(&[1.0, t, t * 0.5])); q.push(Some(20.0 + (i % 7) as f32)); } e.push(unit(&[0.0, 0.0, 1.0])); q.push(Some(16.0)); let refs: Vec<&[f32]> = e.iter().map(Vec::as_slice).collect(); let chosen = select(&refs, &q, 3); assert_eq!(chosen.len(), 3); assert_eq!( chosen[1], 200, "the profile shot was not second: {chosen:?}" ); // Seeded on the longest vector. assert_eq!(q[chosen[0]], Some(26.0)); } /// Faces inside the span of the chosen ones add no volume and are not /// taken to fill the cap. #[test] fn the_cap_is_not_filled_from_inside_the_span() { let e = [ unit(&[1.0, 0.0]), unit(&[0.0, 1.0]), unit(&[1.0, 1.0]), unit(&[2.0, -1.0]), ]; let refs: Vec<&[f32]> = e.iter().map(Vec::as_slice).collect(); let q = [Some(20.0); 4]; assert_eq!(select(&refs, &q, 3).len(), 2); } #[test] fn the_choice_is_deterministic() { let e: Vec> = (0..50) .map(|i| { let a = (i as f32) * 0.37; unit(&[a.cos(), a.sin(), (a * 3.0).sin(), 0.2]) }) .collect(); let refs: Vec<&[f32]> = e.iter().map(Vec::as_slice).collect(); let q = vec![Some(18.0); 50]; assert_eq!(select(&refs, &q, 5), select(&refs, &q, 5)); } }