# Portrait/Profile Face Detection Plan

**Version:** 1.0  
**Created:** November 2025  
**Status:** Planning Phase

---

## Executive Summary

This plan outlines the implementation of automatic face pose detection using RetinaFace directly (not via DeepFace) to identify and mark faces based on their pose/orientation. The system will detect multiple pose modes including profile (yaw), looking up/down (pitch), tilted faces (roll), and their combinations. This enables intelligent filtering in auto-match and other features.

**Key Benefits:**
- Automatic detection of face pose (yaw, pitch, roll) without user input
- Ability to filter faces by pose mode in auto-match (profile, looking up, tilted, etc.)
- Better face matching accuracy by excluding low-quality or extreme-angle views
- Enhanced user experience with automatic pose classification
- Support for multiple pose modes: profile, looking up/down, tilted, extreme angles

---

## Current State Analysis

### Current Implementation

**Face Detection Method:**
- Uses DeepFace library which wraps RetinaFace
- `DeepFace.represent()` provides: `facial_area`, `face_confidence`, `embedding`
- No access to facial landmarks or pose information

**Data Stored:**
- Face bounding box: `{x, y, w, h}`
- Detection confidence: `face_confidence`
- Face encoding: 512-dimensional embedding
- Quality score: calculated from image properties
- No pose/angle information stored

**Database Schema:**
```sql
CREATE TABLE faces (
    id INTEGER PRIMARY KEY,
    photo_id INTEGER,
    person_id INTEGER,
    encoding BLOB,
    location TEXT,  -- JSON: {"x": x, "y": y, "w": w, "h": h}
    confidence REAL,
    quality_score REAL,
    is_primary_encoding BOOLEAN,
    detector_backend TEXT,
    model_name TEXT,
    face_confidence REAL,
    exif_orientation INTEGER
    -- NO is_portrait field
)
```

### Limitations

1. **No Landmark Access:** DeepFace doesn't expose RetinaFace landmarks
2. **No Pose Estimation:** Cannot calculate yaw, pitch, roll angles
3. **No Profile Classification:** Cannot automatically identify profile faces
4. **Manual Filtering Required:** Users cannot filter profile faces in auto-match

---

## Requirements

### Functional Requirements

1. **Automatic Pose Detection:**
   - Detect face pose angles (yaw, pitch, roll) during processing
   - Classify faces into pose modes: frontal, profile, looking up, looking down, tilted, extreme angles
   - Store pose information in database
   - No user intervention required

2. **Pose Mode Classification:**
   - **Yaw (left/right):** frontal, profile_left, profile_right, extreme_yaw
   - **Pitch (up/down):** looking_up, looking_down, extreme_pitch
   - **Roll (tilted):** tilted_left, tilted_right, extreme_roll
   - **Combined modes:** e.g., profile_left_looking_up, tilted_profile_right
   - Threshold-based classification using pose angles

3. **Filtering Support:**
   - Filter faces by pose mode in auto-match (exclude profile, looking up, tilted, etc.)
   - Multiple filter options: exclude profile, exclude extreme angles, exclude specific modes
   - Optional filtering in other features (search, identify)

4. **Clean Database:**
   - Starting with fresh database - no migration needed
   - All faces will have pose data from the start

### Technical Requirements

1. **RetinaFace Direct Integration:**
   - Use RetinaFace library directly (not via DeepFace)
   - Extract facial landmarks (5 points: eyes, nose, mouth corners)
   - Calculate all pose angles (yaw, pitch, roll) from landmarks

2. **Performance:**
   - Minimal performance impact (RetinaFace is already used by DeepFace)
   - Reuse existing face detection results where possible
   - Angle calculations are fast (< 1ms per face)

3. **Accuracy:**
   - Pose detection accuracy > 90% for clear frontal/profile views
   - Handle edge cases (slight angles, extreme angles, occlusions)
   - Robust to lighting and image quality variations

4. **Pose Modes Supported:**
   - **Yaw:** Frontal (|yaw| < 30°), Profile Left (yaw < -30°), Profile Right (yaw > 30°), Extreme Yaw (|yaw| > 60°)
   - **Pitch:** Level (|pitch| < 20°), Looking Up (pitch > 20°), Looking Down (pitch < -20°), Extreme Pitch (|pitch| > 45°)
   - **Roll:** Upright (|roll| < 15°), Tilted Left (roll < -15°), Tilted Right (roll > 15°), Extreme Roll (|roll| > 45°)

---

## Technical Approach

### Architecture Overview

```
┌─────────────────────────────────────────────────────────┐
│                    Face Processing                       │
├─────────────────────────────────────────────────────────┤
│                                                           │
│  1. Use RetinaFace directly for face detection          │
│     └─> Returns: bounding box, landmarks, confidence    │
│                                                           │
│  2. Calculate pose angles from landmarks                │
│     └─> Yaw (left/right rotation)                       │
│     └─> Pitch (up/down tilt)                            │
│     └─> Roll (rotation around face axis)                 │
│                                                           │
│  3. Calculate all pose angles from landmarks            │
│     └─> Yaw (left/right rotation): -90° to +90°        │
│     └─> Pitch (up/down tilt): -90° to +90°              │
│     └─> Roll (rotation around face): -90° to +90°      │
│                                                           │
│  4. Classify face pose modes                            │
│     └─> Yaw: frontal, profile_left, profile_right      │
│     └─> Pitch: level, looking_up, looking_down         │
│     └─> Roll: upright, tilted_left, tilted_right       │
│     └─> Combined: profile_left_looking_up, etc.         │
│                                                           │
│  5. Still use DeepFace for encoding generation         │
│     └─> RetinaFace: detection + landmarks               │
│     └─> DeepFace: encoding generation (ArcFace)          │
│                                                           │
│  6. Store pose information in database                 │
│     └─> pose_mode TEXT (e.g., "frontal", "profile_left")│
│     └─> yaw_angle, pitch_angle, roll_angle REAL         │
│                                                           │
└─────────────────────────────────────────────────────────┘
```

### Pose Estimation from Landmarks

**RetinaFace Landmarks (5 points):**
- Left eye: `(x1, y1)`
- Right eye: `(x2, y2)`
- Nose: `(x3, y3)`
- Left mouth corner: `(x4, y4)`
- Right mouth corner: `(x5, y5)`

