Read each face's eyes, and whether sunglasses hide them

Two MIT classifiers from the same author as the reference pipeline's
whole-body detector: OCEC answers P(open) for one 40×24 eye, SGC
P(sunglasses) for a 48×48 head. Both load in tract once their batch
dimension is pinned by tools/fix-face-model-shapes.sh, like the embedder.

The crops come through the same fitted similarity the aligned face does,
so an eye window is a constant in template units rather than a second
warp, and a tilted head yields an upright eye. Measured on 60 proxies
from the reference library: the eye window plateaus at 22×11, the S
variant beats M and L (which overfit their own domain), and for
sunglasses the aligned face beats a head framing but the higher of the
two catches 11 of 12 pairs against 9 for either alone.

The reading keeps both eyes and the sunglasses number apart, because a
wink averages to the least informative value and a lens of dark glass
draws a confident answer from the eye classifier — over a woman in
sunglasses it read the right eye 0.97 open. Sunglasses take precedence,
and a face behind them is neither open nor a blink.
This commit is contained in:
2026-09-19 14:03:31 +02:00
parent 2481904016
commit 6b51726322
7 changed files with 931 additions and 22 deletions
+139
View File
@@ -0,0 +1,139 @@
//! TRACES: FR-CULL-13
//! What a face's eyes are doing, and how the three numbers behind it are read.
//!
//! Model-free: the classifiers in [`crate::classify`] produce the numbers,
//! and everything that interprets them — the catalog's filter, the People
//! screen's label — comes through here, so a threshold lives in exactly one
//! place.
//!
//! # Three numbers, one answer
//!
//! An eye classifier answers "open or closed" for whatever it is shown, and
//! shown a lens of dark glass it answers anyway. Its answer over sunglasses is
//! not *wrong* in any way it can report — it is a confident probability of a
//! state that cannot be seen — and a filter for "eyes open" that trusted it
//! would drop every photograph from the beach. So the reading carries a
//! third number, from a classifier that looks at the whole head, and it takes
//! precedence: a face behind sunglasses is [`EyeState::Sunglasses`], whatever
//! the eye classifier made of the glass.
//!
//! The two eyes are kept apart rather than averaged. A wink is one eye
//! closed, and averaging it lands at 0.5 — the one value that says the least.
//! [`EyeState::Open`] requires both.
/// The probabilities the classifiers produced for one face.
///
/// Stored per face, nullable as a whole: a face indexed before the eye models
/// existed, or on a device without them, has no reading rather than a
/// reading of zeros.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct EyeReading {
/// P(open) for the subject's **right** eye — image-left, landmark 0.
pub right_open: f32,
/// P(open) for the subject's **left** eye — image-right, landmark 1.
pub left_open: f32,
/// P(the head wears sunglasses).
pub sunglasses: f32,
}
/// Above this an eye is open. The classifier's own decision point; its
/// training put the two classes either side of a sigmoid and this is where
/// the sigmoid crosses.
pub const EYES_OPEN_THRESHOLD: f32 = 0.5;
/// Above this the head wears sunglasses and the eye readings are moot.
pub const SUNGLASSES_THRESHOLD: f32 = 0.5;
/// What the reading says, for a screen or a filter.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EyeState {
/// Both eyes open.
Open,
/// At least one eye closed — a blink, or a wink.
Closed,
/// The eyes cannot be seen. Neither open nor closed, and a filter for
/// either leaves the face alone.
Sunglasses,
}
impl EyeReading {
pub fn state(&self) -> EyeState {
if self.sunglasses >= SUNGLASSES_THRESHOLD {
EyeState::Sunglasses
} else if self.right_open >= EYES_OPEN_THRESHOLD && self.left_open >= EYES_OPEN_THRESHOLD {
EyeState::Open
} else {
EyeState::Closed
}
}
/// Whether this is a face a "no one blinking" filter should drop.
///
/// The filter's question, rather than [`EyeState`]'s three-way answer,
/// because the two differ on exactly the case that matters: a face behind
/// sunglasses is not open, and it is not a blink either. Only
/// [`EyeState::Closed`] is one.
pub fn is_blink(&self) -> bool {
self.state() == EyeState::Closed
}
}
impl EyeState {
/// The word the People screen puts on the face.
pub fn label(&self) -> &'static str {
match self {
EyeState::Open => "Eyes open",
EyeState::Closed => "Eyes closed",
EyeState::Sunglasses => "Sunglasses",
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn reading(right: f32, left: f32, sunglasses: f32) -> EyeReading {
EyeReading {
right_open: right,
left_open: left,
sunglasses,
}
}
#[test]
fn both_eyes_open_is_open() {
assert_eq!(reading(0.9, 0.8, 0.1).state(), EyeState::Open);
assert!(!reading(0.9, 0.8, 0.1).is_blink());
}
/// A wink is not "eyes open": one eye closed lands the same place a
/// blink does, and a filter for "nobody blinking" should drop it.
#[test]
fn one_eye_closed_is_closed() {
assert_eq!(reading(0.9, 0.2, 0.1).state(), EyeState::Closed);
assert_eq!(reading(0.2, 0.9, 0.1).state(), EyeState::Closed);
assert!(reading(0.2, 0.9, 0.1).is_blink());
}
/// The whole reason the third number exists: whatever the eye classifier
/// says over dark glass, it is not a reading of the eyes.
#[test]
fn sunglasses_override_the_eye_readings_either_way() {
assert_eq!(reading(0.9, 0.9, 0.8).state(), EyeState::Sunglasses);
assert_eq!(reading(0.1, 0.1, 0.8).state(), EyeState::Sunglasses);
assert!(!reading(0.1, 0.1, 0.8).is_blink());
}
#[test]
fn the_thresholds_are_inclusive_at_the_decision_point() {
assert_eq!(
reading(EYES_OPEN_THRESHOLD, EYES_OPEN_THRESHOLD, 0.0).state(),
EyeState::Open
);
assert_eq!(
reading(1.0, 1.0, SUNGLASSES_THRESHOLD).state(),
EyeState::Sunglasses
);
}
}