Stand each person in the grouping pass by at most 100 references
Benchmarks / CPU and I/O (per commit) (push) Failing after 6m23s
Benchmarks / Frame budget (on demand) (push) Skipped
Build and test / Desktop (Linux) (push) Failing after 55s
Build and test / Layer separation (push) Successful in 27s
Traceability / Requirement traces (push) Failing after 54s
🐳 Android image / Build and push (push) Successful in 1s
Build and test / android-image (push) Successful in 1s
🐳 Windows image / Build and push (push) Successful in 1s
Build and test / windows-image (push) Successful in 1s
Build and test / Android (aarch64) (push) Failing after 2m21s
Build and test / Windows (x86_64, cross) (push) Failing after 3m5s
Benchmarks / CPU and I/O (per commit) (push) Failing after 6m23s
Benchmarks / Frame budget (on demand) (push) Skipped
Build and test / Desktop (Linux) (push) Failing after 55s
Build and test / Layer separation (push) Successful in 27s
Traceability / Requirement traces (push) Failing after 54s
🐳 Android image / Build and push (push) Successful in 1s
Build and test / android-image (push) Successful in 1s
🐳 Windows image / Build and push (push) Successful in 1s
Build and test / windows-image (push) Successful in 1s
Build and test / Android (aarch64) (push) Failing after 2m21s
Build and test / Windows (x86_64, cross) (push) Failing after 3m5s
Every face the user has ruled on entered the pass as an anchor, and the scan is exhaustive by design (`dr_face::neighbours`), so a person with 750 confirmed faces cost 750 comparisons against every other face in the library — and the cost of a library grew with how well it was named. Most of those comparisons said nothing new: thirty frames from one afternoon are one point of view, not thirty, and a face that matches one of them matches the rest. Each person now enters through at most 100 of their anchored faces (`dr_face::references`). Eligible are those whose raw embedding is at least 15 long — one above the gallery floor, since a reference speaks for someone rather than merely being admitted — with an unmeasured length admitted as it is everywhere else. From those, the set spanning the greatest volume is chosen greedily: the longest vector first, then at each step the face with the largest component orthogonal to the chosen so far. That is pivoted Gram–Schmidt, and the product of the residuals it picks is the Gram determinant, so the greedy step is the exact greedy on the objective. A near-duplicate of a chosen face has no residual and is passed over; the one profile shot among two hundred frontal frames is taken early; faces inside the span of the chosen add no volume and are not taken to fill the cap. The faces not chosen keep their confirmations and are not touched by the pass — they stay in the anchor map, so it never releases them — they are simply not compared. A person none of whose faces is long enough is still stood for, by their longest, rather than losing their anchor and having their next face filed as a stranger. Under the cap nothing changes: every eligible face stands, and the short ones stay in as the probes they were. At the reference library's 3,851 confirmations the scan shrinks by about a fifth; at 15,000 it is a fifth of what it was.
This commit is contained in:
@@ -50,6 +50,7 @@ pub mod eyes;
|
||||
pub mod landmarks;
|
||||
pub mod naming;
|
||||
pub mod neighbours;
|
||||
pub mod references;
|
||||
|
||||
/// Smallest long edge a face crop may be sampled from.
|
||||
///
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
//! 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<f32>) -> 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<f32>], max: usize) -> Vec<usize> {
|
||||
debug_assert_eq!(embeddings.len(), quality.len());
|
||||
let mut pool: Vec<usize> = (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<Vec<f32>> = 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<f32> = 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<f32> {
|
||||
let n = v.iter().map(|x| x * x).sum::<f32>().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<f32>> = 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<Vec<f32>> = (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));
|
||||
}
|
||||
}
|
||||
@@ -821,6 +821,16 @@ here, and it is the phone and tablet story that should decide whether it gets bu
|
||||
already has, the raw vector written over the old one and its id, box and identity untouched
|
||||
(`faces::record_updates`). No detector runs and no suggestion is lost — the cost is the
|
||||
original fetched once more, since the length exists only at the moment of embedding.
|
||||
- **A person is stood for by their references.** Every face the user has ruled on is an anchor,
|
||||
and the scan is exhaustive, so a person with 750 confirmations would cost 750 comparisons
|
||||
against every other face — and the cost of a library would grow with how well it was named.
|
||||
Instead each person enters through at most `MAX_REFERENCES` (100) of their anchored faces,
|
||||
chosen by `dr_face::references`: those whose raw embedding is at least `MIN_REFERENCE_QUALITY`
|
||||
(15) long, and among them the set spanning the greatest volume — greedy max-determinant, the
|
||||
longest vector first and then, at each step, the face with the largest component orthogonal to
|
||||
the chosen so far. Thirty frames from one afternoon contribute one reference; the single profile
|
||||
shot is taken early. The faces not chosen keep their confirmations and are not touched by the
|
||||
pass; they are simply not compared.
|
||||
|
||||
**The algorithm.** Constrained average-link agglomeration over the probability graph, merging while
|
||||
the average pairwise probability exceeds **0.9** and no cannot-link is violated. Average-link rather
|
||||
|
||||
@@ -9,8 +9,8 @@ Denominators are parsed from [`requirements.md`](requirements.md) at run time, n
|
||||
|
||||
| Metric | Value |
|
||||
|---|---|
|
||||
| Source files scanned | 391 |
|
||||
| TRACES tags found | 1567 |
|
||||
| Source files scanned | 392 |
|
||||
| TRACES tags found | 1568 |
|
||||
| Requirements defined | 184 |
|
||||
| Requirements deferred (post-v1) | 24 |
|
||||
| Requirements covered | 154 |
|
||||
@@ -50,7 +50,7 @@ A tag naming an ID `requirements.md` does not define — what renumbering produc
|
||||
| FR-CAT-8 | [`core/dr-catalog/src/schema.rs:740`](../core/dr-catalog/src/schema.rs#L740), [`core/dr-pipeline/src/coverage.rs:1`](../core/dr-pipeline/src/coverage.rs#L1), [`core/dr-pipeline/src/graph.rs:587`](../core/dr-pipeline/src/graph.rs#L587), [`core/dr-pipeline/src/graph.rs:640`](../core/dr-pipeline/src/graph.rs#L640), [`core/dr-pipeline/src/ops/curve.rs:137`](../core/dr-pipeline/src/ops/curve.rs#L137), [`core/dr-pipeline/src/ops/curve.rs:656`](../core/dr-pipeline/src/ops/curve.rs#L656), [`core/dr-pipeline/src/sidecar.rs:1400`](../core/dr-pipeline/src/sidecar.rs#L1400), [`core/dr-pipeline/src/sidecar.rs:2232`](../core/dr-pipeline/src/sidecar.rs#L2232), [`core/dr-pipeline/src/sidecar.rs:96`](../core/dr-pipeline/src/sidecar.rs#L96), [`core/dr-pipeline/src/state.rs:1`](../core/dr-pipeline/src/state.rs#L1), [`core/dr-pipeline/src/state.rs:75`](../core/dr-pipeline/src/state.rs#L75), [`core/dr-pipeline/tests/tone_curve.rs:34`](../core/dr-pipeline/tests/tone_curve.rs#L34), [`core/dr-sync/src/scan.rs:31`](../core/dr-sync/src/scan.rs#L31), [`core/dr-sync/src/scan.rs:56`](../core/dr-sync/src/scan.rs#L56), [`core/dr-sync/src/scan.rs:926`](../core/dr-sync/src/scan.rs#L926), [`ui/dr-ui/src/develop.rs:2155`](../ui/dr-ui/src/develop.rs#L2155), [`ui/dr-ui/src/develop.rs:5430`](../ui/dr-ui/src/develop.rs#L5430), [`ui/dr-ui/src/develop.rs:5459`](../ui/dr-ui/src/develop.rs#L5459), [`ui/dr-ui/src/develop.rs:6833`](../ui/dr-ui/src/develop.rs#L6833), [`ui/dr-ui/src/develop.rs:6887`](../ui/dr-ui/src/develop.rs#L6887), [`ui/dr-ui/src/develop.rs:6989`](../ui/dr-ui/src/develop.rs#L6989), [`ui/dr-ui/src/export.rs:812`](../ui/dr-ui/src/export.rs#L812), [`ui/dr-ui/src/lib.rs:1330`](../ui/dr-ui/src/lib.rs#L1330), [`ui/dr-ui/src/lib.rs:1888`](../ui/dr-ui/src/lib.rs#L1888), [`ui/dr-ui/src/lib.rs:2457`](../ui/dr-ui/src/lib.rs#L2457), [`ui/dr-ui/src/lib.rs:2604`](../ui/dr-ui/src/lib.rs#L2604), [`ui/dr-ui/src/lib.rs:757`](../ui/dr-ui/src/lib.rs#L757), [`ui/dr-ui/src/library.rs:1455`](../ui/dr-ui/src/library.rs#L1455), [`ui/dr-ui/src/library.rs:1490`](../ui/dr-ui/src/library.rs#L1490), [`ui/dr-ui/src/library.rs:2491`](../ui/dr-ui/src/library.rs#L2491), [`ui/dr-ui/src/library.rs:564`](../ui/dr-ui/src/library.rs#L564), [`ui/dr-ui/src/library.rs:616`](../ui/dr-ui/src/library.rs#L616), [`ui/dr-ui/src/library.rs:653`](../ui/dr-ui/src/library.rs#L653), [`ui/dr-ui/src/library.rs:7386`](../ui/dr-ui/src/library.rs#L7386), [`ui/dr-ui/src/library.rs:73`](../ui/dr-ui/src/library.rs#L73), [`ui/dr-ui/src/library.rs:926`](../ui/dr-ui/src/library.rs#L926), [`ui/dr-ui/src/library_ui.rs:1339`](../ui/dr-ui/src/library_ui.rs#L1339), [`ui/dr-ui/src/library_ui.rs:6350`](../ui/dr-ui/src/library_ui.rs#L6350), [`ui/dr-ui/src/sidecar_cache.rs:1`](../ui/dr-ui/src/sidecar_cache.rs#L1) |
|
||||
| FR-CAT-9 | [`core/dr-catalog/src/cache.rs:1`](../core/dr-catalog/src/cache.rs#L1), [`core/dr-catalog/src/scan.rs:1`](../core/dr-catalog/src/scan.rs#L1), [`core/dr-catalog/src/schema.rs:1182`](../core/dr-catalog/src/schema.rs#L1182), [`core/dr-catalog/src/walk.rs:1022`](../core/dr-catalog/src/walk.rs#L1022), [`core/dr-catalog/src/walk.rs:162`](../core/dr-catalog/src/walk.rs#L162), [`core/dr-catalog/src/walk.rs:1`](../core/dr-catalog/src/walk.rs#L1), [`core/dr-catalog/src/walk.rs:435`](../core/dr-catalog/src/walk.rs#L435), [`core/dr-catalog/src/walk.rs:728`](../core/dr-catalog/src/walk.rs#L728), [`core/dr-sync-nextcloud/src/desktop_client.rs:35`](../core/dr-sync-nextcloud/src/desktop_client.rs#L35), [`core/dr-sync/src/error.rs:113`](../core/dr-sync/src/error.rs#L113), [`core/dr-sync/src/reachability.rs:1`](../core/dr-sync/src/reachability.rs#L1), [`core/dr-sync/src/scan.rs:211`](../core/dr-sync/src/scan.rs#L211), [`core/dr-sync/src/scan.rs:519`](../core/dr-sync/src/scan.rs#L519), [`core/dr-types/src/lib.rs:122`](../core/dr-types/src/lib.rs#L122), [`ui/dr-ui/src/develop.rs:4719`](../ui/dr-ui/src/develop.rs#L4719), [`ui/dr-ui/src/develop.rs:4762`](../ui/dr-ui/src/develop.rs#L4762), [`ui/dr-ui/src/library.rs:1355`](../ui/dr-ui/src/library.rs#L1355), [`ui/dr-ui/src/library.rs:1408`](../ui/dr-ui/src/library.rs#L1408), [`ui/dr-ui/src/library.rs:1439`](../ui/dr-ui/src/library.rs#L1439), [`ui/dr-ui/src/library.rs:1490`](../ui/dr-ui/src/library.rs#L1490), [`ui/dr-ui/src/library.rs:169`](../ui/dr-ui/src/library.rs#L169), [`ui/dr-ui/src/library.rs:2019`](../ui/dr-ui/src/library.rs#L2019), [`ui/dr-ui/src/library.rs:2471`](../ui/dr-ui/src/library.rs#L2471), [`ui/dr-ui/src/library.rs:2527`](../ui/dr-ui/src/library.rs#L2527), [`ui/dr-ui/src/library.rs:274`](../ui/dr-ui/src/library.rs#L274), [`ui/dr-ui/src/library.rs:5224`](../ui/dr-ui/src/library.rs#L5224), [`ui/dr-ui/src/library.rs:653`](../ui/dr-ui/src/library.rs#L653), [`ui/dr-ui/src/library.rs:910`](../ui/dr-ui/src/library.rs#L910), [`ui/dr-ui/src/library.rs:926`](../ui/dr-ui/src/library.rs#L926), [`ui/dr-ui/src/library.rs:980`](../ui/dr-ui/src/library.rs#L980), [`ui/dr-ui/src/library_ui.rs:1406`](../ui/dr-ui/src/library_ui.rs#L1406), [`ui/dr-ui/src/library_ui.rs:1997`](../ui/dr-ui/src/library_ui.rs#L1997), [`ui/dr-ui/src/library_ui.rs:2041`](../ui/dr-ui/src/library_ui.rs#L2041), [`ui/dr-ui/src/library_ui.rs:2057`](../ui/dr-ui/src/library_ui.rs#L2057), [`ui/dr-ui/src/library_ui.rs:2151`](../ui/dr-ui/src/library_ui.rs#L2151), [`ui/dr-ui/src/library_ui.rs:284`](../ui/dr-ui/src/library_ui.rs#L284), [`ui/dr-ui/src/library_ui.rs:2993`](../ui/dr-ui/src/library_ui.rs#L2993), [`ui/dr-ui/src/library_ui.rs:320`](../ui/dr-ui/src/library_ui.rs#L320), [`ui/dr-ui/src/library_ui.rs:328`](../ui/dr-ui/src/library_ui.rs#L328), [`ui/dr-ui/src/library_ui.rs:3616`](../ui/dr-ui/src/library_ui.rs#L3616), [`ui/dr-ui/src/library_ui.rs:3838`](../ui/dr-ui/src/library_ui.rs#L3838), [`ui/dr-ui/src/library_ui.rs:4116`](../ui/dr-ui/src/library_ui.rs#L4116), [`ui/dr-ui/src/library_ui.rs:4232`](../ui/dr-ui/src/library_ui.rs#L4232), [`ui/dr-ui/src/library_ui.rs:4420`](../ui/dr-ui/src/library_ui.rs#L4420), [`ui/dr-ui/src/library_ui.rs:4542`](../ui/dr-ui/src/library_ui.rs#L4542), [`ui/dr-ui/src/library_ui.rs:573`](../ui/dr-ui/src/library_ui.rs#L573), [`ui/dr-ui/src/library_ui.rs:662`](../ui/dr-ui/src/library_ui.rs#L662), [`ui/dr-ui/src/library_ui.rs:6864`](../ui/dr-ui/src/library_ui.rs#L6864), [`ui/dr-ui/src/library_ui.rs:6946`](../ui/dr-ui/src/library_ui.rs#L6946), [`ui/dr-ui/src/presets.rs:486`](../ui/dr-ui/src/presets.rs#L486), [`ui/dr-ui/src/presets.rs:498`](../ui/dr-ui/src/presets.rs#L498), [`ui/dr-ui/src/sidecar_cache.rs:1`](../ui/dr-ui/src/sidecar_cache.rs#L1), [`ui/dr-ui/tests/export_ignores_the_viewport.rs:1`](../ui/dr-ui/tests/export_ignores_the_viewport.rs#L1) |
|
||||
| FR-CULL-1 | [`core/dr-decode/src/preview.rs:121`](../core/dr-decode/src/preview.rs#L121) |
|
||||
| FR-CULL-10 | [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:1011`](../core/dr-catalog/src/schema.rs#L1011), [`core/dr-catalog/src/schema.rs:603`](../core/dr-catalog/src/schema.rs#L603), [`core/dr-catalog/src/schema.rs:781`](../core/dr-catalog/src/schema.rs#L781), [`core/dr-face/src/assign.rs:1`](../core/dr-face/src/assign.rs#L1), [`core/dr-face/src/neighbours.rs:1`](../core/dr-face/src/neighbours.rs#L1), [`core/dr-types/src/settings.rs:129`](../core/dr-types/src/settings.rs#L129), [`ui/dr-ui/src/develop.rs:121`](../ui/dr-ui/src/develop.rs#L121), [`ui/dr-ui/src/develop.rs:130`](../ui/dr-ui/src/develop.rs#L130), [`ui/dr-ui/src/develop.rs:196`](../ui/dr-ui/src/develop.rs#L196), [`ui/dr-ui/src/develop.rs:2416`](../ui/dr-ui/src/develop.rs#L2416), [`ui/dr-ui/src/develop.rs:715`](../ui/dr-ui/src/develop.rs#L715), [`ui/dr-ui/src/faces.rs:1`](../ui/dr-ui/src/faces.rs#L1), [`ui/dr-ui/src/identity.rs:1`](../ui/dr-ui/src/identity.rs#L1), [`ui/dr-ui/src/identity_ui.rs:1325`](../ui/dr-ui/src/identity_ui.rs#L1325), [`ui/dr-ui/src/identity_ui.rs:1`](../ui/dr-ui/src/identity_ui.rs#L1), [`ui/dr-ui/src/lib.rs:2576`](../ui/dr-ui/src/lib.rs#L2576), [`ui/dr-ui/src/repairs.rs:1`](../ui/dr-ui/src/repairs.rs#L1), [`ui/dr-ui/ui/identity.slint:1`](../ui/dr-ui/ui/identity.slint#L1), [`ui/dr-ui/ui/settings.slint:529`](../ui/dr-ui/ui/settings.slint#L529), [`ui/dr-ui/ui/settings.slint:94`](../ui/dr-ui/ui/settings.slint#L94) |
|
||||
| FR-CULL-10 | [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:1011`](../core/dr-catalog/src/schema.rs#L1011), [`core/dr-catalog/src/schema.rs:603`](../core/dr-catalog/src/schema.rs#L603), [`core/dr-catalog/src/schema.rs:781`](../core/dr-catalog/src/schema.rs#L781), [`core/dr-face/src/assign.rs:1`](../core/dr-face/src/assign.rs#L1), [`core/dr-face/src/neighbours.rs:1`](../core/dr-face/src/neighbours.rs#L1), [`core/dr-face/src/references.rs:1`](../core/dr-face/src/references.rs#L1), [`core/dr-types/src/settings.rs:129`](../core/dr-types/src/settings.rs#L129), [`ui/dr-ui/src/develop.rs:121`](../ui/dr-ui/src/develop.rs#L121), [`ui/dr-ui/src/develop.rs:130`](../ui/dr-ui/src/develop.rs#L130), [`ui/dr-ui/src/develop.rs:196`](../ui/dr-ui/src/develop.rs#L196), [`ui/dr-ui/src/develop.rs:2416`](../ui/dr-ui/src/develop.rs#L2416), [`ui/dr-ui/src/develop.rs:715`](../ui/dr-ui/src/develop.rs#L715), [`ui/dr-ui/src/faces.rs:1`](../ui/dr-ui/src/faces.rs#L1), [`ui/dr-ui/src/identity.rs:1`](../ui/dr-ui/src/identity.rs#L1), [`ui/dr-ui/src/identity_ui.rs:1325`](../ui/dr-ui/src/identity_ui.rs#L1325), [`ui/dr-ui/src/identity_ui.rs:1`](../ui/dr-ui/src/identity_ui.rs#L1), [`ui/dr-ui/src/lib.rs:2576`](../ui/dr-ui/src/lib.rs#L2576), [`ui/dr-ui/src/repairs.rs:1`](../ui/dr-ui/src/repairs.rs#L1), [`ui/dr-ui/ui/identity.slint:1`](../ui/dr-ui/ui/identity.slint#L1), [`ui/dr-ui/ui/settings.slint:529`](../ui/dr-ui/ui/settings.slint#L529), [`ui/dr-ui/ui/settings.slint:94`](../ui/dr-ui/ui/settings.slint#L94) |
|
||||
| FR-CULL-11 | [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:1011`](../core/dr-catalog/src/schema.rs#L1011), [`ui/dr-ui/src/identity.rs:1`](../ui/dr-ui/src/identity.rs#L1), [`ui/dr-ui/src/identity_ui.rs:1`](../ui/dr-ui/src/identity_ui.rs#L1), [`ui/dr-ui/src/library.rs:295`](../ui/dr-ui/src/library.rs#L295), [`ui/dr-ui/src/library.rs:345`](../ui/dr-ui/src/library.rs#L345), [`ui/dr-ui/ui/identity.slint:1`](../ui/dr-ui/ui/identity.slint#L1) |
|
||||
| FR-CULL-12 | [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:1011`](../core/dr-catalog/src/schema.rs#L1011), [`core/dr-catalog/src/schema.rs:603`](../core/dr-catalog/src/schema.rs#L603), [`ui/dr-ui/src/identity.rs:1`](../ui/dr-ui/src/identity.rs#L1), [`ui/dr-ui/ui/identity.slint:1`](../ui/dr-ui/ui/identity.slint#L1) |
|
||||
| FR-CULL-13 | [`core/dr-catalog/src/schema.rs:917`](../core/dr-catalog/src/schema.rs#L917), [`core/dr-types/src/place.rs:129`](../core/dr-types/src/place.rs#L129), [`ui/dr-ui/src/identity.rs:104`](../ui/dr-ui/src/identity.rs#L104), [`ui/dr-ui/src/library.rs:321`](../ui/dr-ui/src/library.rs#L321), [`ui/dr-ui/src/library.rs:7219`](../ui/dr-ui/src/library.rs#L7219), [`ui/dr-ui/src/library.rs:7262`](../ui/dr-ui/src/library.rs#L7262), [`ui/dr-ui/src/library.rs:7300`](../ui/dr-ui/src/library.rs#L7300), [`ui/dr-ui/ui/app.slint:707`](../ui/dr-ui/ui/app.slint#L707), [`ui/dr-ui/ui/identity.slint:63`](../ui/dr-ui/ui/identity.slint#L63), [`ui/dr-ui/ui/library.slint:1512`](../ui/dr-ui/ui/library.slint#L1512) |
|
||||
@@ -59,7 +59,7 @@ A tag naming an ID `requirements.md` does not define — what renumbering produc
|
||||
| FR-CULL-4 | [`core/dr-catalog/src/rating.rs:1`](../core/dr-catalog/src/rating.rs#L1), [`core/dr-pipeline/src/sidecar.rs:171`](../core/dr-pipeline/src/sidecar.rs#L171), [`core/dr-xmp/src/lib.rs:442`](../core/dr-xmp/src/lib.rs#L442), [`ui/dr-ui/src/library.rs:254`](../ui/dr-ui/src/library.rs#L254), [`ui/dr-ui/src/library.rs:564`](../ui/dr-ui/src/library.rs#L564) |
|
||||
| FR-CULL-5 | [`core/dr-catalog/src/bursts.rs:1`](../core/dr-catalog/src/bursts.rs#L1), [`core/dr-catalog/src/schema.rs:646`](../core/dr-catalog/src/schema.rs#L646), [`ui/dr-ui/src/bursts.rs:1`](../ui/dr-ui/src/bursts.rs#L1), [`ui/dr-ui/src/library.rs:210`](../ui/dr-ui/src/library.rs#L210), [`ui/dr-ui/src/library.rs:6039`](../ui/dr-ui/src/library.rs#L6039) |
|
||||
| FR-CULL-8 | [`core/dr-catalog/src/face_shard.rs:1`](../core/dr-catalog/src/face_shard.rs#L1), [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:1011`](../core/dr-catalog/src/schema.rs#L1011), [`core/dr-catalog/src/schema.rs:714`](../core/dr-catalog/src/schema.rs#L714), [`core/dr-catalog/src/schema.rs:970`](../core/dr-catalog/src/schema.rs#L970), [`core/dr-face/src/align.rs:296`](../core/dr-face/src/align.rs#L296), [`core/dr-types/src/settings.rs:176`](../core/dr-types/src/settings.rs#L176), [`ui/dr-ui/examples/face_detectors.rs:1`](../ui/dr-ui/examples/face_detectors.rs#L1), [`ui/dr-ui/examples/face_native.rs:1`](../ui/dr-ui/examples/face_native.rs#L1), [`ui/dr-ui/src/derived_sync.rs:124`](../ui/dr-ui/src/derived_sync.rs#L124), [`ui/dr-ui/src/faces.rs:1`](../ui/dr-ui/src/faces.rs#L1), [`ui/dr-ui/src/faces.rs:446`](../ui/dr-ui/src/faces.rs#L446), [`ui/dr-ui/src/faces.rs:462`](../ui/dr-ui/src/faces.rs#L462), [`ui/dr-ui/src/faces.rs:584`](../ui/dr-ui/src/faces.rs#L584), [`ui/dr-ui/src/identity_ui.rs:1325`](../ui/dr-ui/src/identity_ui.rs#L1325), [`ui/dr-ui/src/identity_ui.rs:603`](../ui/dr-ui/src/identity_ui.rs#L603), [`ui/dr-ui/src/lib.rs:1511`](../ui/dr-ui/src/lib.rs#L1511), [`ui/dr-ui/src/lib.rs:3942`](../ui/dr-ui/src/lib.rs#L3942), [`ui/dr-ui/src/library.rs:3624`](../ui/dr-ui/src/library.rs#L3624), [`ui/dr-ui/src/library.rs:3889`](../ui/dr-ui/src/library.rs#L3889), [`ui/dr-ui/src/library_ui.rs:406`](../ui/dr-ui/src/library_ui.rs#L406), [`ui/dr-ui/src/library_ui.rs:493`](../ui/dr-ui/src/library_ui.rs#L493), [`ui/dr-ui/src/repairs.rs:1`](../ui/dr-ui/src/repairs.rs#L1), [`ui/dr-ui/src/settings_ui.rs:210`](../ui/dr-ui/src/settings_ui.rs#L210), [`ui/dr-ui/src/settings_ui.rs:552`](../ui/dr-ui/src/settings_ui.rs#L552), [`ui/dr-ui/ui/app.slint:946`](../ui/dr-ui/ui/app.slint#L946), [`ui/dr-ui/ui/settings.slint:100`](../ui/dr-ui/ui/settings.slint#L100), [`ui/dr-ui/ui/settings.slint:467`](../ui/dr-ui/ui/settings.slint#L467), [`ui/dr-ui/ui/settings.slint:529`](../ui/dr-ui/ui/settings.slint#L529), [`ui/dr-ui/ui/settings.slint:82`](../ui/dr-ui/ui/settings.slint#L82), [`ui/dr-ui/ui/settings.slint:94`](../ui/dr-ui/ui/settings.slint#L94) |
|
||||
| FR-CULL-8a | [`core/dr-catalog/src/faces.rs:124`](../core/dr-catalog/src/faces.rs#L124), [`core/dr-catalog/src/schema.rs:917`](../core/dr-catalog/src/schema.rs#L917), [`core/dr-face/src/classify.rs:1`](../core/dr-face/src/classify.rs#L1), [`core/dr-face/src/eyes.rs:1`](../core/dr-face/src/eyes.rs#L1), [`core/dr-face/src/landmarks.rs:1`](../core/dr-face/src/landmarks.rs#L1), [`core/dr-types/src/place.rs:129`](../core/dr-types/src/place.rs#L129), [`ui/dr-ui/src/faces.rs:166`](../ui/dr-ui/src/faces.rs#L166), [`ui/dr-ui/src/faces.rs:1748`](../ui/dr-ui/src/faces.rs#L1748), [`ui/dr-ui/src/faces.rs:308`](../ui/dr-ui/src/faces.rs#L308), [`ui/dr-ui/src/identity.rs:104`](../ui/dr-ui/src/identity.rs#L104), [`ui/dr-ui/src/library.rs:321`](../ui/dr-ui/src/library.rs#L321), [`ui/dr-ui/src/library.rs:7219`](../ui/dr-ui/src/library.rs#L7219), [`ui/dr-ui/src/library.rs:7262`](../ui/dr-ui/src/library.rs#L7262), [`ui/dr-ui/src/library.rs:7300`](../ui/dr-ui/src/library.rs#L7300), [`ui/dr-ui/src/repairs.rs:571`](../ui/dr-ui/src/repairs.rs#L571), [`ui/dr-ui/ui/app.slint:707`](../ui/dr-ui/ui/app.slint#L707), [`ui/dr-ui/ui/identity.slint:273`](../ui/dr-ui/ui/identity.slint#L273), [`ui/dr-ui/ui/identity.slint:63`](../ui/dr-ui/ui/identity.slint#L63), [`ui/dr-ui/ui/library.slint:1512`](../ui/dr-ui/ui/library.slint#L1512) |
|
||||
| FR-CULL-8a | [`core/dr-catalog/src/faces.rs:124`](../core/dr-catalog/src/faces.rs#L124), [`core/dr-catalog/src/schema.rs:917`](../core/dr-catalog/src/schema.rs#L917), [`core/dr-face/src/classify.rs:1`](../core/dr-face/src/classify.rs#L1), [`core/dr-face/src/eyes.rs:1`](../core/dr-face/src/eyes.rs#L1), [`core/dr-face/src/landmarks.rs:1`](../core/dr-face/src/landmarks.rs#L1), [`core/dr-types/src/place.rs:129`](../core/dr-types/src/place.rs#L129), [`ui/dr-ui/src/faces.rs:166`](../ui/dr-ui/src/faces.rs#L166), [`ui/dr-ui/src/faces.rs:1821`](../ui/dr-ui/src/faces.rs#L1821), [`ui/dr-ui/src/faces.rs:308`](../ui/dr-ui/src/faces.rs#L308), [`ui/dr-ui/src/identity.rs:104`](../ui/dr-ui/src/identity.rs#L104), [`ui/dr-ui/src/library.rs:321`](../ui/dr-ui/src/library.rs#L321), [`ui/dr-ui/src/library.rs:7219`](../ui/dr-ui/src/library.rs#L7219), [`ui/dr-ui/src/library.rs:7262`](../ui/dr-ui/src/library.rs#L7262), [`ui/dr-ui/src/library.rs:7300`](../ui/dr-ui/src/library.rs#L7300), [`ui/dr-ui/src/repairs.rs:571`](../ui/dr-ui/src/repairs.rs#L571), [`ui/dr-ui/ui/app.slint:707`](../ui/dr-ui/ui/app.slint#L707), [`ui/dr-ui/ui/identity.slint:273`](../ui/dr-ui/ui/identity.slint#L273), [`ui/dr-ui/ui/identity.slint:63`](../ui/dr-ui/ui/identity.slint#L63), [`ui/dr-ui/ui/library.slint:1512`](../ui/dr-ui/ui/library.slint#L1512) |
|
||||
| FR-CULL-9 | [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:1011`](../core/dr-catalog/src/schema.rs#L1011), [`core/dr-catalog/src/schema.rs:781`](../core/dr-catalog/src/schema.rs#L781), [`core/dr-face/src/assign.rs:1`](../core/dr-face/src/assign.rs#L1), [`core/dr-face/src/neighbours.rs:1`](../core/dr-face/src/neighbours.rs#L1), [`core/dr-types/src/settings.rs:129`](../core/dr-types/src/settings.rs#L129), [`ui/dr-ui/src/faces.rs:1`](../ui/dr-ui/src/faces.rs#L1), [`ui/dr-ui/src/faces.rs:584`](../ui/dr-ui/src/faces.rs#L584), [`ui/dr-ui/src/identity_ui.rs:1`](../ui/dr-ui/src/identity_ui.rs#L1), [`ui/dr-ui/src/repairs.rs:517`](../ui/dr-ui/src/repairs.rs#L517), [`ui/dr-ui/ui/identity.slint:347`](../ui/dr-ui/ui/identity.slint#L347) |
|
||||
| FR-DEV-1 | [`core/dr-pipeline/src/graph.rs:1`](../core/dr-pipeline/src/graph.rs#L1), [`core/dr-pipeline/src/sidecar.rs:1`](../core/dr-pipeline/src/sidecar.rs#L1) |
|
||||
| FR-DEV-10 | [`core/dr-gpu/src/mask.rs:1027`](../core/dr-gpu/src/mask.rs#L1027), [`core/dr-gpu/src/mask.rs:1251`](../core/dr-gpu/src/mask.rs#L1251), [`core/dr-gpu/src/mask.rs:1509`](../core/dr-gpu/src/mask.rs#L1509), [`core/dr-gpu/src/mask.rs:421`](../core/dr-gpu/src/mask.rs#L421), [`core/dr-gpu/src/mask.rs:463`](../core/dr-gpu/src/mask.rs#L463), [`core/dr-gpu/src/mask.rs:58`](../core/dr-gpu/src/mask.rs#L58), [`core/dr-gpu/src/mask.rs:60`](../core/dr-gpu/src/mask.rs#L60), [`core/dr-gpu/src/mask.rs:834`](../core/dr-gpu/src/mask.rs#L834), [`core/dr-gpu/src/mask.rs:85`](../core/dr-gpu/src/mask.rs#L85), [`core/dr-gpu/src/mask.rs:913`](../core/dr-gpu/src/mask.rs#L913), [`core/dr-gpu/src/shaders/mask.wgsl:117`](../core/dr-gpu/src/shaders/mask.wgsl#L117), [`core/dr-gpu/src/shaders/mask.wgsl:59`](../core/dr-gpu/src/shaders/mask.wgsl#L59), [`core/dr-gpu/tests/range_masks.rs:119`](../core/dr-gpu/tests/range_masks.rs#L119), [`core/dr-gpu/tests/range_masks.rs:147`](../core/dr-gpu/tests/range_masks.rs#L147), [`core/dr-gpu/tests/range_masks.rs:175`](../core/dr-gpu/tests/range_masks.rs#L175), [`core/dr-gpu/tests/range_masks.rs:1`](../core/dr-gpu/tests/range_masks.rs#L1), [`core/dr-gpu/tests/range_masks.rs:94`](../core/dr-gpu/tests/range_masks.rs#L94), [`core/dr-pipeline/src/mask.rs:125`](../core/dr-pipeline/src/mask.rs#L125), [`core/dr-pipeline/src/mask.rs:2845`](../core/dr-pipeline/src/mask.rs#L2845), [`core/dr-pipeline/src/mask.rs:2858`](../core/dr-pipeline/src/mask.rs#L2858), [`core/dr-pipeline/src/mask.rs:2877`](../core/dr-pipeline/src/mask.rs#L2877), [`core/dr-pipeline/src/mask.rs:2891`](../core/dr-pipeline/src/mask.rs#L2891), [`core/dr-pipeline/src/mask.rs:2901`](../core/dr-pipeline/src/mask.rs#L2901), [`core/dr-pipeline/src/mask.rs:710`](../core/dr-pipeline/src/mask.rs#L710), [`core/dr-pipeline/src/mask.rs:749`](../core/dr-pipeline/src/mask.rs#L749), [`core/dr-pipeline/src/mask.rs:809`](../core/dr-pipeline/src/mask.rs#L809), [`core/dr-pipeline/src/mask.rs:828`](../core/dr-pipeline/src/mask.rs#L828), [`core/dr-pipeline/src/mask.rs:852`](../core/dr-pipeline/src/mask.rs#L852), [`core/dr-pipeline/src/mask.rs:864`](../core/dr-pipeline/src/mask.rs#L864), [`core/dr-pipeline/src/mask.rs:876`](../core/dr-pipeline/src/mask.rs#L876), [`core/dr-pipeline/src/sidecar.rs:1336`](../core/dr-pipeline/src/sidecar.rs#L1336), [`core/dr-pipeline/src/sidecar.rs:1515`](../core/dr-pipeline/src/sidecar.rs#L1515), [`core/dr-pipeline/src/sidecar.rs:1604`](../core/dr-pipeline/src/sidecar.rs#L1604), [`core/dr-pipeline/src/sidecar.rs:1708`](../core/dr-pipeline/src/sidecar.rs#L1708), [`core/dr-pipeline/src/sidecar.rs:1916`](../core/dr-pipeline/src/sidecar.rs#L1916), [`core/dr-pipeline/tests/mask_sidecar.rs:1157`](../core/dr-pipeline/tests/mask_sidecar.rs#L1157), [`core/dr-pipeline/tests/mask_sidecar.rs:1186`](../core/dr-pipeline/tests/mask_sidecar.rs#L1186), [`core/dr-pipeline/tests/mask_sidecar.rs:1204`](../core/dr-pipeline/tests/mask_sidecar.rs#L1204), [`core/dr-pipeline/tests/mask_sidecar.rs:1234`](../core/dr-pipeline/tests/mask_sidecar.rs#L1234), [`ui/dr-ui/src/develop.rs:2318`](../ui/dr-ui/src/develop.rs#L2318), [`ui/dr-ui/src/develop.rs:3666`](../ui/dr-ui/src/develop.rs#L3666), [`ui/dr-ui/src/develop.rs:3693`](../ui/dr-ui/src/develop.rs#L3693), [`ui/dr-ui/src/develop.rs:3725`](../ui/dr-ui/src/develop.rs#L3725), [`ui/dr-ui/src/develop.rs:3749`](../ui/dr-ui/src/develop.rs#L3749), [`ui/dr-ui/src/develop.rs:3755`](../ui/dr-ui/src/develop.rs#L3755), [`ui/dr-ui/src/develop.rs:3762`](../ui/dr-ui/src/develop.rs#L3762), [`ui/dr-ui/src/develop.rs:3782`](../ui/dr-ui/src/develop.rs#L3782), [`ui/dr-ui/src/labels.rs:63`](../ui/dr-ui/src/labels.rs#L63), [`ui/dr-ui/src/masks_ui.rs:1150`](../ui/dr-ui/src/masks_ui.rs#L1150), [`ui/dr-ui/src/masks_ui.rs:748`](../ui/dr-ui/src/masks_ui.rs#L748) |
|
||||
@@ -164,7 +164,7 @@ A tag naming an ID `requirements.md` does not define — what renumbering produc
|
||||
| NFR-P14 | [`core/dr-gpu/src/focus.rs:186`](../core/dr-gpu/src/focus.rs#L186), [`core/dr-gpu/src/focus.rs:1`](../core/dr-gpu/src/focus.rs#L1), [`core/dr-gpu/src/focus.rs:317`](../core/dr-gpu/src/focus.rs#L317), [`core/dr-gpu/src/shaders/focus_peak.wgsl:1`](../core/dr-gpu/src/shaders/focus_peak.wgsl#L1), [`ui/dr-ui/src/develop.rs:4238`](../ui/dr-ui/src/develop.rs#L4238), [`ui/dr-ui/src/lib.rs:2153`](../ui/dr-ui/src/lib.rs#L2153) |
|
||||
| NFR-P3 | [`tools/bench/src/thumbnails.rs:1`](../tools/bench/src/thumbnails.rs#L1) |
|
||||
| NFR-P5 | [`core/dr-catalog/src/schema.rs:564`](../core/dr-catalog/src/schema.rs#L564), [`ui/dr-ui/src/library.rs:4815`](../ui/dr-ui/src/library.rs#L4815), [`ui/dr-ui/src/library.rs:5978`](../ui/dr-ui/src/library.rs#L5978), [`ui/dr-ui/src/library_ui.rs:5005`](../ui/dr-ui/src/library_ui.rs#L5005), [`ui/dr-ui/src/library_ui.rs:64`](../ui/dr-ui/src/library_ui.rs#L64) |
|
||||
| NFR-P9 | [`core/dr-catalog/src/schema.rs:917`](../core/dr-catalog/src/schema.rs#L917), [`ui/dr-ui/src/collections_ui.rs:1`](../ui/dr-ui/src/collections_ui.rs#L1), [`ui/dr-ui/src/export.rs:1005`](../ui/dr-ui/src/export.rs#L1005), [`ui/dr-ui/src/library.rs:1`](../ui/dr-ui/src/library.rs#L1), [`ui/dr-ui/src/library_ui.rs:1`](../ui/dr-ui/src/library_ui.rs#L1), [`ui/dr-ui/src/trash.rs:1`](../ui/dr-ui/src/trash.rs#L1) |
|
||||
| NFR-P9 | [`core/dr-catalog/src/schema.rs:917`](../core/dr-catalog/src/schema.rs#L917), [`core/dr-face/src/references.rs:1`](../core/dr-face/src/references.rs#L1), [`ui/dr-ui/src/collections_ui.rs:1`](../ui/dr-ui/src/collections_ui.rs#L1), [`ui/dr-ui/src/export.rs:1005`](../ui/dr-ui/src/export.rs#L1005), [`ui/dr-ui/src/library.rs:1`](../ui/dr-ui/src/library.rs#L1), [`ui/dr-ui/src/library_ui.rs:1`](../ui/dr-ui/src/library_ui.rs#L1), [`ui/dr-ui/src/trash.rs:1`](../ui/dr-ui/src/trash.rs#L1) |
|
||||
| NFR-PORT-1 | [`core/dr-catalog/src/walk.rs:1`](../core/dr-catalog/src/walk.rs#L1), [`core/dr-types/src/lib.rs:272`](../core/dr-types/src/lib.rs#L272), [`core/dr-types/src/lib.rs:305`](../core/dr-types/src/lib.rs#L305), [`platform/dr-plat/src/dirs.rs:1`](../platform/dr-plat/src/dirs.rs#L1), [`platform/dr-plat/src/display.rs:1`](../platform/dr-plat/src/display.rs#L1), [`platform/dr-plat/src/storage.rs:1`](../platform/dr-plat/src/storage.rs#L1), [`platform/dr-plat/src/storage.rs:216`](../platform/dr-plat/src/storage.rs#L216), [`platform/dr-plat/src/storage.rs:287`](../platform/dr-plat/src/storage.rs#L287), [`platform/dr-plat/src/storage.rs:344`](../platform/dr-plat/src/storage.rs#L344), [`platform/dr-plat/src/volumes.rs:1`](../platform/dr-plat/src/volumes.rs#L1), [`platform/dr-plat/src/volumes.rs:62`](../platform/dr-plat/src/volumes.rs#L62) |
|
||||
| NFR-PORT-2 | [`core/dr-gpu/src/lib.rs:1`](../core/dr-gpu/src/lib.rs#L1) |
|
||||
| NFR-PORT-3 | [`platform/dr-plat/src/storage.rs:1`](../platform/dr-plat/src/storage.rs#L1) |
|
||||
|
||||
@@ -1069,6 +1069,79 @@ impl Population {
|
||||
ids.push(f.face);
|
||||
}
|
||||
|
||||
// Not every anchored face enters the scan: each person is stood for
|
||||
// by their references (`dr_face::references`), and the rest of their
|
||||
// faces stay out of the comparison. They keep their place -- they are
|
||||
// in `anchors`, so the pass never releases them -- and they are the
|
||||
// cost that would otherwise grow with every name the user gives.
|
||||
let mut by_person: std::collections::BTreeMap<u64, Vec<usize>> =
|
||||
std::collections::BTreeMap::new();
|
||||
for (i, c) in candidates.iter().enumerate() {
|
||||
if let Some(p) = c.confirmed_person {
|
||||
by_person.entry(p).or_default().push(i);
|
||||
}
|
||||
}
|
||||
let mut keep = vec![true; candidates.len()];
|
||||
let mut anchored = 0usize;
|
||||
for members in by_person.values() {
|
||||
anchored += members.len();
|
||||
// Only a person over the cap loses anyone: under it, `select`
|
||||
// returns every eligible face, and the ineligible are kept too,
|
||||
// since a short vector was a probe before and still is.
|
||||
if members.len() <= dr_face::references::MAX_REFERENCES {
|
||||
continue;
|
||||
}
|
||||
let embeddings: Vec<&[f32]> = members
|
||||
.iter()
|
||||
.map(|&i| candidates[i].embedding.as_slice())
|
||||
.collect();
|
||||
let quality: Vec<Option<f32>> =
|
||||
members.iter().map(|&i| candidates[i].quality).collect();
|
||||
let chosen = dr_face::references::select(
|
||||
&embeddings,
|
||||
&quality,
|
||||
dr_face::references::MAX_REFERENCES,
|
||||
);
|
||||
for &i in members {
|
||||
keep[i] = false;
|
||||
}
|
||||
for &k in &chosen {
|
||||
keep[members[k]] = true;
|
||||
}
|
||||
// A person none of whose faces is long enough to vouch is still
|
||||
// a person, and a pass they had no anchor in would file their
|
||||
// next face as a stranger. The longest stand in.
|
||||
if chosen.is_empty() {
|
||||
let mut by_quality = members.clone();
|
||||
by_quality.sort_by(|&a, &b| {
|
||||
let qa = candidates[a].quality.unwrap_or(0.0);
|
||||
let qb = candidates[b].quality.unwrap_or(0.0);
|
||||
qb.total_cmp(&qa).then(a.cmp(&b))
|
||||
});
|
||||
for &i in by_quality.iter().take(dr_face::references::MAX_REFERENCES) {
|
||||
keep[i] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
let left_out = keep.iter().filter(|k| !**k).count();
|
||||
if left_out > 0 {
|
||||
let mut i = 0;
|
||||
candidates.retain(|_| {
|
||||
i += 1;
|
||||
keep[i - 1]
|
||||
});
|
||||
let mut i = 0;
|
||||
ids.retain(|_| {
|
||||
i += 1;
|
||||
keep[i - 1]
|
||||
});
|
||||
log::info!(
|
||||
"references: {} of {anchored} anchored face(s) stand for {} people; {left_out} left out of the scan",
|
||||
anchored - left_out,
|
||||
by_person.len(),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
cal,
|
||||
candidates,
|
||||
@@ -2121,6 +2194,45 @@ mod tests {
|
||||
assert_eq!(after[0].suggested_faces, 3);
|
||||
}
|
||||
|
||||
/// A person over the reference cap is stood for by a chosen few, and the
|
||||
/// rest of their faces stay where they are: not compared, not released,
|
||||
/// and still theirs when the next face arrives.
|
||||
#[test]
|
||||
fn a_well_covered_person_is_stood_for_by_their_references() {
|
||||
let many = dr_face::references::MAX_REFERENCES + 50;
|
||||
let catalog = catalog_with(many + 1);
|
||||
for i in 0..many {
|
||||
put_face(&catalog, i as u64 + 1, 0, 1.0 - (i as f32) * 1e-3);
|
||||
}
|
||||
recluster(&catalog, TEST_MODEL, &FaceSettings::default()).unwrap();
|
||||
let him = faces::people(catalog.connection()).unwrap()[0].id;
|
||||
faces::rename_person(catalog.connection(), him, "Ian").unwrap();
|
||||
|
||||
// Every one of his faces lies in one plane, so two references span
|
||||
// them all and the selector stops there rather than filling the cap.
|
||||
let pop = Population::read(&catalog, TEST_MODEL).unwrap();
|
||||
let standing = pop
|
||||
.candidates
|
||||
.iter()
|
||||
.filter(|c| c.confirmed_person.is_some())
|
||||
.count();
|
||||
assert!(
|
||||
(2..=dr_face::references::MAX_REFERENCES).contains(&standing),
|
||||
"{standing} of {many} faces entered the scan"
|
||||
);
|
||||
assert_eq!(pop.anchors.len(), many, "the rest lost their anchor");
|
||||
|
||||
put_face(&catalog, many as u64 + 1, 0, 0.98);
|
||||
recluster(&catalog, TEST_MODEL, &FaceSettings::default()).unwrap();
|
||||
let after = faces::people(catalog.connection()).unwrap();
|
||||
assert_eq!(after.len(), 1, "a second Ian appeared: {after:?}");
|
||||
assert_eq!(
|
||||
after[0].suggested_faces,
|
||||
(many + 1) as u64,
|
||||
"faces were released"
|
||||
);
|
||||
}
|
||||
|
||||
/// `dr-types` sits below the face engine and cannot name its constant, so
|
||||
/// it restates the number. This is the thing that stops the two drifting:
|
||||
/// a settings page marking 0.80 as "default" while the engine had moved to
|
||||
|
||||
Reference in New Issue
Block a user