**Yaw Angle Calculation (Left/Right Rotation):**
```python
# Calculate yaw from eye and nose positions
left_eye = landmarks['left_eye']
right_eye = landmarks['right_eye']
nose = landmarks['nose']

# Eye midpoint
eye_mid_x = (left_eye[0] + right_eye[0]) / 2
eye_mid_y = (left_eye[1] + right_eye[1]) / 2

# Horizontal offset from nose to eye midpoint
horizontal_offset = nose[0] - eye_mid_x
face_width = abs(right_eye[0] - left_eye[0])

# Yaw angle (degrees)
yaw_angle = atan2(horizontal_offset, face_width) * 180 / π
# Negative: face turned left (right profile visible)
# Positive: face turned right (left profile visible)
```

**Pitch Angle Calculation (Up/Down Tilt):**
```python
# Calculate pitch from eye and mouth positions
left_eye = landmarks['left_eye']
right_eye = landmarks['right_eye']
left_mouth = landmarks['left_mouth']
right_mouth = landmarks['right_mouth']
nose = landmarks['nose']

# Eye midpoint
eye_mid_y = (left_eye[1] + right_eye[1]) / 2
# Mouth midpoint
mouth_mid_y = (left_mouth[1] + right_mouth[1]) / 2
# Nose vertical position
nose_y = nose[1]

# Expected nose position (between eyes and mouth)
expected_nose_y = eye_mid_y + (mouth_mid_y - eye_mid_y) * 0.6
face_height = abs(mouth_mid_y - eye_mid_y)

# Vertical offset
vertical_offset = nose_y - expected_nose_y

# Pitch angle (degrees)
pitch_angle = atan2(vertical_offset, face_height) * 180 / π
# Positive: looking up
# Negative: looking down
```

**Roll Angle Calculation (Rotation Around Face Axis):**
```python
# Calculate roll from eye positions
left_eye = landmarks['left_eye']
right_eye = landmarks['right_eye']

# Calculate angle of eye line
dx = right_eye[0] - left_eye[0]
dy = right_eye[1] - left_eye[1]

# Roll angle (degrees)
roll_angle = atan2(dy, dx) * 180 / π
# Positive: tilted right (clockwise)
# Negative: tilted left (counterclockwise)
```

**Combined Pose Mode Classification:**
```python
# Classify pose mode based on all three angles
def classify_pose_mode(yaw, pitch, roll):
    """Classify face pose mode from angles"""
    
    # Yaw classification
    if abs(yaw) < 30:
        yaw_mode = "frontal"
    elif yaw < -30:
        yaw_mode = "profile_right"
    elif yaw > 30:
        yaw_mode = "profile_left"
    else:
        yaw_mode = "slight_yaw"
    
    # Pitch classification
    if abs(pitch) < 20:
        pitch_mode = "level"
    elif pitch > 20:
        pitch_mode = "looking_up"
    elif pitch < -20:
        pitch_mode = "looking_down"
    else:
        pitch_mode = "slight_pitch"
    
    # Roll classification
    if abs(roll) < 15:
        roll_mode = "upright"
    elif roll > 15:
        roll_mode = "tilted_right"
    elif roll < -15:
        roll_mode = "tilted_left"
    else:
        roll_mode = "slight_roll"
    
    # Combine modes
    if yaw_mode == "frontal" and pitch_mode == "level" and roll_mode == "upright":
        return "frontal"
    else:
        return f"{yaw_mode}_{pitch_mode}_{roll_mode}"
```

---

## Implementation Plan

### Phase 1: Database Schema Updates

#### Step 1.1: Add Pose Fields to Database

**Desktop Database (`src/core/database.py`):**
```python
# Add to faces table
ALTER TABLE faces ADD COLUMN pose_mode TEXT DEFAULT 'frontal';  -- e.g., 'frontal', 'profile_left', 'looking_up', etc.
ALTER TABLE faces ADD COLUMN yaw_angle REAL DEFAULT NULL;  -- Yaw angle in degrees
ALTER TABLE faces ADD COLUMN pitch_angle REAL DEFAULT NULL;  -- Pitch angle in degrees
ALTER TABLE faces ADD COLUMN roll_angle REAL DEFAULT NULL;  -- Roll angle in degrees
```

**Web Database (Alembic Migration):**
```python
# Create new Alembic migration
alembic revision -m "add_pose_detection_to_faces"

# Migration file: alembic/versions/YYYYMMDD_add_pose_to_faces.py
def upgrade():
    # Add pose fields
    op.add_column('faces', sa.Column('pose_mode', sa.String(50), 
                                     nullable=False, server_default='frontal'))
    op.add_column('faces', sa.Column('yaw_angle', sa.Numeric(), nullable=True))
    op.add_column('faces', sa.Column('pitch_angle', sa.Numeric(), nullable=True))
    op.add_column('faces', sa.Column('roll_angle', sa.Numeric(), nullable=True))
    
    # Create indices
    op.create_index('ix_faces_pose_mode', 'faces', ['pose_mode'])

def downgrade():
    op.drop_index('ix_faces_pose_mode', table_name='faces')
    op.drop_column('faces', 'roll_angle')
    op.drop_column('faces', 'pitch_angle')
    op.drop_column('faces', 'yaw_angle')
    op.drop_column('faces', 'pose_mode')
```

**SQLAlchemy Model (`src/web/db/models.py`):**
```python
class Face(Base):
    # ... existing fields ...
    pose_mode = Column(String(50), default='frontal', nullable=False, index=True)  # e.g., 'frontal', 'profile_left'
    yaw_angle = Column(Numeric, nullable=True)  # Yaw angle in degrees
    pitch_angle = Column(Numeric, nullable=True)  # Pitch angle in degrees
    roll_angle = Column(Numeric, nullable=True)  # Roll angle in degrees
```

#### Step 1.2: Update Database Methods

**`src/core/database.py` - `add_face()` method:**
```python
def add_face(self, photo_id: int, encoding: bytes, location: str, 
             confidence: float = 0.0, quality_score: float = 0.0, 
             person_id: Optional[int] = None,
             detector_backend: str = 'retinaface',
             model_name: str = 'ArcFace',
             face_confidence: float = 0.0,
             pose_mode: str = 'frontal',  # Pose mode classification
             yaw_angle: Optional[float] = None,  # Yaw angle in degrees
             pitch_angle: Optional[float] = None,  # Pitch angle in degrees
             roll_angle: Optional[float] = None) -> int:  # Roll angle in degrees
    """Add face to database with pose detection"""
    cursor.execute('''
        INSERT INTO faces (photo_id, person_id, encoding, location, 
                          confidence, quality_score, is_primary_encoding,
                          detector_backend, model_name, face_confidence,
                          pose_mode, yaw_angle, pitch_angle, roll_angle)
        VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
    ''', (photo_id, person_id, encoding, location, confidence, 
          quality_score, False, detector_backend, model_name, 
          face_confidence, pose_mode, yaw_angle, pitch_angle, roll_angle))
    return cursor.lastrowid
```

