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"));
|
s.push_str(&format!("; {outstanding} to index"));
|
||||||
}
|
}
|
||||||
if self.unmeasured > 0 {
|
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
|
s
|
||||||
}
|
}
|
||||||
@@ -284,6 +287,10 @@ pub fn audit(
|
|||||||
/// the models, which is the ordinary state of one that has not been given
|
/// 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
|
/// them, and `None` — logged — where the models refuse: a face the embedder
|
||||||
/// could use is not lost for the want of an eye reading.
|
/// 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(
|
fn read_eyes(
|
||||||
models: Option<&mut EyeModels>,
|
models: Option<&mut EyeModels>,
|
||||||
px: dr_face::Pixels<'_>,
|
px: dr_face::Pixels<'_>,
|
||||||
@@ -291,13 +298,17 @@ fn read_eyes(
|
|||||||
height: usize,
|
height: usize,
|
||||||
bbox: (f32, f32, f32, f32),
|
bbox: (f32, f32, f32, f32),
|
||||||
landmarks: &[(f32, f32); 5],
|
landmarks: &[(f32, f32); 5],
|
||||||
) -> Option<EyeReading> {
|
long_edge: f32,
|
||||||
let models = models?;
|
) -> (Option<EyeReading>, Vec<u8>) {
|
||||||
|
let Some(models) = models else {
|
||||||
|
return (None, Vec::new());
|
||||||
|
};
|
||||||
match models.read(px, width, height, bbox, landmarks) {
|
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) => {
|
Err(e) => {
|
||||||
log::debug!("eye reading failed: {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 embedded = embedder.embed(&aligned)?;
|
||||||
let eyes = read_eyes(
|
let (eyes, landmarks_dense) = read_eyes(
|
||||||
eye_models.as_deref_mut(),
|
eye_models.as_deref_mut(),
|
||||||
dr_face::Pixels::RgbF32(rgb),
|
dr_face::Pixels::RgbF32(rgb),
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
d.bbox,
|
d.bbox,
|
||||||
&d.landmarks,
|
&d.landmarks,
|
||||||
|
long_edge,
|
||||||
);
|
);
|
||||||
|
|
||||||
out.push(DetectedFace {
|
out.push(DetectedFace {
|
||||||
@@ -390,6 +402,7 @@ pub fn index_proxy(
|
|||||||
crop_px: aligned.source_px(),
|
crop_px: aligned.source_px(),
|
||||||
quality: Some(embedded.quality),
|
quality: Some(embedded.quality),
|
||||||
eyes,
|
eyes,
|
||||||
|
landmarks_dense,
|
||||||
model_id: embedder.model().as_str().to_string(),
|
model_id: embedder.model().as_str().to_string(),
|
||||||
// Cut here, while the buffer is still in hand. This is the only
|
// Cut here, while the buffer is still in hand. This is the only
|
||||||
// moment in the whole pipeline where the pixels are free.
|
// 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
|
// 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
|
// — the eye is a fortieth of the face, and it is here that the
|
||||||
// native render pays for itself twice.
|
// native render pays for itself twice.
|
||||||
let eyes = read_eyes(
|
let (eyes, landmarks_dense) = read_eyes(
|
||||||
eye_models.as_deref_mut(),
|
eye_models.as_deref_mut(),
|
||||||
native,
|
native,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
(bx, by, bx + bw, by + bh),
|
(bx, by, bx + bw, by + bh),
|
||||||
&landmarks,
|
&landmarks,
|
||||||
|
long_edge,
|
||||||
);
|
);
|
||||||
|
|
||||||
out.push(DetectedFace {
|
out.push(DetectedFace {
|
||||||
@@ -519,6 +533,7 @@ pub fn index_native(
|
|||||||
crop_px: aligned.source_px(),
|
crop_px: aligned.source_px(),
|
||||||
quality: Some(embedded.quality),
|
quality: Some(embedded.quality),
|
||||||
eyes,
|
eyes,
|
||||||
|
landmarks_dense,
|
||||||
model_id: embedder.model().as_str().to_string(),
|
model_id: embedder.model().as_str().to_string(),
|
||||||
crop: cut_crop_native(native, width, height, (bx, by, bw, bh)).unwrap_or_default(),
|
crop: cut_crop_native(native, width, height, (bx, by, bw, bh)).unwrap_or_default(),
|
||||||
});
|
});
|
||||||
@@ -582,8 +597,8 @@ pub fn measure_native(
|
|||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let embedded = embedder.embed(&aligned)?;
|
let embedded = embedder.embed(&aligned)?;
|
||||||
let eyes = if f.eyes.is_some() {
|
let (eyes, landmarks_dense) = if f.eyes.is_some() {
|
||||||
None
|
(None, Vec::new())
|
||||||
} else {
|
} else {
|
||||||
let bbox = (
|
let bbox = (
|
||||||
f.x * long_edge,
|
f.x * long_edge,
|
||||||
@@ -598,6 +613,7 @@ pub fn measure_native(
|
|||||||
height,
|
height,
|
||||||
bbox,
|
bbox,
|
||||||
&landmarks,
|
&landmarks,
|
||||||
|
long_edge,
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
out.measured.push(faces::Measurement {
|
out.measured.push(faces::Measurement {
|
||||||
@@ -605,6 +621,7 @@ pub fn measure_native(
|
|||||||
embedding: embedded.to_f16_bytes(),
|
embedding: embedded.to_f16_bytes(),
|
||||||
quality: embedded.quality,
|
quality: embedded.quality,
|
||||||
eyes,
|
eyes,
|
||||||
|
landmarks_dense,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Ok(out)
|
Ok(out)
|
||||||
@@ -1610,6 +1627,7 @@ mod tests {
|
|||||||
crop_px: 280.0,
|
crop_px: 280.0,
|
||||||
quality: None,
|
quality: None,
|
||||||
eyes: None,
|
eyes: None,
|
||||||
|
landmarks_dense: Vec::new(),
|
||||||
model_id: "w600k_mbf".into(),
|
model_id: "w600k_mbf".into(),
|
||||||
person: None,
|
person: None,
|
||||||
probability: 0.0,
|
probability: 0.0,
|
||||||
@@ -1718,11 +1736,7 @@ mod tests {
|
|||||||
};
|
};
|
||||||
assert!(a.coverage.is_complete());
|
assert!(a.coverage.is_complete());
|
||||||
assert!(!a.is_complete());
|
assert!(!a.is_complete());
|
||||||
assert!(
|
assert!(a.summary().contains("4 face(s) to read"), "{}", a.summary());
|
||||||
a.summary().contains("4 face(s) to measure"),
|
|
||||||
"{}",
|
|
||||||
a.summary()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The figure the real library actually produced: 110 of 23,528 rounds to
|
/// The figure the real library actually produced: 110 of 23,528 rounds to
|
||||||
@@ -1807,6 +1821,7 @@ mod tests {
|
|||||||
crop_px: 120.0,
|
crop_px: 120.0,
|
||||||
quality: None,
|
quality: None,
|
||||||
eyes: None,
|
eyes: None,
|
||||||
|
landmarks_dense: Vec::new(),
|
||||||
model_id: "w600k_mbf".into(),
|
model_id: "w600k_mbf".into(),
|
||||||
person: None,
|
person: None,
|
||||||
probability: 0.0,
|
probability: 0.0,
|
||||||
@@ -1926,6 +1941,7 @@ mod tests {
|
|||||||
crop_px: 150.0,
|
crop_px: 150.0,
|
||||||
quality: None,
|
quality: None,
|
||||||
eyes: None,
|
eyes: None,
|
||||||
|
landmarks_dense: Vec::new(),
|
||||||
model_id: TEST_MODEL.to_string(),
|
model_id: TEST_MODEL.to_string(),
|
||||||
crop: Vec::new(),
|
crop: Vec::new(),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -808,6 +808,7 @@ mod tests {
|
|||||||
crop_px: 180.0,
|
crop_px: 180.0,
|
||||||
quality: Some(f32::from(seed) + 10.0),
|
quality: Some(f32::from(seed) + 10.0),
|
||||||
eyes: None,
|
eyes: None,
|
||||||
|
landmarks_dense: Vec::new(),
|
||||||
model_id: "w600k_mbf".into(),
|
model_id: "w600k_mbf".into(),
|
||||||
crop: Vec::new(),
|
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
|
// image has a face": most of a library has none, and that is a
|
||||||
// finding.
|
// finding.
|
||||||
window.set_identity_coverage_complete(a.is_complete());
|
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) => {
|
Err(e) => {
|
||||||
log::warn!("identity: coverage check: {e}");
|
log::warn!("identity: coverage check: {e}");
|
||||||
|
|||||||
@@ -6926,6 +6926,7 @@ mod tests {
|
|||||||
crop_px: 120.0,
|
crop_px: 120.0,
|
||||||
quality: None,
|
quality: None,
|
||||||
eyes: None,
|
eyes: None,
|
||||||
|
landmarks_dense: Vec::new(),
|
||||||
crop: Vec::new(),
|
crop: Vec::new(),
|
||||||
model_id: "w600k_mbf".into(),
|
model_id: "w600k_mbf".into(),
|
||||||
};
|
};
|
||||||
@@ -6977,6 +6978,7 @@ mod tests {
|
|||||||
crop_px: 120.0,
|
crop_px: 120.0,
|
||||||
quality: None,
|
quality: None,
|
||||||
eyes: None,
|
eyes: None,
|
||||||
|
landmarks_dense: Vec::new(),
|
||||||
crop: Vec::new(),
|
crop: Vec::new(),
|
||||||
model_id: "w600k_mbf".into(),
|
model_id: "w600k_mbf".into(),
|
||||||
};
|
};
|
||||||
@@ -7134,6 +7136,7 @@ mod tests {
|
|||||||
crop_px: 120.0,
|
crop_px: 120.0,
|
||||||
quality,
|
quality,
|
||||||
eyes: None,
|
eyes: None,
|
||||||
|
landmarks_dense: Vec::new(),
|
||||||
crop: Vec::new(),
|
crop: Vec::new(),
|
||||||
model_id: "w600k_mbf".into(),
|
model_id: "w600k_mbf".into(),
|
||||||
};
|
};
|
||||||
@@ -8131,6 +8134,7 @@ mod tests {
|
|||||||
crop_px: 120.0,
|
crop_px: 120.0,
|
||||||
quality: None,
|
quality: None,
|
||||||
eyes: None,
|
eyes: None,
|
||||||
|
landmarks_dense: Vec::new(),
|
||||||
crop: Vec::new(),
|
crop: Vec::new(),
|
||||||
model_id: "w600k_mbf".into(),
|
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-back-label: "‹ Library";
|
||||||
in property <string> identity-coverage;
|
in property <string> identity-coverage;
|
||||||
in property <bool> identity-coverage-complete: false;
|
in property <bool> identity-coverage-complete: false;
|
||||||
|
in property <bool> identity-coverage-read-only: false;
|
||||||
in property <int> identity-picked: 0;
|
in property <int> identity-picked: 0;
|
||||||
/// The namesake merge the Identity screen is offering. See IdentityScreen.
|
/// The namesake merge the Identity screen is offering. See IdentityScreen.
|
||||||
in property <string> identity-merge-offer-name;
|
in property <string> identity-merge-offer-name;
|
||||||
@@ -1419,6 +1420,7 @@ in property <bool> panel-visible: true;
|
|||||||
back-label: root.identity-back-label;
|
back-label: root.identity-back-label;
|
||||||
coverage: root.identity-coverage;
|
coverage: root.identity-coverage;
|
||||||
coverage-complete: root.identity-coverage-complete;
|
coverage-complete: root.identity-coverage-complete;
|
||||||
|
coverage-read-only: root.identity-coverage-read-only;
|
||||||
merge-offer-name: root.identity-merge-offer-name;
|
merge-offer-name: root.identity-merge-offer-name;
|
||||||
merge-offer-faces: root.identity-merge-offer-faces;
|
merge-offer-faces: root.identity-merge-offer-faces;
|
||||||
name-revision: root.identity-name-revision;
|
name-revision: root.identity-name-revision;
|
||||||
|
|||||||
@@ -270,6 +270,12 @@ export component IdentityScreen inherits Rectangle {
|
|||||||
// hides the reason the rest are outstanding.
|
// hides the reason the rest are outstanding.
|
||||||
in property <string> coverage;
|
in property <string> coverage;
|
||||||
in property <bool> coverage-complete: false;
|
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).
|
// No model on disk: face indexing cannot run at all (docs/faces.md §2.2).
|
||||||
in property <bool> model-missing: false;
|
in property <bool> model-missing: false;
|
||||||
in property <int> picked-count: 0;
|
in property <int> picked-count: 0;
|
||||||
@@ -616,7 +622,7 @@ export component IdentityScreen inherits Rectangle {
|
|||||||
clicked => { root.confirm-all(); }
|
clicked => { root.confirm-all(); }
|
||||||
}
|
}
|
||||||
if !root.indexing && !root.model-missing && !root.coverage-complete: Button {
|
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(); }
|
clicked => { root.index-faces(); }
|
||||||
}
|
}
|
||||||
if root.indexing: Button {
|
if root.indexing: Button {
|
||||||
|
|||||||
Reference in New Issue
Block a user