Keep each face's eye reading in the catalog and in its shard

Three nullable columns beside quality — P(open) for each eye and
P(sunglasses) — because the verdict is a rule with thresholds in it and a
rule belongs in code, not in rows that would have to be re-measured. NULL
is "never read": a face from before the models, or from a device without
them, and every reader treats it as unknown rather than as closed.

The measuring pass V14 built for the embedding's length is what fills
them, so the sweep's work list now also names faces with no eye reading
— but only on a device that has the models, or it would fetch every
original to do nothing to it. A peer's shard without the reading is still
adopted, unlike one without the quality: the pass finds this work by the
NULL rather than by the run marker, so adoption costs it nothing.
This commit is contained in:
2026-09-19 14:04:06 +02:00
parent 6b51726322
commit b908d861e0
4 changed files with 328 additions and 44 deletions
+189 -14
View File
@@ -58,6 +58,7 @@ use rusqlite::{Connection, OptionalExtension};
use dr_types::ImageId;
use crate::error::CatalogError;
use dr_face::EyeReading;
/// The embedder half of a model id: what makes two faces comparable.
///
@@ -120,6 +121,12 @@ pub struct DetectedFace {
/// no length left to read, so the only way to measure one is to embed it
/// again (schema V14).
pub quality: Option<f32>,
/// TRACES: FR-CULL-13
/// What the eyes are doing — P(open) per eye and P(sunglasses), read by
/// `dr_face::classify` from the same aligned crop. `None` where the eye
/// models were not present at indexing, or the face came from a peer
/// that had none; the measuring pass fills it in (schema V16).
pub eyes: Option<EyeReading>,
/// Which model produced the embedding. Comparing across models is the one
/// mistake that yields plausible garbage rather than an error.
pub model_id: String,
@@ -152,6 +159,8 @@ pub struct Face {
/// See [`DetectedFace::quality`]. `None` for a face indexed before it was
/// recorded.
pub quality: Option<f32>,
/// See [`DetectedFace::eyes`]. `None` for a face never read.
pub eyes: Option<EyeReading>,
pub model_id: String,
/// `None` when the face belongs to no one yet.
pub person: Option<PersonId>,
@@ -256,8 +265,10 @@ pub fn record_detections(
tx.execute(
"INSERT INTO faces
(image_id, x, y, w, h, landmarks, detector_confidence,
embedding, crop_px, model_id, detected_at, crop, quality)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)",
embedding, crop_px, model_id, detected_at, crop, quality,
eye_right, eye_left, sunglasses)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13,
?14, ?15, ?16)",
rusqlite::params![
image_id.0 as i64,
f.x as f64,
@@ -275,6 +286,9 @@ pub fn record_detections(
// about.
(!f.crop.is_empty()).then_some(f.crop.as_slice()),
f.quality.map(f64::from),
f.eyes.map(|e| f64::from(e.right_open)),
f.eyes.map(|e| f64::from(e.left_open)),
f.eyes.map(|e| f64::from(e.sunglasses)),
],
)?;
let id = FaceId(tx.last_insert_rowid() as u64);
@@ -332,6 +346,9 @@ pub struct Measurement {
/// 512 × f16, raw — `dr_face::Embedded::to_f16_bytes`.
pub embedding: Vec<u8>,
pub quality: f32,
/// See [`DetectedFace::eyes`]. `None` where the measuring device has no
/// eye models, in which case the stored reading is left as it was.
pub eyes: Option<EyeReading>,
}
/// Write fresh embeddings over faces that were found before their quality was
@@ -368,6 +385,19 @@ pub fn record_measurements(
"UPDATE faces SET embedding = ?2, quality = ?3 WHERE id = ?1",
rusqlite::params![m.face.0 as i64, m.embedding, f64::from(m.quality)],
)?;
// Written only when read: a device without the eye models measuring
// a face a peer had already read must not blank the reading.
if let Some(e) = m.eyes {
tx.execute(
"UPDATE faces SET eye_right = ?2, eye_left = ?3, sunglasses = ?4 WHERE id = ?1",
rusqlite::params![
m.face.0 as i64,
f64::from(e.right_open),
f64::from(e.left_open),
f64::from(e.sunglasses)
],
)?;
}
}
for f in dropped {
tx.execute("DELETE FROM faces WHERE id = ?1", [f.0 as i64])?;
@@ -399,30 +429,45 @@ pub fn record_measurements(
Ok(())
}
/// The faces on one image that have no quality reading yet.
/// The faces on one image that have no quality reading yet — or, when the
/// device can read eyes, no eye reading either.
///
/// The measuring pass's per-image work: every face this model found whose
/// vector was stored as a unit one (schema V14), with the landmarks the
/// warp is rebuilt from.
/// warp is rebuilt from; and, with `eyes`, every face never shown to the eye
/// models (schema V16). `eyes` is whether this device *has* those models —
/// a device without them must not list faces it cannot measure, or the pass
/// would fetch every original in the library to do nothing to it.
pub fn unmeasured_on_image(
conn: &Connection,
image_id: ImageId,
model_id: &str,
eyes: bool,
) -> Result<Vec<Face>, CatalogError> {
Ok(for_image(conn, image_id)?
.into_iter()
.filter(|f| embedder_of(&f.model_id) == embedder_of(model_id) && f.quality.is_none())
.filter(|f| {
embedder_of(&f.model_id) == embedder_of(model_id)
&& (f.quality.is_none() || (eyes && f.eyes.is_none()))
})
.collect())
}
/// How many of a model's faces have no quality reading.
/// How many of a model's faces have no quality reading, or — with `eyes` —
/// no eye reading.
///
/// What the measuring pass has left to do, for a screen that wants to say so.
pub fn faces_unmeasured(conn: &Connection, model_id: &str) -> Result<u64, CatalogError> {
/// `eyes` means what it means in [`unmeasured_on_image`].
pub fn faces_unmeasured(
conn: &Connection,
model_id: &str,
eyes: bool,
) -> Result<u64, CatalogError> {
conn.query_row(
&format!(
"SELECT COUNT(*) FROM faces WHERE {} = ?1 AND quality IS NULL",
embedder_sql("model_id")
"SELECT COUNT(*) FROM faces WHERE {} = ?1 AND {}",
embedder_sql("model_id"),
unmeasured_sql("", eyes)
),
[embedder_of(model_id)],
|r| r.get::<_, i64>(0),
@@ -559,7 +604,8 @@ pub fn for_image(conn: &Connection, image_id: ImageId) -> Result<Vec<Face>, Cata
let mut q = conn.prepare(
"SELECT f.id, f.image_id, f.x, f.y, f.w, f.h, f.landmarks,
f.detector_confidence, f.crop_px, f.model_id,
fp.person_id, fp.probability, fp.confirmed, f.quality
fp.person_id, fp.probability, fp.confirmed, f.quality,
f.eye_right, f.eye_left, f.sunglasses
FROM faces f
LEFT JOIN face_person fp ON fp.face_id = f.id
WHERE f.image_id = ?1
@@ -852,7 +898,8 @@ pub fn for_person(
let mut q = conn.prepare(
"SELECT f.id, f.image_id, f.x, f.y, f.w, f.h, f.landmarks,
f.detector_confidence, f.crop_px, f.model_id,
fp.person_id, fp.probability, fp.confirmed, f.quality
fp.person_id, fp.probability, fp.confirmed, f.quality,
f.eye_right, f.eye_left, f.sunglasses
FROM faces f
JOIN face_person fp ON fp.face_id = f.id
WHERE fp.person_id = ?1 AND (?2 OR fp.confirmed = 1)
@@ -1112,6 +1159,7 @@ fn read_face(r: &rusqlite::Row<'_>) -> rusqlite::Result<Face> {
confidence: r.get::<_, f64>(7)? as f32,
crop_px: r.get::<_, f64>(8)? as f32,
quality: r.get::<_, Option<f64>>(13)?.map(|q| q as f32),
eyes: read_eyes(r, 14)?,
model_id: r.get(9)?,
person: person.map(|p| PersonId(p as u64)),
probability: r.get::<_, Option<f64>>(11)?.unwrap_or(0.0) as f32,
@@ -1119,6 +1167,44 @@ fn read_face(r: &rusqlite::Row<'_>) -> rusqlite::Result<Face> {
})
}
/// The three eye columns at `first`, `first + 1`, `first + 2`, as one reading.
///
/// All three or none: they are written together, and a row with one of them
/// NULL is a row nothing in this crate produced. Read as absent rather than
/// invented, which is what a reader of a half-written row deserves.
pub(crate) fn read_eyes(
r: &rusqlite::Row<'_>,
first: usize,
) -> rusqlite::Result<Option<EyeReading>> {
let right: Option<f64> = r.get(first)?;
let left: Option<f64> = r.get(first + 1)?;
let sunglasses: Option<f64> = r.get(first + 2)?;
Ok(match (right, left, sunglasses) {
(Some(right), Some(left), Some(sunglasses)) => Some(EyeReading {
right_open: right as f32,
left_open: left as f32,
sunglasses: sunglasses as f32,
}),
_ => None,
})
}
/// The predicate "this face still needs measuring", over `faces` aliased as
/// `prefix` (`"f."` or `""`).
///
/// One place for it because two queries ask — the count above and the
/// sweep's work list in `dr_ui::library` — and the two agreeing is what
/// makes the pass converge: a face the count reports is a face the list
/// fetches, and a face the list fetches is one whose reading the write
/// fills, so it leaves both.
pub fn unmeasured_sql(prefix: &str, eyes: bool) -> String {
if eyes {
format!("({prefix}quality IS NULL OR {prefix}eye_right IS NULL)")
} else {
format!("{prefix}quality IS NULL")
}
}
fn landmarks_to_blob(lm: &[(f32, f32); 5]) -> Vec<u8> {
let mut out = Vec::with_capacity(40);
for &(x, y) in lm {
@@ -1235,6 +1321,11 @@ mod tests {
embedding: vec![seed; 1024],
crop_px: 180.0,
quality: Some(10.0 + f32::from(seed)),
eyes: Some(EyeReading {
right_open: 0.9,
left_open: 0.8,
sunglasses: 0.1,
}),
model_id: "w600k_mbf".into(),
crop: Vec::new(),
}
@@ -1269,6 +1360,84 @@ mod tests {
assert_eq!(stored.len(), 2);
assert_eq!(stored[0].quality, Some(11.0), "oldest first");
assert_eq!(stored[1].quality, Some(12.0));
// And the eyes, as one reading.
assert_eq!(got[0].eyes.map(|e| e.left_open), Some(0.8));
assert_eq!(
got[0].eyes.map(|e| e.state()),
Some(dr_face::EyeState::Open)
);
}
/// The three eye columns are one fact: a face with none of them reads as
/// unread, and the measuring pass is what fills them.
#[test]
fn eyes_are_measured_only_where_the_device_can_read_them() {
let c = db();
let img = image(&c, 1);
let unread = DetectedFace {
eyes: None,
..face(1)
};
let ids = record_detections(&c, img, "w600k_mbf", 1024, &[unread, face(2)]).unwrap();
assert_eq!(for_image(&c, img).unwrap().len(), 2);
// Quality is present on both, so a device without the eye models has
// nothing to do here; one with them has one face to read.
assert_eq!(faces_unmeasured(&c, "w600k_mbf", false).unwrap(), 0);
assert_eq!(faces_unmeasured(&c, "w600k_mbf", true).unwrap(), 1);
assert_eq!(
unmeasured_on_image(&c, img, "w600k_mbf", false)
.unwrap()
.len(),
0
);
let todo = unmeasured_on_image(&c, img, "w600k_mbf", true).unwrap();
assert_eq!(todo.len(), 1);
assert_eq!(todo[0].id, ids[0]);
// A measurement with no reading leaves the columns alone …
record_measurements(
&c,
img,
"w600k_mbf",
6000,
&[Measurement {
face: ids[0],
embedding: vec![9; 1024],
quality: 21.5,
eyes: None,
}],
&[],
)
.unwrap();
assert_eq!(faces_unmeasured(&c, "w600k_mbf", true).unwrap(), 1);
// … and one with a reading fills them.
record_measurements(
&c,
img,
"w600k_mbf",
6000,
&[Measurement {
face: ids[0],
embedding: vec![9; 1024],
quality: 21.5,
eyes: Some(EyeReading {
right_open: 0.2,
left_open: 0.9,
sunglasses: 0.0,
}),
}],
&[],
)
.unwrap();
assert_eq!(faces_unmeasured(&c, "w600k_mbf", true).unwrap(), 0);
let got = for_image(&c, img).unwrap();
let read = got.iter().find(|f| f.id == ids[0]).unwrap();
assert_eq!(
read.eyes.map(|e| e.state()),
Some(dr_face::EyeState::Closed)
);
}
/// The measuring pass writes over the vector and nothing else: the face
@@ -1291,8 +1460,13 @@ mod tests {
.unwrap();
let person = create_person(&c, "Anna").unwrap();
confirm(&c, ids[0], person).unwrap();
assert_eq!(faces_unmeasured(&c, "w600k_mbf").unwrap(), 2);
assert_eq!(unmeasured_on_image(&c, img, "w600k_mbf").unwrap().len(), 2);
assert_eq!(faces_unmeasured(&c, "w600k_mbf", false).unwrap(), 2);
assert_eq!(
unmeasured_on_image(&c, img, "w600k_mbf", false)
.unwrap()
.len(),
2
);
let marked_at: i64 = c
.query_row("SELECT indexed_at FROM face_index", [], |r| r.get(0))
.unwrap();
@@ -1308,6 +1482,7 @@ mod tests {
face: ids[0],
embedding: vec![9; 1024],
quality: 21.5,
eyes: None,
}],
&[ids[1]],
)
@@ -1321,7 +1496,7 @@ mod tests {
assert!(got[0].confirmed);
let e = embeddings(&c, "w600k_mbf").unwrap();
assert_eq!(e[0].embedding[0], 9);
assert_eq!(faces_unmeasured(&c, "w600k_mbf").unwrap(), 0);
assert_eq!(faces_unmeasured(&c, "w600k_mbf", false).unwrap(), 0);
// The marker says one face at the native edge, and is fresh — which
// is what makes the sync export it again.