diff --git a/ui/dr-ui/src/faces.rs b/ui/dr-ui/src/faces.rs index 606ce36..5294b5b 100644 --- a/ui/dr-ui/src/faces.rs +++ b/ui/dr-ui/src/faces.rs @@ -200,7 +200,10 @@ impl IndexAudit { s.push_str(&format!("; {outstanding} to index")); } if self.unmeasured > 0 { - s.push_str(&format!("; {} face(s) to measure", self.unmeasured)); + s.push_str(&format!( + "; {} face(s) to read for quality or eye state", + self.unmeasured + )); } s } @@ -284,6 +287,10 @@ pub fn audit( /// the models, which is the ordinary state of one that has not been given /// them, and `None` — logged — where the models refuse: a face the embedder /// could use is not lost for the want of an eye reading. +/// +/// Returns the reading and the dense landmarks it was read from, the +/// latter already packed for the catalog (`landmarks_dense`, normalised by +/// `long_edge` like the five points). fn read_eyes( models: Option<&mut EyeModels>, px: dr_face::Pixels<'_>, @@ -291,13 +298,17 @@ fn read_eyes( height: usize, bbox: (f32, f32, f32, f32), landmarks: &[(f32, f32); 5], -) -> Option { - let models = models?; + long_edge: f32, +) -> (Option, Vec) { + let Some(models) = models else { + return (None, Vec::new()); + }; match models.read(px, width, height, bbox, landmarks) { - Ok(r) => r, + Ok(Some((reading, dense))) => (Some(reading), dense.to_packed_bytes(long_edge)), + Ok(None) => (None, Vec::new()), Err(e) => { log::debug!("eye reading failed: {e}"); - None + (None, Vec::new()) } } } @@ -370,13 +381,14 @@ pub fn index_proxy( } let embedded = embedder.embed(&aligned)?; - let eyes = read_eyes( + let (eyes, landmarks_dense) = read_eyes( eye_models.as_deref_mut(), dr_face::Pixels::RgbF32(rgb), width, height, d.bbox, &d.landmarks, + long_edge, ); out.push(DetectedFace { @@ -390,6 +402,7 @@ pub fn index_proxy( crop_px: aligned.source_px(), quality: Some(embedded.quality), eyes, + landmarks_dense, 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. @@ -499,13 +512,14 @@ pub fn index_native( // From the native buffer, box and landmarks scaled like the crop's // — the eye is a fortieth of the face, and it is here that the // native render pays for itself twice. - let eyes = read_eyes( + let (eyes, landmarks_dense) = read_eyes( eye_models.as_deref_mut(), native, width, height, (bx, by, bx + bw, by + bh), &landmarks, + long_edge, ); out.push(DetectedFace { @@ -519,6 +533,7 @@ pub fn index_native( crop_px: aligned.source_px(), quality: Some(embedded.quality), eyes, + landmarks_dense, model_id: embedder.model().as_str().to_string(), crop: cut_crop_native(native, width, height, (bx, by, bw, bh)).unwrap_or_default(), }); @@ -582,8 +597,8 @@ pub fn measure_native( continue; }; let embedded = embedder.embed(&aligned)?; - let eyes = if f.eyes.is_some() { - None + let (eyes, landmarks_dense) = if f.eyes.is_some() { + (None, Vec::new()) } else { let bbox = ( f.x * long_edge, @@ -598,6 +613,7 @@ pub fn measure_native( height, bbox, &landmarks, + long_edge, ) }; out.measured.push(faces::Measurement { @@ -605,6 +621,7 @@ pub fn measure_native( embedding: embedded.to_f16_bytes(), quality: embedded.quality, eyes, + landmarks_dense, }); } Ok(out) @@ -1610,6 +1627,7 @@ mod tests { crop_px: 280.0, quality: None, eyes: None, + landmarks_dense: Vec::new(), model_id: "w600k_mbf".into(), person: None, probability: 0.0, @@ -1718,11 +1736,7 @@ mod tests { }; assert!(a.coverage.is_complete()); assert!(!a.is_complete()); - assert!( - a.summary().contains("4 face(s) to measure"), - "{}", - a.summary() - ); + assert!(a.summary().contains("4 face(s) to read"), "{}", a.summary()); } /// The figure the real library actually produced: 110 of 23,528 rounds to @@ -1807,6 +1821,7 @@ mod tests { crop_px: 120.0, quality: None, eyes: None, + landmarks_dense: Vec::new(), model_id: "w600k_mbf".into(), person: None, probability: 0.0, @@ -1926,6 +1941,7 @@ mod tests { crop_px: 150.0, quality: None, eyes: None, + landmarks_dense: Vec::new(), model_id: TEST_MODEL.to_string(), crop: Vec::new(), }; diff --git a/ui/dr-ui/src/identity.rs b/ui/dr-ui/src/identity.rs index 08abf3a..d862bf3 100644 --- a/ui/dr-ui/src/identity.rs +++ b/ui/dr-ui/src/identity.rs @@ -808,6 +808,7 @@ mod tests { crop_px: 180.0, quality: Some(f32::from(seed) + 10.0), eyes: None, + landmarks_dense: Vec::new(), model_id: "w600k_mbf".into(), crop: Vec::new(), } diff --git a/ui/dr-ui/src/identity_ui.rs b/ui/dr-ui/src/identity_ui.rs index b1cafec..2afdc70 100644 --- a/ui/dr-ui/src/identity_ui.rs +++ b/ui/dr-ui/src/identity_ui.rs @@ -402,6 +402,9 @@ pub fn refresh_coverage( // image has a face": most of a library has none, and that is a // finding. window.set_identity_coverage_complete(a.is_complete()); + // Detection done, readings outstanding: the button names the + // pass it will run rather than promising to index. + window.set_identity_coverage_read_only(a.coverage.is_complete() && a.unmeasured > 0); } Err(e) => { log::warn!("identity: coverage check: {e}"); diff --git a/ui/dr-ui/src/library.rs b/ui/dr-ui/src/library.rs index 1e5450a..755579f 100644 --- a/ui/dr-ui/src/library.rs +++ b/ui/dr-ui/src/library.rs @@ -6926,6 +6926,7 @@ mod tests { crop_px: 120.0, quality: None, eyes: None, + landmarks_dense: Vec::new(), crop: Vec::new(), model_id: "w600k_mbf".into(), }; @@ -6977,6 +6978,7 @@ mod tests { crop_px: 120.0, quality: None, eyes: None, + landmarks_dense: Vec::new(), crop: Vec::new(), model_id: "w600k_mbf".into(), }; @@ -7134,6 +7136,7 @@ mod tests { crop_px: 120.0, quality, eyes: None, + landmarks_dense: Vec::new(), crop: Vec::new(), model_id: "w600k_mbf".into(), }; @@ -8131,6 +8134,7 @@ mod tests { crop_px: 120.0, quality: None, eyes: None, + landmarks_dense: Vec::new(), crop: Vec::new(), model_id: "w600k_mbf".into(), }; diff --git a/ui/dr-ui/ui/app.slint b/ui/dr-ui/ui/app.slint index 88d993e..bae9790 100644 --- a/ui/dr-ui/ui/app.slint +++ b/ui/dr-ui/ui/app.slint @@ -406,6 +406,7 @@ export component AppWindow inherits Window { in property identity-back-label: "‹ Library"; in property identity-coverage; in property identity-coverage-complete: false; + in property identity-coverage-read-only: false; in property identity-picked: 0; /// The namesake merge the Identity screen is offering. See IdentityScreen. in property identity-merge-offer-name; @@ -1419,6 +1420,7 @@ in property panel-visible: true; back-label: root.identity-back-label; coverage: root.identity-coverage; coverage-complete: root.identity-coverage-complete; + coverage-read-only: root.identity-coverage-read-only; merge-offer-name: root.identity-merge-offer-name; merge-offer-faces: root.identity-merge-offer-faces; name-revision: root.identity-name-revision; diff --git a/ui/dr-ui/ui/identity.slint b/ui/dr-ui/ui/identity.slint index afc51eb..85d380e 100644 --- a/ui/dr-ui/ui/identity.slint +++ b/ui/dr-ui/ui/identity.slint @@ -270,6 +270,12 @@ export component IdentityScreen inherits Rectangle { // hides the reason the rest are outstanding. in property coverage; in property coverage-complete: false; + /// TRACES: FR-CULL-8a + /// Every image has been through the detector and only faces are left to + /// read — the eye pass over a library indexed before the eye models + /// existed. The same sweep, fetching the same originals; the button says + /// what it will actually do. + in property coverage-read-only: false; // No model on disk: face indexing cannot run at all (docs/faces.md §2.2). in property model-missing: false; in property picked-count: 0; @@ -616,7 +622,7 @@ export component IdentityScreen inherits Rectangle { clicked => { root.confirm-all(); } } if !root.indexing && !root.model-missing && !root.coverage-complete: Button { - text: "Index faces"; + text: root.coverage-read-only ? "Read eye state" : "Index faces"; clicked => { root.index-faces(); } } if root.indexing: Button {