Name the eye pass on the People screen
Once every image has been through the detector and only readings are left — the state an already-indexed library is in the day the eye models arrive — the button reads "Read eye state" rather than promising to index, and the coverage line says what the faces are waiting for.
This commit is contained in:
+30
-14
@@ -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<EyeReading> {
|
||||
let models = models?;
|
||||
long_edge: f32,
|
||||
) -> (Option<EyeReading>, Vec<u8>) {
|
||||
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(),
|
||||
};
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
|
||||
@@ -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}");
|
||||
|
||||
@@ -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(),
|
||||
};
|
||||
|
||||
@@ -406,6 +406,7 @@ export component AppWindow inherits Window {
|
||||
in property <string> identity-back-label: "‹ Library";
|
||||
in property <string> identity-coverage;
|
||||
in property <bool> identity-coverage-complete: false;
|
||||
in property <bool> identity-coverage-read-only: false;
|
||||
in property <int> identity-picked: 0;
|
||||
/// The namesake merge the Identity screen is offering. See IdentityScreen.
|
||||
in property <string> identity-merge-offer-name;
|
||||
@@ -1419,6 +1420,7 @@ in property <bool> 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;
|
||||
|
||||
@@ -270,6 +270,12 @@ export component IdentityScreen inherits Rectangle {
|
||||
// hides the reason the rest are outstanding.
|
||||
in property <string> coverage;
|
||||
in property <bool> 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 <bool> coverage-read-only: false;
|
||||
// No model on disk: face indexing cannot run at all (docs/faces.md §2.2).
|
||||
in property <bool> model-missing: false;
|
||||
in property <int> 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 {
|
||||
|
||||
Reference in New Issue
Block a user