---

### Phase 2: RetinaFace Direct Integration

#### Step 2.1: Install/Verify RetinaFace Library

**Check if RetinaFace is available:**
```python
try:
    from retinaface import RetinaFace
    RETINAFACE_AVAILABLE = True
except ImportError:
    RETINAFACE_AVAILABLE = False
    # RetinaFace is typically installed with DeepFace
    # If not, install: pip install retina-face
```

**Update `requirements.txt`:**
```
retina-face>=0.0.13  # Already included, but verify version
```

#### Step 2.2: Create Pose Detection Utility

**New file: `src/utils/pose_detection.py`**

```python
"""Face pose detection (yaw, pitch, roll) using RetinaFace landmarks"""

import numpy as np
from math import atan2, degrees, pi
from typing import Dict, Tuple, Optional, List

try:
    from retinaface import RetinaFace
    RETINAFACE_AVAILABLE = True
except ImportError:
    RETINAFACE_AVAILABLE = False
    RetinaFace = None


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
    EXTREME_YAW_THRESHOLD = 60.0  # Faces with |yaw| >= 60° are extreme profile
    
    PITCH_THRESHOLD = 20.0  # Faces with |pitch| >= 20° are looking up/down
    EXTREME_PITCH_THRESHOLD = 45.0  # Faces with |pitch| >= 45° are extreme
    
    ROLL_THRESHOLD = 15.0  # Faces with |roll| >= 15° are tilted
    EXTREME_ROLL_THRESHOLD = 45.0  # Faces with |roll| >= 45° are extreme
    
    def __init__(self, 
                 yaw_threshold: float = None,
                 pitch_threshold: float = None,
                 roll_threshold: float = None):
        """Initialize pose detector
        
        Args:
            yaw_threshold: Yaw angle threshold for profile detection (degrees)
                          Default: 30.0
            pitch_threshold: Pitch angle threshold for up/down detection (degrees)
                           Default: 20.0
            roll_threshold: Roll angle threshold for tilt detection (degrees)
                          Default: 15.0
        """
        if not RETINAFACE_AVAILABLE:
            raise RuntimeError("RetinaFace not available")
        
        self.yaw_threshold = yaw_threshold or self.PROFILE_YAW_THRESHOLD
        self.pitch_threshold = pitch_threshold or self.PITCH_THRESHOLD
        self.roll_threshold = roll_threshold or self.ROLL_THRESHOLD
    
    @staticmethod
    def detect_faces_with_landmarks(img_path: str) -> Dict:
        """Detect faces using RetinaFace directly
        
        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),
                        'nose': (x, y),
                        'left_mouth': (x, y),
                        'right_mouth': (x, y)
                    },
                    'confidence': 0.95
                }
            }
        """
        if not RETINAFACE_AVAILABLE:
            return {}
        
        faces = RetinaFace.detect_faces(img_path)
        return faces
    
    @staticmethod
    def calculate_yaw_from_landmarks(landmarks: Dict) -> Optional[float]:
        """Calculate yaw angle from facial landmarks
        
        Args:
            landmarks: Dictionary with landmark positions:
                      {
                          'left_eye': (x, y),
                          'right_eye': (x, y),
                          'nose': (x, y),
                          'left_mouth': (x, y),
                          'right_mouth': (x, y)
                      }
        
        Returns:
            Yaw angle in degrees (-90 to +90):
            - Negative: face turned left (right profile)
            - Positive: face turned right (left profile)
            - Zero: frontal face
            - None: if landmarks invalid
        """
        if not landmarks:
            return None
        
        left_eye = landmarks.get('left_eye')
        right_eye = landmarks.get('right_eye')
        nose = landmarks.get('nose')
        
        if not all([left_eye, right_eye, nose]):
            return None
        
        # Calculate eye midpoint
        eye_mid_x = (left_eye[0] + right_eye[0]) / 2
        eye_mid_y = (left_eye[1] + right_eye[1]) / 2
        
        # Calculate horizontal distance from nose to eye midpoint
        nose_x = nose[0]
        eye_midpoint_x = eye_mid_x
        
        # Calculate face width (eye distance)
        face_width = abs(right_eye[0] - left_eye[0])
        
        if face_width == 0:
            return None
        
        # Calculate horizontal offset
        horizontal_offset = nose_x - eye_midpoint_x
        
        # Calculate yaw angle using atan2
        # Normalize by face width to get angle
        yaw_radians = atan2(horizontal_offset, face_width)
        yaw_degrees = degrees(yaw_radians)
        
        return yaw_degrees
    
    @staticmethod
    def calculate_pitch_from_landmarks(landmarks: Dict) -> Optional[float]:
        """Calculate pitch angle from facial landmarks (up/down tilt)
        
        Args:
            landmarks: Dictionary with landmark positions
        
        Returns:
            Pitch angle in degrees (-90 to +90):
            - Positive: looking up
            - Negative: looking down
            - None: if landmarks invalid
        """
        if not landmarks:
            return None
        
        left_eye = landmarks.get('left_eye')
        right_eye = landmarks.get('right_eye')
        left_mouth = landmarks.get('left_mouth')
        right_mouth = landmarks.get('right_mouth')
        nose = landmarks.get('nose')
        
        if not all([left_eye, right_eye, left_mouth, right_mouth, nose]):
            return None
        
        # Eye midpoint
        eye_mid_y = (left_eye[1] + right_eye[1]) / 2
        # Mouth midpoint
        mouth_mid_y = (left_mouth[1] + right_mouth[1]) / 2
        # Nose vertical position
        nose_y = nose[1]
        
        # Expected nose position (typically 60% down from eyes to mouth)
        expected_nose_y = eye_mid_y + (mouth_mid_y - eye_mid_y) * 0.6
        face_height = abs(mouth_mid_y - eye_mid_y)
        
        if face_height == 0:
            return None
        
        # Vertical offset from expected position
        vertical_offset = nose_y - expected_nose_y
        
        # Calculate pitch angle
        pitch_radians = atan2(vertical_offset, face_height)
        pitch_degrees = degrees(pitch_radians)
        
        return pitch_degrees
    
    @staticmethod
    def calculate_roll_from_landmarks(landmarks: Dict) -> Optional[float]:
        """Calculate roll angle from facial landmarks (rotation around face axis)
        
        Args:
            landmarks: Dictionary with landmark positions
        
        Returns:
            Roll angle in degrees (-90 to +90):
            - Positive: tilted right (clockwise)
            - Negative: tilted left (counterclockwise)
            - None: if landmarks invalid
        """
        if not landmarks:
            return None
        
        left_eye = landmarks.get('left_eye')
        right_eye = landmarks.get('right_eye')
        
        if not all([left_eye, right_eye]):
            return None
        
        # Calculate angle of eye line
        dx = right_eye[0] - left_eye[0]
        dy = right_eye[1] - left_eye[1]
        
        if dx == 0:
            return 90.0 if dy > 0 else -90.0  # Vertical line
        
        # Roll angle
        roll_radians = atan2(dy, dx)
        roll_degrees = degrees(roll_radians)
        
        return roll_degrees
    
    @staticmethod
    def classify_pose_mode(yaw: Optional[float], 
                          pitch: Optional[float], 
                          roll: Optional[float]) -> str:
        """Classify face pose mode from all three angles
        
        Args:
            yaw: Yaw angle in degrees
            pitch: Pitch angle in degrees
            roll: Roll angle in degrees
        
        Returns:
            Pose mode classification string:
            - 'frontal': frontal, level, upright
            - 'profile_left', 'profile_right': profile views
            - 'looking_up', 'looking_down': pitch variations
            - 'tilted_left', 'tilted_right': roll variations
            - Combined modes: e.g., 'profile_left_looking_up'
        """
        # Default to frontal if angles unknown
        if yaw is None:
            yaw = 0.0
        if pitch is None:
            pitch = 0.0
        if roll is None:
            roll = 0.0
        
        # Yaw classification
        abs_yaw = abs(yaw)
        if abs_yaw < 30.0:
            yaw_mode = "frontal"
        elif yaw < -30.0:
            yaw_mode = "profile_right"
        elif yaw > 30.0:
            yaw_mode = "profile_left"
        else:
            yaw_mode = "slight_yaw"
        
        # Pitch classification
        abs_pitch = abs(pitch)
        if abs_pitch < 20.0:
            pitch_mode = "level"
        elif pitch > 20.0:
            pitch_mode = "looking_up"
        elif pitch < -20.0:
            pitch_mode = "looking_down"
        else:
            pitch_mode = "slight_pitch"
        
        # Roll classification
        abs_roll = abs(roll)
        if abs_roll < 15.0:
            roll_mode = "upright"
        elif roll > 15.0:
            roll_mode = "tilted_right"
        elif roll < -15.0:
            roll_mode = "tilted_left"
        else:
            roll_mode = "slight_roll"
        
        # Combine modes - simple case first
        if yaw_mode == "frontal" and pitch_mode == "level" and roll_mode == "upright":
            return "frontal"
        
        # Build combined mode string
        modes = []
        if yaw_mode != "frontal":
            modes.append(yaw_mode)
        if pitch_mode != "level":
            modes.append(pitch_mode)
        if roll_mode != "upright":
            modes.append(roll_mode)
        
        return "_".join(modes) if modes else "frontal"
    
    def detect_pose_faces(self, img_path: str) -> List[Dict]:
        """Detect all faces and classify pose status (all angles)
        
        Args:
            img_path: Path to image file
        
        Returns:
            List of face dictionaries with pose information:
            [{
                'facial_area': {...},
                'landmarks': {...},
                'confidence': 0.95,
                'yaw_angle': -45.2,
                'pitch_angle': 10.5,
                'roll_angle': -5.2,
                'pose_mode': 'profile_right_level_upright'
            }, ...]
        """
        faces = self.detect_faces_with_landmarks(img_path)
        
        results = []
        for face_key, face_data in faces.items():
            landmarks = face_data.get('landmarks', {})
            
            # Calculate all three angles
            yaw_angle = self.calculate_yaw_from_landmarks(landmarks)
            pitch_angle = self.calculate_pitch_from_landmarks(landmarks)
            roll_angle = self.calculate_roll_from_landmarks(landmarks)
            
            # Classify pose mode
            pose_mode = self.classify_pose_mode(yaw_angle, pitch_angle, roll_angle)
            
            result = {
                'facial_area': face_data.get('facial_area', {}),
                'landmarks': landmarks,
                'confidence': face_data.get('confidence', 0.0),
                'yaw_angle': yaw_angle,
                'pitch_angle': pitch_angle,
                'roll_angle': roll_angle,
                'pose_mode': pose_mode
            }
            results.append(result)
        
        return results
```

