feat: add debug mode, distance-based thresholds, and improve pose detection
- Add debug mode support for encoding statistics in API responses - Debug info includes encoding length, min/max/mean/std, and first 10 values - Frontend logs encoding stats to browser console when debug enabled - Identify page enables debug mode by default - Implement distance-based confidence thresholds for stricter matching - Borderline distances require higher confidence (70-95% vs 50%) - Applied when use_distance_based_thresholds=True (auto-match) - Reduces false positives for borderline matches - Dual tolerance system for auto-match - Default tolerance 0.6 for regular browsing (more lenient) - Run auto-match button uses 0.5 tolerance with distance-based thresholds (stricter) - Auto-accept threshold updated to 85% (from 70%) - Enhance pose detection with single-eye detection - Profile threshold reduced from 30° to 15° (stricter) - Detect single-eye visibility for extreme profile views - Infer profile direction from landmark visibility - Improved face width threshold (20px vs 10px) - Clean up debug code - Remove test photo UUID checks from production code - Remove debug print statements - Replace print statements with proper logging
This commit is contained in:
+97
-28
@@ -22,7 +22,7 @@ class PoseDetector:
|
||||
"""Detect face pose (yaw, pitch, roll) using RetinaFace landmarks"""
|
||||
|
||||
# Thresholds for pose detection (in degrees)
|
||||
PROFILE_YAW_THRESHOLD = 30.0 # Faces with |yaw| >= 30° are considered profile
|
||||
PROFILE_YAW_THRESHOLD = 15.0 # Faces with |yaw| >= 15° are considered profile
|
||||
EXTREME_YAW_THRESHOLD = 60.0 # Faces with |yaw| >= 60° are extreme profile
|
||||
|
||||
PITCH_THRESHOLD = 20.0 # Faces with |pitch| >= 20° are looking up/down
|
||||
@@ -39,7 +39,7 @@ class PoseDetector:
|
||||
|
||||
Args:
|
||||
yaw_threshold: Yaw angle threshold for profile detection (degrees)
|
||||
Default: 30.0
|
||||
Default: 15.0
|
||||
pitch_threshold: Pitch angle threshold for up/down detection (degrees)
|
||||
Default: 20.0
|
||||
roll_threshold: Roll angle threshold for tilt detection (degrees)
|
||||
@@ -53,17 +53,24 @@ class PoseDetector:
|
||||
self.roll_threshold = roll_threshold or self.ROLL_THRESHOLD
|
||||
|
||||
@staticmethod
|
||||
def detect_faces_with_landmarks(img_path: str) -> Dict:
|
||||
def detect_faces_with_landmarks(img_path: str, filter_estimated_landmarks: bool = False) -> Dict:
|
||||
"""Detect faces using RetinaFace directly
|
||||
|
||||
Args:
|
||||
img_path: Path to image file
|
||||
filter_estimated_landmarks: If True, remove landmarks that appear to be estimated
|
||||
(e.g., hidden eye in profile views) rather than actually visible.
|
||||
Uses heuristics: if eyes are very close together (< 20px) and
|
||||
yaw calculation suggests extreme profile, mark hidden eye as None.
|
||||
|
||||
Returns:
|
||||
Dictionary with face keys and landmark data:
|
||||
{
|
||||
'face_1': {
|
||||
'facial_area': {'x': x, 'y': y, 'w': w, 'h': h},
|
||||
'landmarks': {
|
||||
'left_eye': (x, y),
|
||||
'right_eye': (x, y),
|
||||
'left_eye': (x, y) or None,
|
||||
'right_eye': (x, y) or None,
|
||||
'nose': (x, y),
|
||||
'left_mouth': (x, y),
|
||||
'right_mouth': (x, y)
|
||||
@@ -76,6 +83,42 @@ class PoseDetector:
|
||||
return {}
|
||||
|
||||
faces = RetinaFace.detect_faces(img_path)
|
||||
|
||||
# Post-process to filter estimated landmarks if requested
|
||||
if filter_estimated_landmarks:
|
||||
for face_key, face_data in faces.items():
|
||||
landmarks = face_data.get('landmarks', {})
|
||||
if not landmarks:
|
||||
continue
|
||||
|
||||
left_eye = landmarks.get('left_eye')
|
||||
right_eye = landmarks.get('right_eye')
|
||||
nose = landmarks.get('nose')
|
||||
|
||||
# Check if both eyes are present and very close together (profile view)
|
||||
if left_eye and right_eye and nose:
|
||||
face_width = abs(right_eye[0] - left_eye[0])
|
||||
|
||||
# If eyes are very close (< 20px), likely a profile view
|
||||
if face_width < 20.0:
|
||||
# Calculate which eye is likely hidden based on nose position
|
||||
eye_mid_x = (left_eye[0] + right_eye[0]) / 2
|
||||
nose_x = nose[0]
|
||||
|
||||
# If nose is closer to left eye, right eye is likely hidden (face turned left)
|
||||
# If nose is closer to right eye, left eye is likely hidden (face turned right)
|
||||
dist_to_left = abs(nose_x - left_eye[0])
|
||||
dist_to_right = abs(nose_x - right_eye[0])
|
||||
|
||||
if dist_to_left < dist_to_right:
|
||||
# Nose closer to left eye = face turned left = right eye hidden
|
||||
landmarks['right_eye'] = None
|
||||
else:
|
||||
# Nose closer to right eye = face turned right = left eye hidden
|
||||
landmarks['left_eye'] = None
|
||||
|
||||
face_data['landmarks'] = landmarks
|
||||
|
||||
return faces
|
||||
|
||||
@staticmethod
|
||||
@@ -260,7 +303,8 @@ class PoseDetector:
|
||||
def classify_pose_mode(yaw: Optional[float],
|
||||
pitch: Optional[float],
|
||||
roll: Optional[float],
|
||||
face_width: Optional[float] = None) -> str:
|
||||
face_width: Optional[float] = None,
|
||||
landmarks: Optional[Dict] = None) -> str:
|
||||
"""Classify face pose mode from all three angles and optionally face width
|
||||
|
||||
Args:
|
||||
@@ -268,8 +312,10 @@ class PoseDetector:
|
||||
pitch: Pitch angle in degrees
|
||||
roll: Roll angle in degrees
|
||||
face_width: Face width in pixels (eye distance). Used as indicator for profile detection.
|
||||
If face_width < 25px, indicates profile view. When yaw is available but < 30°,
|
||||
If face_width < 25px, indicates profile view. When yaw is available but < 15°,
|
||||
face_width can override yaw if it suggests profile (face_width < 25px).
|
||||
landmarks: Optional facial landmarks dictionary. Used to detect single-eye visibility
|
||||
for extreme profile views where only one eye is visible.
|
||||
|
||||
Returns:
|
||||
Pose mode classification string:
|
||||
@@ -279,6 +325,28 @@ class PoseDetector:
|
||||
- 'tilted_left', 'tilted_right': roll variations
|
||||
- Combined modes: e.g., 'profile_left_looking_up'
|
||||
"""
|
||||
# Check for single-eye visibility to infer profile direction
|
||||
# This handles extreme profile views where only one eye is visible
|
||||
if landmarks:
|
||||
left_eye = landmarks.get('left_eye')
|
||||
right_eye = landmarks.get('right_eye')
|
||||
|
||||
# Only right eye visible -> face turned left -> profile_left
|
||||
if left_eye is None and right_eye is not None:
|
||||
# Infer profile_left when only right eye is visible
|
||||
inferred_profile = "profile_left"
|
||||
# Only left eye visible -> face turned right -> profile_right
|
||||
elif left_eye is not None and right_eye is None:
|
||||
# Infer profile_right when only left eye is visible
|
||||
inferred_profile = "profile_right"
|
||||
# No eyes visible -> extreme profile, default to profile_left
|
||||
elif left_eye is None and right_eye is None:
|
||||
inferred_profile = "profile_left"
|
||||
else:
|
||||
inferred_profile = None # Both eyes visible, use normal logic
|
||||
else:
|
||||
inferred_profile = None
|
||||
|
||||
# Default to frontal if angles unknown
|
||||
yaw_original = yaw
|
||||
if yaw is None:
|
||||
@@ -290,20 +358,23 @@ class PoseDetector:
|
||||
|
||||
# Face width threshold for profile detection (in pixels)
|
||||
# Profile faces have very small eye distance (< 25 pixels typically)
|
||||
PROFILE_FACE_WIDTH_THRESHOLD = 10.0 #25.0
|
||||
PROFILE_FACE_WIDTH_THRESHOLD = 20.0
|
||||
|
||||
# Yaw classification - PRIMARY INDICATOR
|
||||
# Use yaw angle as the primary indicator (30° threshold)
|
||||
# Use yaw angle as the primary indicator (15° threshold)
|
||||
abs_yaw = abs(yaw)
|
||||
|
||||
# Primary classification based on yaw angle
|
||||
if abs_yaw < 30.0:
|
||||
if abs_yaw < 15.0:
|
||||
# Yaw indicates frontal view
|
||||
# Trust yaw when it's available and reasonable (< 30°)
|
||||
# Trust yaw when it's available and reasonable (< 15°)
|
||||
# Only use face_width as fallback when yaw is unavailable (None)
|
||||
if yaw_original is None:
|
||||
# Yaw unavailable - use face_width as fallback
|
||||
if face_width is not None:
|
||||
# Yaw unavailable - check for single-eye visibility first
|
||||
if inferred_profile is not None:
|
||||
# Single eye visible or no eyes visible -> use inferred profile direction
|
||||
yaw_mode = inferred_profile
|
||||
elif face_width is not None:
|
||||
if face_width < PROFILE_FACE_WIDTH_THRESHOLD:
|
||||
# Face width suggests profile view - use it when yaw is unavailable
|
||||
yaw_mode = "profile_left" # Default direction when yaw unavailable
|
||||
@@ -311,16 +382,14 @@ class PoseDetector:
|
||||
# Face width is normal (>= 25px) - likely frontal
|
||||
yaw_mode = "frontal"
|
||||
else:
|
||||
# Both yaw and face_width unavailable - cannot determine reliably
|
||||
# This usually means landmarks are incomplete (missing nose and/or eyes)
|
||||
# For extreme profile views, both eyes might not be visible, which would
|
||||
# cause face_width to be None. In this case, we cannot reliably determine
|
||||
# pose without additional indicators (like face bounding box aspect ratio).
|
||||
# Default to frontal (conservative approach), but this might misclassify
|
||||
# some extreme profile faces.
|
||||
yaw_mode = "frontal"
|
||||
# Both yaw and face_width unavailable - check if we inferred profile from landmarks
|
||||
if inferred_profile is not None:
|
||||
yaw_mode = inferred_profile
|
||||
else:
|
||||
# Cannot determine reliably - default to frontal
|
||||
yaw_mode = "frontal"
|
||||
else:
|
||||
# Yaw is available and < 30° - but still check face_width
|
||||
# Yaw is available and < 15° - but still check face_width
|
||||
# If face_width is very small (< 25px), it suggests profile even with small yaw
|
||||
if face_width is not None:
|
||||
if face_width < PROFILE_FACE_WIDTH_THRESHOLD:
|
||||
@@ -332,11 +401,11 @@ class PoseDetector:
|
||||
else:
|
||||
# No face_width provided - trust yaw, classify as frontal
|
||||
yaw_mode = "frontal"
|
||||
elif yaw <= -30.0:
|
||||
# abs_yaw >= 30.0 and yaw is negative - profile left
|
||||
elif yaw <= -15.0:
|
||||
# abs_yaw >= 15.0 and yaw is negative - profile left
|
||||
yaw_mode = "profile_left" # Negative yaw = face turned left = left profile visible
|
||||
elif yaw >= 30.0:
|
||||
# abs_yaw >= 30.0 and yaw is positive - profile right
|
||||
elif yaw >= 15.0:
|
||||
# abs_yaw >= 15.0 and yaw is positive - profile right
|
||||
yaw_mode = "profile_right" # Positive yaw = face turned right = right profile visible
|
||||
else:
|
||||
# This should never be reached, but handle edge case
|
||||
@@ -411,8 +480,8 @@ class PoseDetector:
|
||||
# Calculate face width (eye distance) for profile detection
|
||||
face_width = self.calculate_face_width_from_landmarks(landmarks)
|
||||
|
||||
# Classify pose mode (using face width as additional indicator)
|
||||
pose_mode = self.classify_pose_mode(yaw_angle, pitch_angle, roll_angle, face_width)
|
||||
# Classify pose mode (using face width and landmarks as additional indicators)
|
||||
pose_mode = self.classify_pose_mode(yaw_angle, pitch_angle, roll_angle, face_width, landmarks)
|
||||
|
||||
# Normalize facial_area format (RetinaFace returns list [x, y, w, h] or dict)
|
||||
facial_area_raw = face_data.get('facial_area', {})
|
||||
|
||||
Reference in New Issue
Block a user