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:
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user