---

### Phase 3: Integrate Portrait Detection into Face Processing

**Important: Backward Compatibility Requirement**
- All pose detection must have graceful fallback to defaults (`frontal`, `None` angles)
- If RetinaFace is unavailable or fails, use defaults and continue processing
- Do not fail face processing if pose detection fails
- See "Backward Compatibility & Graceful Degradation" section for details

#### Step 3.1: Update Face Processing Pipeline

**File: `src/core/face_processing.py`**

**Changes:**
1. Import portrait detection utility
2. Use RetinaFace for detection + landmarks (with graceful fallback)
3. Use DeepFace for encoding generation
4. Store portrait status in database (with defaults if unavailable)

**Modified `process_faces()` method:**

```python
from src.utils.pose_detection import PoseDetector, RETINAFACE_AVAILABLE

class FaceProcessor:
    def __init__(self, ...):
        # ... existing initialization ...
        self.pose_detector = None
        if RETINAFACE_AVAILABLE:
            try:
                self.pose_detector = PoseDetector()
            except Exception as e:
                print(f"⚠️  Pose detection not available: {e}")
    
    def process_faces(self, ...):
        """Process faces with portrait detection"""
        # ... existing code ...
        
        for photo_id, photo_path, filename in unprocessed_photos:
            # Step 1: Use RetinaFace directly for detection + landmarks
            pose_faces = []
            if self.pose_detector:
                try:
                    pose_faces = self.pose_detector.detect_pose_faces(photo_path)
                except Exception as e:
                    print(f"⚠️  Pose detection failed for {filename}: {e}")
                    pose_faces = []
            
            # Step 2: Use DeepFace for encoding generation
            results = DeepFace.represent(
                img_path=face_detection_path,
                model_name=self.model_name,
                detector_backend=self.detector_backend,
                enforce_detection=DEEPFACE_ENFORCE_DETECTION,
                align=DEEPFACE_ALIGN_FACES
            )
            
            # Step 3: Match RetinaFace results with DeepFace results
            # Match by facial_area position
            for i, deepface_result in enumerate(results):
                facial_area = deepface_result.get('facial_area', {})
                
                # Find matching RetinaFace result
                pose_info = self._find_matching_pose_info(
                    facial_area, pose_faces
                )
                
                pose_mode = pose_info.get('pose_mode', 'frontal')
                yaw_angle = pose_info.get('yaw_angle')
                pitch_angle = pose_info.get('pitch_angle')
                roll_angle = pose_info.get('roll_angle')
                
                # Store face with pose information
                face_id = self.db.add_face(
                    photo_id=photo_id,
                    encoding=encoding.tobytes(),
                    location=location_str,
                    confidence=0.0,
                    quality_score=quality_score,
                    person_id=None,
                    detector_backend=self.detector_backend,
                    model_name=self.model_name,
                    face_confidence=face_confidence,
                    pose_mode=pose_mode,
                    yaw_angle=yaw_angle,
                    pitch_angle=pitch_angle,
                    roll_angle=roll_angle
                )
    
    def _find_matching_pose_info(self, facial_area: Dict, 
                                  pose_faces: List[Dict]) -> Dict:
        """Match DeepFace result with RetinaFace pose detection result
        
        Args:
            facial_area: DeepFace facial_area {'x': x, 'y': y, 'w': w, 'h': h}
            pose_faces: List of RetinaFace detection results with pose info
        
        Returns:
            Dictionary with pose information, or defaults
        """
        # Match by bounding box overlap
        # Simple approach: find closest match by center point
        if not pose_faces:
            return {
                'pose_mode': 'frontal',
                'yaw_angle': None,
                'pitch_angle': None,
                'roll_angle': None
            }
        
        deepface_center_x = facial_area.get('x', 0) + facial_area.get('w', 0) / 2
        deepface_center_y = facial_area.get('y', 0) + facial_area.get('h', 0) / 2
        
        best_match = None
        min_distance = float('inf')
        
        for pose_face in pose_faces:
            pose_area = pose_face.get('facial_area', {})
            pose_center_x = (pose_area.get('x', 0) + 
                             pose_area.get('w', 0) / 2)
            pose_center_y = (pose_area.get('y', 0) + 
                             pose_area.get('h', 0) / 2)
            
            # Calculate distance between centers
            distance = ((deepface_center_x - pose_center_x) ** 2 + 
                       (deepface_center_y - pose_center_y) ** 2) ** 0.5
            
            if distance < min_distance:
                min_distance = distance
                best_match = pose_face
        
        # If match is close enough (within 50 pixels), use it
        if best_match and min_distance < 50:
            return {
                'pose_mode': best_match.get('pose_mode', 'frontal'),
                'yaw_angle': best_match.get('yaw_angle'),
                'pitch_angle': best_match.get('pitch_angle'),
                'roll_angle': best_match.get('roll_angle')
            }
        
        return {
            'pose_mode': 'frontal',
            'yaw_angle': None,
            'pitch_angle': None,
            'roll_angle': None
        }
```

