# Pose Detection Investigation & Fixes

## Summary

Investigated two issues with pose detection in the faces table:
1. **Pitch angles not being calculated** - All pitch angles were `NULL` in the database
2. **Roll angle normalization** - Roll angles were showing extreme values near ±180° instead of normalized [-90, 90] range

## Issue 1: Pitch Angles Not Calculated

### Root Cause
RetinaFace returns landmark keys as:
- `'mouth_left'` and `'mouth_right'`

But the code was looking for:
- `'left_mouth'` and `'right_mouth'`

This mismatch caused `calculate_pitch_from_landmarks()` to always return `None` because it couldn't find the required landmarks.

### Fix
Updated `calculate_pitch_from_landmarks()` in `src/utils/pose_detection.py` to handle both naming conventions:

```python
# RetinaFace uses 'mouth_left' and 'mouth_right', not 'left_mouth' and 'right_mouth'
left_mouth = landmarks.get('mouth_left') or landmarks.get('left_mouth')
right_mouth = landmarks.get('mouth_right') or landmarks.get('right_mouth')
```

### Result
Pitch angles are now being calculated correctly. Test results show:
- Face 1: Pitch = 18.35° (looking up)
- Face 2: Pitch = -2.96° (slightly down)
- Face 3: Pitch = 3.49° (slightly up)

## Issue 2: Roll Angle Normalization

### Root Cause
The `calculate_roll_from_landmarks()` function uses `atan2(dy, dx)` which returns angles in the range [-180, 180] degrees. When the eye line is nearly horizontal (which is normal for most faces), small values of `dy` relative to `dx` can result in angles near ±180° instead of near 0°.

For example:
- If `dx = -92.94` and `dy = -2.80` (eyes nearly horizontal)
- `atan2(-2.80, -92.94) = -178.28°` (should be ~1.72°)

### Fix
Added normalization to convert roll angles to [-90, 90] range in `calculate_roll_from_landmarks()`:

```python
# Roll angle - atan2 returns [-180, 180], normalize to [-90, 90]
roll_radians = atan2(dy, dx)
roll_degrees = degrees(roll_radians)

# Normalize to [-90, 90] range for head tilt
# If angle is > 90°, subtract 180°; if < -90°, add 180°
if roll_degrees > 90.0:
    roll_degrees = roll_degrees - 180.0
elif roll_degrees < -90.0:
    roll_degrees = roll_degrees + 180.0
```

### Result
Roll angles are now normalized correctly. Database examples:
- Before: -179.18° → After: 0.82°
- Before: 177.25° → After: -2.75°
- Before: -178.97° → After: 1.03°

## Testing

The fixes were tested using `scripts/test_pose_calculation.py`:
- ✅ Pitch angles now calculate correctly
- ✅ Roll angles are normalized to [-90, 90] range
- ✅ All three angles (yaw, pitch, roll) are working as expected

## Database Impact

### Current State (Web Database)
- Total faces: 163
- Faces with angle data: 10 (only those with non-frontal poses)
- Pitch angles: 0 (all NULL before fix)
- Roll angles: 10 (had extreme values before fix)

### After Fix
- Pitch angles will be calculated for all faces processed after the fix
- Roll angles will be normalized to [-90, 90] range
- Existing faces in database will need to be reprocessed to get pitch angles and normalized roll angles

## Files Modified

1. `src/utils/pose_detection.py`
   - `calculate_pitch_from_landmarks()`: Fixed landmark key names
   - `calculate_roll_from_landmarks()`: Added normalization to [-90, 90] range

## Next Steps

1. **Reprocess existing faces** (optional): To get pitch angles and normalized roll angles for existing faces, reprocess photos through the face detection pipeline
2. **Monitor new faces**: New faces processed after the fix will automatically have correct pitch and roll angles
3. **Update database migration** (if needed): Consider adding a migration script to normalize existing roll angles in the database

