Store seven eye numbers per face rather than three

Per eye P(open), the pixels across its box and the sharpness of the
patch; and P(sunglasses). The verdict — open, closed, sunglasses,
unclear — stays a rule in dr_face::eyes so the floors can move without
re-measuring twenty thousand faces. Shards carry the same seven, and a
peer's shard from before any of them is still read.
This commit is contained in:
2026-09-19 14:04:08 +02:00
parent f5956707e7
commit 54b543fb77
3 changed files with 134 additions and 58 deletions
+76 -32
View File
@@ -58,7 +58,7 @@ use rusqlite::{Connection, OptionalExtension};
use dr_types::ImageId;
use crate::error::CatalogError;
use dr_face::EyeReading;
use dr_face::{Eye, EyeReading};
/// The embedder half of a model id: what makes two faces comparable.
///
@@ -266,9 +266,10 @@ pub fn record_detections(
"INSERT INTO faces
(image_id, x, y, w, h, landmarks, detector_confidence,
embedding, crop_px, model_id, detected_at, crop, quality,
eye_right, eye_left, sunglasses)
eye_right, eye_right_px, eye_right_sharp,
eye_left, eye_left_px, eye_left_sharp, sunglasses)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13,
?14, ?15, ?16)",
?14, ?15, ?16, ?17, ?18, ?19, ?20)",
rusqlite::params![
image_id.0 as i64,
f.x as f64,
@@ -286,8 +287,12 @@ 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.right.open)),
f.eyes.map(|e| f64::from(e.right.px)),
f.eyes.map(|e| f64::from(e.right.sharpness)),
f.eyes.map(|e| f64::from(e.left.open)),
f.eyes.map(|e| f64::from(e.left.px)),
f.eyes.map(|e| f64::from(e.left.sharpness)),
f.eyes.map(|e| f64::from(e.sunglasses)),
],
)?;
@@ -389,12 +394,20 @@ pub fn record_measurements(
// 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",
"UPDATE faces
SET eye_right = ?2, eye_right_px = ?3, eye_right_sharp = ?4,
eye_left = ?5, eye_left_px = ?6, eye_left_sharp = ?7,
sunglasses = ?8
WHERE id = ?1",
rusqlite::params![
m.face.0 as i64,
f64::from(e.right_open),
f64::from(e.left_open),
f64::from(e.sunglasses)
f64::from(e.right.open),
f64::from(e.right.px),
f64::from(e.right.sharpness),
f64::from(e.left.open),
f64::from(e.left.px),
f64::from(e.left.sharpness),
f64::from(e.sunglasses),
],
)?;
}
@@ -605,7 +618,8 @@ pub fn for_image(conn: &Connection, image_id: ImageId) -> Result<Vec<Face>, Cata
"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,
f.eye_right, f.eye_left, f.sunglasses
f.eye_right, f.eye_right_px, f.eye_right_sharp,
f.eye_left, f.eye_left_px, f.eye_left_sharp, f.sunglasses
FROM faces f
LEFT JOIN face_person fp ON fp.face_id = f.id
WHERE f.image_id = ?1
@@ -899,7 +913,8 @@ pub fn for_person(
"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,
f.eye_right, f.eye_left, f.sunglasses
f.eye_right, f.eye_right_px, f.eye_right_sharp,
f.eye_left, f.eye_left_px, f.eye_left_sharp, 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)
@@ -1167,26 +1182,38 @@ fn read_face(r: &rusqlite::Row<'_>) -> rusqlite::Result<Face> {
})
}
/// The three eye columns at `first`, `first + 1`, `first + 2`, as one reading.
/// The seven eye columns at `first` .. `first + 6`, in [`EYE_COLUMNS`]'s
/// order, 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.
/// All seven 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.
///
/// [`EYE_COLUMNS`]: crate::schema::EYE_COLUMNS
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,
})
let mut v = [0.0_f32; 7];
for (i, slot) in v.iter_mut().enumerate() {
match r.get::<_, Option<f64>>(first + i)? {
Some(x) => *slot = x as f32,
None => return Ok(None),
}
}
Ok(Some(EyeReading {
right: Eye {
open: v[0],
px: v[1],
sharpness: v[2],
},
left: Eye {
open: v[3],
px: v[4],
sharpness: v[5],
},
sunglasses: v[6],
}))
}
/// The predicate "this face still needs measuring", over `faces` aliased as
@@ -1322,8 +1349,16 @@ mod tests {
crop_px: 180.0,
quality: Some(10.0 + f32::from(seed)),
eyes: Some(EyeReading {
right_open: 0.9,
left_open: 0.8,
right: Eye {
open: 0.9,
px: 40.0,
sharpness: 0.2,
},
left: Eye {
open: 0.8,
px: 38.0,
sharpness: 0.3,
},
sunglasses: 0.1,
}),
model_id: "w600k_mbf".into(),
@@ -1361,14 +1396,15 @@ mod tests {
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.left.open), Some(0.8));
assert_eq!(got[0].eyes.map(|e| e.right.sharpness), Some(0.2));
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
/// The seven 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() {
@@ -1423,8 +1459,16 @@ mod tests {
embedding: vec![9; 1024],
quality: 21.5,
eyes: Some(EyeReading {
right_open: 0.2,
left_open: 0.9,
right: Eye {
open: 0.2,
px: 40.0,
sharpness: 0.2,
},
left: Eye {
open: 0.9,
px: 40.0,
sharpness: 0.2,
},
sunglasses: 0.0,
}),
}],