#### Step 3.2: Update Web Face Service

**File: `src/web/services/face_service.py`**

Similar changes to integrate portrait detection in web service:

```python
from src.utils.pose_detection import PoseDetector, RETINAFACE_AVAILABLE

def process_photo_faces(...):
    """Process faces with pose detection"""
    # ... existing code ...
    
    # Step 1: Detect faces with RetinaFace for landmarks
    pose_detector = None
    pose_faces = []
    if RETINAFACE_AVAILABLE:
        try:
            pose_detector = PoseDetector()
            pose_faces = pose_detector.detect_pose_faces(photo_path)
        except Exception as e:
            print(f"[FaceService] Pose detection failed: {e}")
    
    # Step 2: Use DeepFace for encoding
    results = DeepFace.represent(...)
    
    # Step 3: Match and store
    for idx, result in enumerate(results):
        # ... existing processing ...
        
        # Match pose info
        pose_info = _find_matching_pose_info(
            facial_area, pose_faces
        )
        
        # Store face
        face = Face(
            # ... existing fields ...
            pose_mode=pose_info.get('pose_mode', 'frontal'),
            yaw_angle=pose_info.get('yaw_angle'),
            pitch_angle=pose_info.get('pitch_angle'),
            roll_angle=pose_info.get('roll_angle')
        )
```

---

### Phase 4: Update Auto-Match Filtering

#### Step 4.1: Add Portrait Filter to Auto-Match

**File: `src/web/services/face_service.py` - `find_auto_match_matches()`**

```python
def find_auto_match_matches(
    db: Session,
    tolerance: float = 0.6,
    exclude_portraits: bool = True,  # NEW parameter
) -> List[Tuple[int, int, Face, List[Tuple[Face, float, float]]]]:
    """Find auto-match matches with optional portrait filtering"""
    
    # ... existing code to get identified faces ...
    
    # For each person, find similar faces
    for person_id, reference_face in person_faces.items():
        # Find similar faces
        similar_faces = find_similar_faces(
            db, reference_face.id, limit=100, tolerance=tolerance
        )
        
        # Filter out portraits/extreme angles if requested
        if exclude_portraits:
            similar_faces = [
                (face, distance, confidence_pct)
                for face, distance, confidence_pct in similar_faces
                if face.pose_mode == 'frontal'  # Filter non-frontal faces
            ]
        
        # ... rest of matching logic ...
```

#### Step 4.2: Update Auto-Match API

**File: `src/web/api/faces.py`**

```python
@router.post("/auto-match", response_model=AutoMatchResponse)
def auto_match_faces(
    request: AutoMatchRequest,
    db: Session = Depends(get_db),
) -> AutoMatchResponse:
    """Auto-match with portrait filtering option"""
    
    # Get exclude_portraits from request (default: True)
    exclude_portraits = getattr(request, 'exclude_portraits', True)
    
    matches = find_auto_match_matches(
        db,
        tolerance=request.tolerance,
        exclude_portraits=exclude_portraits,  # NEW
    )
    # ... rest of logic ...
```

**File: `src/web/schemas/faces.py`**

```python
class AutoMatchRequest(BaseModel):
    tolerance: float = Field(0.6, ge=0.0, le=1.0)
    exclude_portraits: bool = Field(True, description="Exclude portrait/profile faces from matching")  # NEW
    exclude_pose_modes: List[str] = Field([], description="Exclude specific pose modes (e.g., ['looking_up', 'tilted'])")  # NEW
    exclude_extreme_angles: bool = Field(True, description="Exclude extreme angle faces (|yaw|>60°, |pitch|>45°, |roll|>45°)")  # NEW
```

