use crate::{
ChallengeStep, FrameAnalysis, Landmarks, LivenessChallenge, Point, PoseCheck, TouchTarget,
VerificationThresholds,
};
pub fn validate_pose(
step: &ChallengeStep,
frame: &FrameAnalysis,
_challenge: &LivenessChallenge,
thresholds: &VerificationThresholds,
) -> PoseCheck {
if !frame.face.detected {
return PoseCheck::fail("No se detectó tu cara.");
}
let centered = validate_face_centered(frame);
if !centered.valid {
return centered;
}
match step {
ChallengeStep::FaceCentered => PoseCheck::ok(),
ChallengeStep::Smile => validate_smile(frame, thresholds),
ChallengeStep::TurnLeftOnScreen => validate_yaw(frame, thresholds.yaw_min, true),
ChallengeStep::TurnRightOnScreen => validate_yaw(frame, thresholds.yaw_min, false),
ChallengeStep::ShowFingers { count } => validate_fingers(frame, *count, thresholds),
ChallengeStep::TouchTarget { target } => validate_touch(frame, *target, thresholds),
}
}
fn validate_face_centered(frame: &FrameAnalysis) -> PoseCheck {
let Some(b) = frame.face.bounding_box else {
return PoseCheck::fail("Centra tu cara en el óvalo.");
};
let cx = b.x + b.width / 2.0;
let cy = b.y + b.height / 2.0;
let dx = (cx - 0.5).abs();
let dy = (cy - 0.45).abs();
if dx > 0.18 || dy > 0.20 || b.width < 0.20 || b.width > 0.75 {
return PoseCheck::fail("Centra tu cara en el óvalo.");
}
PoseCheck::ok()
}
fn validate_smile(frame: &FrameAnalysis, thresholds: &VerificationThresholds) -> PoseCheck {
let smile = frame.face.smile_score.unwrap_or(0.0);
if smile < thresholds.smile_min {
return PoseCheck::fail("Sonríe más.");
}
PoseCheck::ok()
}
fn validate_yaw(frame: &FrameAnalysis, yaw_min: f32, left_on_screen: bool) -> PoseCheck {
let Some(yaw) = compute_yaw(frame.face.landmarks.as_ref()) else {
return PoseCheck::fail("Gira más hacia la flecha.");
};
let ok = if left_on_screen {
yaw > yaw_min
} else {
yaw < -yaw_min
};
if ok {
PoseCheck::ok()
} else {
PoseCheck::fail("Gira más hacia la flecha.")
}
}
fn validate_fingers(
frame: &FrameAnalysis,
target: u8,
thresholds: &VerificationThresholds,
) -> PoseCheck {
if frame.hands.is_empty() {
return PoseCheck::fail("No detectamos tu mano en la foto.");
}
let ok = frame
.hands
.iter()
.filter_map(|h| h.extended_fingers)
.any(|count| count.abs_diff(target) <= thresholds.fingers_tolerance);
if ok {
PoseCheck::ok()
} else {
PoseCheck::fail(format!(
"Muestra {target} {}.",
if target == 1 { "dedo" } else { "dedos" }
))
}
}
fn validate_touch(
frame: &FrameAnalysis,
target: TouchTarget,
thresholds: &VerificationThresholds,
) -> PoseCheck {
if frame.hands.is_empty() {
return PoseCheck::fail("No detectamos tu mano en la foto.");
}
let Some(landmarks) = frame.face.landmarks.as_ref() else {
return PoseCheck::fail("No encontramos la zona objetivo.");
};
let targets = face_target_points(landmarks, target);
if targets.is_empty() {
return PoseCheck::fail("No encontramos la zona objetivo.");
}
let Some(face_width) = face_width_from_landmarks(landmarks) else {
return PoseCheck::fail("No encontramos la zona objetivo.");
};
let min_distance = frame
.hands
.iter()
.flat_map(|h| h.finger_tips.iter().copied())
.flat_map(|tip| {
targets
.iter()
.copied()
.map(move |target| tip.distance_to(target))
})
.fold(f32::INFINITY, f32::min);
if !min_distance.is_finite() || min_distance / face_width > thresholds.touch_target_max_distance
{
return PoseCheck::fail(target.touch_message());
}
PoseCheck::ok()
}
pub fn compute_yaw(landmarks: Option<&Landmarks>) -> Option<f32> {
let landmarks = landmarks?;
let nose = landmarks.get(30)?;
let left = landmarks.get(0)?;
let right = landmarks.get(16)?;
let d_l = nose.distance_to(left);
let d_r = nose.distance_to(right);
let total = d_l + d_r;
if total <= f32::EPSILON {
None
} else {
Some((d_l - d_r) / total)
}
}
fn face_width_from_landmarks(landmarks: &Landmarks) -> Option<f32> {
let left = landmarks.get(0)?;
let right = landmarks.get(16)?;
Some(left.distance_to(right).max(1.0))
}
fn face_target_points(landmarks: &Landmarks, target: TouchTarget) -> Vec<Point> {
match target {
TouchTarget::Nose => landmarks.get(30).into_iter().collect(),
TouchTarget::Ear => [landmarks.get(0), landmarks.get(16)]
.into_iter()
.flatten()
.collect(),
TouchTarget::Cheek => [landmarks.get(2), landmarks.get(14)]
.into_iter()
.flatten()
.collect(),
}
}