Keep the dense landmarks behind each eye reading, packed

The 106 points the eye boxes were cut from, stored beside the reading as
16-bit fixed point over the frame: 424 bytes a face, a seventh of a pixel
on a 6000-pixel frame, where f16 at the same size would have been six.
Derived data like the embedding, kept for the same reason — it cost a
fetch and a model run, and the next per-face pass should run from the
catalog. Shards carry it; a peer's shard from before it is still read.
This commit is contained in:
2026-09-19 14:24:15 +02:00
parent 85cc2b1dcc
commit facb44cb55
10 changed files with 264 additions and 99 deletions
+11 -5
View File
@@ -39,7 +39,7 @@ use crate::align::{
SUNGLASSES_EDGE,
};
use crate::eyes::{Eye, EyeReading};
use crate::landmarks::Landmarker;
use crate::landmarks::{Landmarker, Landmarks};
use crate::{install_backend, FaceError, Pixels};
/// A loaded OCEC graph.
@@ -202,13 +202,18 @@ impl EyeModels {
})
}
/// Read one face's eyes.
/// Read one face's eyes, and hand back the dense landmarks it read them
/// from.
///
/// `bbox` is the detector's `(x0, y0, x1, y1)` and `landmarks5` its five
/// points, both in source pixels; the buffer is the one the aligned
/// crop was taken from, so an eye is read from the same pixels the
/// embedder saw the face in. `None` where nothing could be cut — a
/// degenerate box or landmarks — which the caller stores as "not read".
///
/// The landmarks come back because they cost a model run the caller will
/// not want to pay twice: stored beside the reading, a later pass over
/// faces — head pose, expression — has them without the original.
pub fn read(
&mut self,
px: Pixels<'_>,
@@ -216,7 +221,7 @@ impl EyeModels {
height: usize,
bbox: (f32, f32, f32, f32),
landmarks5: &[(f32, f32); 5],
) -> Result<Option<EyeReading>, FaceError> {
) -> Result<Option<(EyeReading, Landmarks)>, FaceError> {
let Some(lm) = self.landmarks.landmarks(px, width, height, bbox)? else {
return Ok(None);
};
@@ -249,10 +254,11 @@ impl EyeModels {
};
let right = eye(&lm.right_eye())?;
let left = eye(&lm.left_eye())?;
Ok(Some(EyeReading {
let reading = EyeReading {
right,
left,
sunglasses: self.sunglasses.classify(&head)?,
}))
};
Ok(Some((reading, lm)))
}
}
+76
View File
@@ -43,6 +43,13 @@ pub const POINTS: usize = 106;
/// The crop's edge as a multiple of the detector box's longer edge.
const CROP_SCALE: f32 = 1.5;
/// The span of the frame, in long-edge units, the packed form covers: a
/// quarter of the frame outside each edge.
pub const PACKED_RANGE: (f32, f32) = (-0.25, 1.25);
/// Bytes the packed form of one face's landmarks takes.
pub const PACKED_BYTES: usize = POINTS * 4;
/// Point indices of the subject's right eye's lid contour (image-left).
pub const RIGHT_EYE: [usize; 10] = [33, 34, 35, 36, 37, 38, 39, 40, 41, 42];
/// Point indices of the subject's left eye's lid contour (image-right).
@@ -55,6 +62,49 @@ pub struct Landmarks {
}
impl Landmarks {
/// Storage form: `106 × (x, y)` as little-endian **`u16` fixed point**
/// over the frame, 424 bytes.
///
/// Each coordinate is normalised by `long_edge` like the five points the
/// catalog already keeps, then mapped over [`PACKED_RANGE`] — a quarter
/// of the frame either side of it, because a landmark on a face at the
/// edge does land outside the image — onto 0..65535. That is 0.14 source
/// pixels on a 6000-pixel frame. `f16` would be the same size and worse:
/// its three significant figures near 1.0 are six pixels at that scale,
/// and the eye contour this is kept for is drawn to the pixel.
pub fn to_packed_bytes(&self, long_edge: f32) -> Vec<u8> {
let (lo, hi) = PACKED_RANGE;
let pack = |v: f32| -> [u8; 2] {
let t = ((v / long_edge - lo) / (hi - lo)).clamp(0.0, 1.0);
((t * 65535.0).round() as u16).to_le_bytes()
};
let mut out = Vec::with_capacity(POINTS * 4);
for &(x, y) in &self.points {
out.extend_from_slice(&pack(x));
out.extend_from_slice(&pack(y));
}
out
}
/// [`Self::to_packed_bytes`] read back, into source pixels of a frame
/// with this `long_edge`. `None` for a blob of the wrong length.
pub fn from_packed_bytes(bytes: &[u8], long_edge: f32) -> Option<Self> {
if bytes.len() != POINTS * 4 {
return None;
}
let (lo, hi) = PACKED_RANGE;
let unpack = |b: &[u8]| -> f32 {
let t = u16::from_le_bytes([b[0], b[1]]) as f32 / 65535.0;
(t * (hi - lo) + lo) * long_edge
};
let mut points = [(0.0_f32, 0.0_f32); POINTS];
for (i, p) in points.iter_mut().enumerate() {
let at = i * 4;
*p = (unpack(&bytes[at..at + 2]), unpack(&bytes[at + 2..at + 4]));
}
Some(Self { points })
}
/// The lid contour of the subject's right eye.
pub fn right_eye(&self) -> [(f32, f32); 10] {
RIGHT_EYE.map(|i| self.points[i])
@@ -183,3 +233,29 @@ impl Landmarker {
Ok(Some(Landmarks { points }))
}
}
#[cfg(test)]
mod tests {
use super::*;
/// Packed and unpacked, every point comes back within a fifth of a
/// source pixel on a 6000-pixel frame — including one outside the
/// image, which a face at the edge does produce.
#[test]
fn dense_landmarks_round_trip_through_their_packed_bytes() {
let mut points = [(0.0_f32, 0.0_f32); POINTS];
for (i, p) in points.iter_mut().enumerate() {
*p = (i as f32 * 37.3 - 200.0, 5900.0 - i as f32 * 11.1);
}
let lm = Landmarks { points };
let bytes = lm.to_packed_bytes(6000.0);
assert_eq!(bytes.len(), PACKED_BYTES);
assert_eq!(PACKED_BYTES, 424);
let back = Landmarks::from_packed_bytes(&bytes, 6000.0).unwrap();
for (a, b) in lm.points.iter().zip(back.points.iter()) {
assert!((a.0 - b.0).abs() < 0.2, "{} vs {}", a.0, b.0);
assert!((a.1 - b.1).abs() < 0.2, "{} vs {}", a.1, b.1);
}
assert!(Landmarks::from_packed_bytes(&bytes[..100], 6000.0).is_none());
}
}