#### Step 4.3: Update Frontend Auto-Match UI

**File: `frontend/src/pages/AutoMatch.tsx`**

```typescript
// Add checkbox for excluding portraits
const [excludePortraits, setExcludePortraits] = useState(true)

const startAutoMatch = async () => {
  // ...
  const response = await facesApi.autoMatch({ 
    tolerance,
    exclude_portraits: excludePortraits  // NEW
  })
  // ...
}

// Add UI control
<div className="mb-4">
  <label className="flex items-center gap-2">
    <input
      type="checkbox"
      checked={excludePortraits}
      onChange={(e) => setExcludePortraits(e.target.checked)}
    />
    <span>Exclude portrait/profile faces</span>
  </label>
</div>
```

---

### Phase 5: Update Identify Panel Filtering

**File: `src/web/services/face_service.py` - `find_similar_faces()`**

```python
def find_similar_faces(
    db: Session,
    face_id: int,
    limit: int = 20,
    tolerance: float = 0.6,
    exclude_portraits: bool = False,  # NEW: optional filtering
    exclude_pose_modes: List[str] = None,  # NEW: exclude specific pose modes
    exclude_extreme_angles: bool = False,  # NEW: exclude extreme angles
) -> List[Tuple[Face, float, float]]:
    """Find similar faces with optional pose filtering"""
    
    # ... existing matching logic ...
    
    # Filter by pose if requested
    if exclude_portraits or exclude_pose_modes or exclude_extreme_angles:
        filtered_matches = []
        for face, distance, confidence_pct in matches:
            # Exclude portraits (non-frontal faces)
            if exclude_portraits and face.pose_mode != 'frontal':
                continue
            
            # Exclude specific pose modes
            if exclude_pose_modes and face.pose_mode in exclude_pose_modes:
                continue
            
            # Exclude extreme angles
            if exclude_extreme_angles:
                yaw = abs(face.yaw_angle) if face.yaw_angle else 0
                pitch = abs(face.pitch_angle) if face.pitch_angle else 0
                roll = abs(face.roll_angle) if face.roll_angle else 0
                if yaw > 60 or pitch > 45 or roll > 45:
                    continue
            
            filtered_matches.append((face, distance, confidence_pct))
        
        matches = filtered_matches
    
    return matches[:limit]
```

---

### Phase 6: Testing Strategy

#### Unit Tests

**New file: `tests/test_pose_detection.py`**

```python
import pytest
from src.utils.pose_detection import (
    PoseDetector, 
    calculate_yaw_from_landmarks,
    calculate_pitch_from_landmarks,
    calculate_roll_from_landmarks,
    classify_pose_mode
)

def test_pose_detector_initialization():
    """Test pose detector can be initialized"""
    detector = PoseDetector()
    assert detector.yaw_threshold == 30.0
    assert detector.pitch_threshold == 20.0
    assert detector.roll_threshold == 15.0

def test_yaw_calculation():
    """Test yaw angle calculation from landmarks"""
    # Frontal face landmarks
    frontal_landmarks = {
        'left_eye': (100, 100),
        'right_eye': (200, 100),
        'nose': (150, 150),
        'left_mouth': (120, 200),
        'right_mouth': (180, 200)
    }
    yaw = calculate_yaw_from_landmarks(frontal_landmarks)
    assert abs(yaw) < 30.0, "Frontal face should have low yaw"
    
    # Profile face landmarks (face turned right)
    profile_landmarks = {
        'left_eye': (150, 100),
        'right_eye': (200, 100),
        'nose': (180, 150),  # Nose shifted right
        'left_mouth': (160, 200),
        'right_mouth': (190, 200)
    }
    yaw = calculate_yaw_from_landmarks(profile_landmarks)
    assert abs(yaw) >= 30.0, "Profile face should have high yaw"

def test_pitch_calculation():
    """Test pitch angle calculation from landmarks"""
    # Level face landmarks
    level_landmarks = {
        'left_eye': (100, 100),
        'right_eye': (200, 100),
        'nose': (150, 150),  # Normal nose position
        'left_mouth': (120, 200),
        'right_mouth': (180, 200)
    }
    pitch = calculate_pitch_from_landmarks(level_landmarks)
    assert abs(pitch) < 20.0, "Level face should have low pitch"
    
    # Looking up landmarks (nose higher)
    looking_up_landmarks = {
        'left_eye': (100, 100),
        'right_eye': (200, 100),
        'nose': (150, 120),  # Nose higher than expected
        'left_mouth': (120, 200),
        'right_mouth': (180, 200)
    }
    pitch = calculate_pitch_from_landmarks(looking_up_landmarks)
    assert pitch > 20.0, "Looking up should have positive pitch"

def test_roll_calculation():
    """Test roll angle calculation from landmarks"""
    # Upright face landmarks
    upright_landmarks = {
        'left_eye': (100, 100),
        'right_eye': (200, 100),  # Eyes level
        'nose': (150, 150),
        'left_mouth': (120, 200),
        'right_mouth': (180, 200)
    }
    roll = calculate_roll_from_landmarks(upright_landmarks)
    assert abs(roll) < 15.0, "Upright face should have low roll"
    
    # Tilted face landmarks
    tilted_landmarks = {
        'left_eye': (100, 100),
        'right_eye': (200, 120),  # Right eye lower (tilted right)
        'nose': (150, 150),
        'left_mouth': (120, 200),
        'right_mouth': (180, 200)
    }
    roll = calculate_roll_from_landmarks(tilted_landmarks)
    assert abs(roll) >= 15.0, "Tilted face should have high roll"

def test_pose_mode_classification():
    """Test pose mode classification"""
    # Frontal face
    mode = classify_pose_mode(10.0, 5.0, 3.0)
    assert mode == 'frontal', "Should classify as frontal"
    
    # Profile left
    mode = classify_pose_mode(45.0, 5.0, 3.0)
    assert 'profile_left' in mode, "Should classify as profile_left"
    
    # Looking up
    mode = classify_pose_mode(10.0, 30.0, 3.0)
    assert 'looking_up' in mode, "Should classify as looking_up"
    
    # Tilted right
    mode = classify_pose_mode(10.0, 5.0, 25.0)
    assert 'tilted_right' in mode, "Should classify as tilted_right"
    
    # Combined mode
    mode = classify_pose_mode(45.0, 30.0, 25.0)
    assert 'profile_left' in mode and 'looking_up' in mode, "Should have combined mode"

```

#### Integration Tests

