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