**Test pose detection in face processing pipeline:**
1. Process test images with frontal faces → `pose_mode = 'frontal'`
2. Process test images with profile faces → `pose_mode = 'profile_left'` or `'profile_right'`
3. Process test images with looking up faces → `pose_mode = 'looking_up'`
4. Process test images with tilted faces → `pose_mode = 'tilted_left'` or `'tilted_right'`
5. Process test images with combined poses → `pose_mode = 'profile_left_looking_up'`, etc.
6. Verify pose information (pose_mode, angles) is stored correctly
7. Test auto-match filtering excludes portraits, extreme angles, and specific pose modes

---


---

## Implementation Checklist

### Phase 1: Database Schema
- [ ] Create Alembic migration for pose fields (`pose_mode`, `yaw_angle`, `pitch_angle`, `roll_angle`)
- [ ] Update desktop database schema (`src/core/database.py`)
- [ ] Update SQLAlchemy model (`src/web/db/models.py`)
- [ ] Update `DatabaseManager.add_face()` method signature
- [ ] Run migration on test database
- [ ] Verify schema changes

### Phase 2: Pose Detection Utility
- [ ] Create `src/utils/pose_detection.py`
- [ ] Implement `PoseDetector` class
- [ ] Implement landmark-based yaw calculation
- [ ] Implement landmark-based pitch calculation
- [ ] Implement landmark-based roll calculation
- [ ] Implement pose mode classification logic
- [ ] Write unit tests for pose detection (yaw, pitch, roll)
- [ ] Test with sample images (frontal, profile, looking up/down, tilted, extreme)

### Phase 3: Face Processing Integration
- [ ] Update `src/core/face_processing.py` to use RetinaFace directly
- [ ] Integrate pose detection into processing pipeline **with graceful fallback**
- [ ] Implement face matching logic (RetinaFace ↔ DeepFace) **with defaults if matching fails**
- [ ] Update `src/web/services/face_service.py` **with graceful fallback**
- [ ] Test processing with mixed pose faces (frontal, profile, looking up/down, tilted)
- [ ] Verify pose information in database (pose_mode, angles)
- [ ] **Test backward compatibility: verify processing continues if RetinaFace unavailable**
- [ ] **Test error handling: verify processing continues if pose detection fails**

### Phase 4: Auto-Match Filtering
- [ ] Add pose filtering parameters to auto-match functions (`exclude_portraits`, `exclude_pose_modes`, `exclude_extreme_angles`)
- [ ] Update auto-match API endpoint
- [ ] Update auto-match schema
- [ ] Update frontend auto-match UI with pose filtering options
- [ ] Test auto-match with various pose filtering options enabled/disabled

### Phase 5: Identify Panel Filtering
- [ ] Add optional pose filtering to similar faces
- [ ] Update identify API with pose filtering options (optional)
- [ ] Test identify panel with pose filtering

### Phase 6: Testing
- [ ] Write unit tests for pose detection (yaw, pitch, roll)
- [ ] Write integration tests for face processing with pose detection
- [ ] Write tests for auto-match filtering (all pose modes)
- [ ] Test with real-world images (frontal, profile, looking up/down, tilted, extreme)
- [ ] Performance testing (ensure minimal overhead)
- [ ] Accuracy testing (verify > 90% correct pose classification)
- [ ] **Backward compatibility testing: test with existing databases (add columns, verify queries work)**
- [ ] **Graceful degradation testing: test with RetinaFace unavailable (should use defaults)**
- [ ] **Error handling testing: test with RetinaFace errors (should use defaults, not fail)**
- [ ] **Verify existing `add_face()` calls still work without pose parameters**
- [ ] **Verify face matching still works without pose data**

### Phase 7: Documentation
- [ ] Update README with pose detection feature
- [ ] Document pose modes and filtering options
- [ ] Update API documentation with pose filtering parameters
- [ ] Create migration guide (if needed)
- [ ] Document pose mode classifications and thresholds

---

## Performance Considerations

### Expected Overhead

1. **Additional RetinaFace Call:**
   - RetinaFace is already used by DeepFace internally
   - Direct call adds ~10-50ms per image (depending on image size)
   - Can be optimized by caching results

2. **Landmark Processing:**
   - Yaw calculation is very fast (< 1ms per face)
   - Negligible performance impact

3. **Database:**
   - New pose_mode field: text string (50 chars max, ~50 bytes per face)
   - Optional yaw_angle, pitch_angle, roll_angle: 8 bytes each if stored
   - Index on `pose_mode` for fast filtering

### Optimization Strategies

1. **Cache RetinaFace Results:**
   - Store RetinaFace detection results temporarily
   - Reuse for both DeepFace and pose detection

2. **Parallel Processing:**
   - Run RetinaFace and DeepFace in parallel (if possible)
   - Combine results afterwards

3. **Lazy Evaluation:**
   - Only run pose detection if explicitly requested
   - Make it optional via configuration

---

## Configuration Options

### Add to `src/core/config.py`:

```python
# Pose Detection Settings
ENABLE_POSE_DETECTION = True  # Enable/disable pose detection
POSE_YAW_THRESHOLD = 30.0  # Yaw angle threshold for profile detection (degrees)
POSE_PITCH_THRESHOLD = 20.0  # Pitch angle threshold for up/down detection (degrees)
POSE_ROLL_THRESHOLD = 15.0  # Roll angle threshold for tilt detection (degrees)
STORE_POSE_ANGLES = True  # Store yaw/pitch/roll angles in database (optional)
EXCLUDE_NON_FRONTAL_IN_AUTOMATCH = True  # Default auto-match behavior (exclude non-frontal faces)
EXCLUDE_EXTREME_ANGLES_IN_AUTOMATCH = True  # Exclude extreme angles by default
```

---

## Success Criteria

1. ✅ Face poses (yaw, pitch, roll) are automatically detected during processing
2. ✅ Pose information (pose_mode, angles) is stored in database for all faces
3. ✅ Auto-match can filter faces by pose mode (profile, looking up/down, tilted, extreme angles)
4. ✅ Performance impact is minimal (< 10% processing time increase)
5. ✅ Accuracy: > 90% correct classification of pose modes (frontal, profile, looking up/down, tilted)
6. ✅ Support for combined pose modes (e.g., profile_left_looking_up_tilted_right)
7. ✅ Clean database implementation - all faces have pose data from the start

---

## Risks and Mitigation

### Risk 1: False Positives/Negatives
- **Risk:** Profile detection may misclassify some faces
- **Mitigation:** Tune threshold based on testing, allow manual override

### Risk 2: Performance Impact
- **Risk:** Additional RetinaFace call slows processing
- **Mitigation:** Optimize by caching results, make it optional

### Risk 3: RetinaFace Dependency
- **Risk:** RetinaFace may not be available or may fail
- **Mitigation:** Graceful fallback to default (pose_mode = 'frontal')

### Risk 4: Matching Accuracy
- **Risk:** Matching RetinaFace and DeepFace results may be inaccurate
- **Mitigation:** Use bounding box overlap for matching, test thoroughly

### Risk 5: Clean Database Requirements
- **Risk:** Database will be wiped - all existing data will be lost
- **Mitigation:** This is intentional - plan assumes fresh database start

---

## Backward Compatibility & Graceful Degradation

### Make Pose Detection Optional with Graceful Fallback

To ensure existing functionality continues to work without disruption, pose detection must be implemented with graceful fallback mechanisms:

#### 1. **RetinaFace Availability Check**
- Check if RetinaFace is available before attempting pose detection
- If RetinaFace is not available, skip pose detection and use defaults
- Log warnings but do not fail face processing

```python
# In face_processing.py
if RETINAFACE_AVAILABLE:
    try:
        pose_faces = self.pose_detector.detect_pose_faces(photo_path)
    except Exception as e:
        print(f"⚠️  Pose detection failed: {e}, using defaults")
        pose_faces = []  # Fallback to defaults
else:
    pose_faces = []  # RetinaFace not available, use defaults
```

#### 2. **Default Values for Missing Pose Data**
- If pose detection fails or is unavailable, use safe defaults:
  - `pose_mode = 'frontal'` (assumes frontal face)
  - `yaw_angle = None`
  - `pitch_angle = None`
  - `roll_angle = None`
- All existing faces without pose data will default to `'frontal'`
- New faces processed without pose detection will also use defaults

#### 3. **Database Schema Defaults**
- All new columns have default values:
  - `pose_mode TEXT DEFAULT 'frontal'` (NOT NULL with default)
  - `yaw_angle REAL DEFAULT NULL` (nullable)
  - `pitch_angle REAL DEFAULT NULL` (nullable)
  - `roll_angle REAL DEFAULT NULL` (nullable)
- Existing queries will continue to work (NULL values for angles are acceptable)
- Existing faces will automatically get `pose_mode = 'frontal'` when schema is updated

#### 4. **Method Signature Compatibility**
- `add_face()` method signature adds new optional parameters with defaults:
  ```python
  def add_face(self, ..., 
               pose_mode: str = 'frontal',  # Default value
               yaw_angle: Optional[float] = None,  # Optional
               pitch_angle: Optional[float] = None,  # Optional
               roll_angle: Optional[float] = None)  # Optional
  ```
- All existing calls to `add_face()` will continue to work without modification
- New parameters are optional and backward compatible

#### 5. **Error Handling in Face Processing**
- If RetinaFace detection fails for a specific photo:
  - Log the error but continue processing
  - Use default pose values (`frontal`, `None` angles)
  - Do not fail the entire photo processing
- If pose matching between RetinaFace and DeepFace fails:
  - Use default pose values
  - Log warning but continue processing

#### 6. **Configuration Flag (Optional)**
- Add configuration option to enable/disable pose detection:
  ```python
  # In config.py
  ENABLE_POSE_DETECTION = True  # Can be disabled if needed
  ```
- If disabled, skip RetinaFace calls entirely and use defaults
- Allows users to disable feature if experiencing issues

#### 7. **Graceful Degradation Benefits**
- **Existing functionality preserved:** All current features continue to work
- **No breaking changes:** Database queries, face matching, auto-match all work
- **Progressive enhancement:** Pose detection adds value when available, but doesn't break when unavailable
- **Performance fallback:** If RetinaFace is slow or unavailable, processing continues without pose data

#### 8. **Testing Backward Compatibility**
- Test with existing databases (add columns, verify queries still work)
- Test with RetinaFace unavailable (should use defaults)
- Test with RetinaFace errors (should use defaults, not fail)
- Verify existing `add_face()` calls still work
- Verify face matching still works without pose data

---

## Future Enhancements

1. **Landmark Visualization:**
   - Show landmarks in UI for debugging
   - Visualize pose angles (yaw, pitch, roll)

2. **Advanced Filtering:**
   - Filter by specific angle ranges (e.g., yaw between -45° and 45°)
   - Filter by individual pose modes (e.g., only profile_left, exclude looking_up)
   - Custom pose mode combinations

3. **Quality-Based Filtering:**
   - Combine pose information with quality score
   - Filter low-quality faces with extreme angles
   - Prefer frontal faces for matching

4. **Pose Estimation Refinement:**
   - Use more sophisticated algorithms (e.g., 3D face model fitting)
   - Improve accuracy for edge cases
   - Handle occluded faces better

5. **UI Enhancements:**
   - Display pose information in face details
   - Visual indicators for pose modes (icons, colors)
   - Pose-based photo organization

---

## Timeline Estimate

- **Phase 1 (Database):** 1-2 days
- **Phase 2 (Pose Detection Utility):** 3-4 days (includes yaw, pitch, roll calculations)
- **Phase 3 (Face Processing Integration):** 3-4 days
- **Phase 4 (Auto-Match Filtering):** 2-3 days (includes all pose filtering options)
- **Phase 5 (Identify Panel):** 1-2 days
- **Phase 6 (Testing):** 3-4 days (testing all pose modes)
- **Phase 7 (Documentation):** 1-2 days

**Total Estimate:** 14-21 days

---

## Conclusion

This plan provides a comprehensive approach to implementing automatic face pose detection (yaw, pitch, roll) using RetinaFace directly. The implementation will enable automatic classification of face poses into multiple modes (frontal, profile, looking up/down, tilted, and combinations) and intelligent filtering in auto-match and other features.

**Key Features:**
- **Multiple Pose Modes:** Detects yaw (profile), pitch (looking up/down), and roll (tilted) angles
- **Combined Classifications:** Supports combined pose modes (e.g., profile_left_looking_up_tilted_right)
- **Flexible Filtering:** Multiple filtering options (exclude portraits, exclude specific pose modes, exclude extreme angles)
- **Clean Database Design:** All faces have pose data from the start - no migration needed
- **Performance Optimized:** Minimal overhead with efficient angle calculations

The phased approach ensures incremental progress with testing at each stage, minimizing risk and allowing for adjustments based on real-world testing results. This comprehensive pose detection system will significantly improve face matching accuracy and user experience by intelligently filtering out low-quality or difficult-to-match face poses.

