feat: Add pose mode analysis and face width detection for improved profile classification
This commit introduces a comprehensive analysis of pose modes and face width detection to enhance profile classification accuracy. New scripts have been added to analyze pose data in the database, check identified faces for pose information, and validate yaw angles. The PoseDetector class has been updated to calculate face width from landmarks, which serves as an additional indicator for profile detection. The frontend and API have been modified to include pose mode in responses, ensuring better integration with existing functionalities. Documentation has been updated to reflect these changes, improving user experience and accuracy in face processing.
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
# Face Width-Based Profile Detection Implementation
|
||||
|
||||
## Overview
|
||||
|
||||
Implemented face width (eye distance) as an additional indicator for profile face detection. This enhances profile detection accuracy, especially for extreme profile views where yaw angle calculation might be less reliable.
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### 1. Added `calculate_face_width_from_landmarks()` Method
|
||||
|
||||
**Location:** `src/utils/pose_detection.py`
|
||||
|
||||
Calculates the horizontal distance between the two eyes (face width). For profile faces, this distance is very small (< 20-30 pixels), while frontal faces have much larger eye distances (typically 50-100+ pixels).
|
||||
|
||||
```python
|
||||
@staticmethod
|
||||
def calculate_face_width_from_landmarks(landmarks: Dict) -> Optional[float]:
|
||||
"""Calculate face width (eye distance) from facial landmarks."""
|
||||
```
|
||||
|
||||
### 2. Enhanced `classify_pose_mode()` Method
|
||||
|
||||
**Location:** `src/utils/pose_detection.py`
|
||||
|
||||
Added optional `face_width` parameter to `classify_pose_mode()`:
|
||||
|
||||
```python
|
||||
@staticmethod
|
||||
def classify_pose_mode(yaw: Optional[float],
|
||||
pitch: Optional[float],
|
||||
roll: Optional[float],
|
||||
face_width: Optional[float] = None) -> str:
|
||||
```
|
||||
|
||||
**Logic:**
|
||||
- If `face_width < 25 pixels`, strongly indicates profile view
|
||||
- Even if yaw angle is below the 30° threshold, small face width suggests profile
|
||||
- Uses yaw direction (if available) to determine `profile_left` vs `profile_right`
|
||||
- Falls back to `profile_left` if yaw is unavailable but face width is small
|
||||
|
||||
### 3. Updated `detect_pose_faces()` Method
|
||||
|
||||
**Location:** `src/utils/pose_detection.py`
|
||||
|
||||
Now calculates face width and includes it in the result:
|
||||
|
||||
```python
|
||||
# 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)
|
||||
```
|
||||
|
||||
The result dictionary now includes `face_width`:
|
||||
|
||||
```python
|
||||
result = {
|
||||
'facial_area': facial_area,
|
||||
'landmarks': landmarks,
|
||||
'confidence': face_data.get('confidence', 0.0),
|
||||
'yaw_angle': yaw_angle,
|
||||
'pitch_angle': pitch_angle,
|
||||
'roll_angle': roll_angle,
|
||||
'face_width': face_width, # Eye distance in pixels
|
||||
'pose_mode': pose_mode
|
||||
}
|
||||
```
|
||||
|
||||
## Benefits
|
||||
|
||||
1. **Better Profile Detection:** Catches extreme profile views where yaw angle might be unreliable
|
||||
2. **Fallback Indicator:** When yaw calculation fails (None), small face width still indicates profile
|
||||
3. **More Accurate:** Uses both yaw angle and face width for robust profile detection
|
||||
4. **Backward Compatible:** `face_width` parameter is optional, so existing code still works
|
||||
|
||||
## Threshold
|
||||
|
||||
**Profile Face Width Threshold: 25 pixels**
|
||||
|
||||
- Faces with eye distance < 25 pixels are classified as profile
|
||||
- This threshold is based on empirical testing:
|
||||
- Profile faces: 6-10 pixels (e.g., face_4 with yaw=-30.48° has 6.52 pixels)
|
||||
- Frontal faces: 50-100+ pixels (e.g., face_2 with yaw=3.04° has 92.94 pixels)
|
||||
|
||||
## Testing
|
||||
|
||||
To test the implementation:
|
||||
|
||||
1. Process photos with profile faces
|
||||
2. Check pose_mode classification - should see more profile faces detected
|
||||
3. Verify face_width values in pose detection results
|
||||
|
||||
## Example
|
||||
|
||||
**Before:** Face with yaw=16.77° might be classified as "frontal" (below 30° threshold)
|
||||
**After:** Same face with face_width=3.86 pixels is correctly classified as "profile_left"
|
||||
|
||||
## Files Modified
|
||||
|
||||
- `src/utils/pose_detection.py`:
|
||||
- Added `calculate_face_width_from_landmarks()` method
|
||||
- Enhanced `classify_pose_mode()` with face_width parameter
|
||||
- Updated `detect_pose_faces()` to calculate and use face_width
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
# 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
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Analyze all faces to see why most don't have angle data
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
import os
|
||||
|
||||
db_path = "data/punimtag.db"
|
||||
|
||||
if not os.path.exists(db_path):
|
||||
print(f"❌ Database not found: {db_path}")
|
||||
exit(1)
|
||||
|
||||
conn = sqlite3.connect(db_path)
|
||||
conn.row_factory = sqlite3.Row
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Get total faces
|
||||
cursor.execute("SELECT COUNT(*) FROM faces")
|
||||
total_faces = cursor.fetchone()[0]
|
||||
|
||||
# Get faces with angle data
|
||||
cursor.execute("SELECT COUNT(*) FROM faces WHERE yaw_angle IS NOT NULL OR pitch_angle IS NOT NULL OR roll_angle IS NOT NULL")
|
||||
faces_with_angles = cursor.fetchone()[0]
|
||||
|
||||
# Get faces without any angle data
|
||||
faces_without_angles = total_faces - faces_with_angles
|
||||
|
||||
print("=" * 80)
|
||||
print("FACE ANGLE DATA ANALYSIS")
|
||||
print("=" * 80)
|
||||
print(f"\nTotal faces: {total_faces}")
|
||||
print(f"Faces WITH angle data: {faces_with_angles}")
|
||||
print(f"Faces WITHOUT angle data: {faces_without_angles}")
|
||||
print(f"Percentage with angle data: {(faces_with_angles/total_faces*100):.1f}%")
|
||||
|
||||
# Check pose_mode distribution
|
||||
print("\n" + "=" * 80)
|
||||
print("POSE_MODE DISTRIBUTION")
|
||||
print("=" * 80)
|
||||
cursor.execute("""
|
||||
SELECT pose_mode, COUNT(*) as count
|
||||
FROM faces
|
||||
GROUP BY pose_mode
|
||||
ORDER BY count DESC
|
||||
""")
|
||||
|
||||
pose_modes = cursor.fetchall()
|
||||
for row in pose_modes:
|
||||
percentage = (row['count'] / total_faces) * 100
|
||||
print(f" {row['pose_mode']:<30} : {row['count']:>4} ({percentage:>5.1f}%)")
|
||||
|
||||
# Check faces with pose_mode=frontal but might have high yaw
|
||||
print("\n" + "=" * 80)
|
||||
print("FACES WITH POSE_MODE='frontal' BUT NO ANGLE DATA")
|
||||
print("=" * 80)
|
||||
print("(These faces might actually be profile faces but weren't analyzed)")
|
||||
|
||||
cursor.execute("""
|
||||
SELECT COUNT(*)
|
||||
FROM faces
|
||||
WHERE pose_mode = 'frontal'
|
||||
AND yaw_angle IS NULL
|
||||
AND pitch_angle IS NULL
|
||||
AND roll_angle IS NULL
|
||||
""")
|
||||
frontal_no_data = cursor.fetchone()[0]
|
||||
print(f" Faces with pose_mode='frontal' and no angle data: {frontal_no_data}")
|
||||
|
||||
# Check if pose detection is being run for all faces
|
||||
print("\n" + "=" * 80)
|
||||
print("ANALYSIS")
|
||||
print("=" * 80)
|
||||
print(f"Only {faces_with_angles} out of {total_faces} faces have angle data stored.")
|
||||
print("This suggests that pose detection is NOT being run for all faces.")
|
||||
print("\nPossible reasons:")
|
||||
print(" 1. Pose detection may have been disabled or failed for most faces")
|
||||
print(" 2. Only faces processed recently have pose data")
|
||||
print(" 3. Pose detection might only run when RetinaFace is available")
|
||||
|
||||
conn.close()
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Analyze why only 6 faces have yaw angle data - investigate the matching process
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
import os
|
||||
import json
|
||||
|
||||
db_path = "data/punimtag.db"
|
||||
|
||||
if not os.path.exists(db_path):
|
||||
print(f"❌ Database not found: {db_path}")
|
||||
exit(1)
|
||||
|
||||
conn = sqlite3.connect(db_path)
|
||||
conn.row_factory = sqlite3.Row
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Get total faces
|
||||
cursor.execute("SELECT COUNT(*) FROM faces")
|
||||
total_faces = cursor.fetchone()[0]
|
||||
|
||||
# Get faces with angle data
|
||||
cursor.execute("SELECT COUNT(*) FROM faces WHERE yaw_angle IS NOT NULL")
|
||||
faces_with_yaw = cursor.fetchone()[0]
|
||||
|
||||
# Get faces without angle data
|
||||
cursor.execute("SELECT COUNT(*) FROM faces WHERE yaw_angle IS NULL AND pitch_angle IS NULL AND roll_angle IS NULL")
|
||||
faces_without_angles = cursor.fetchone()[0]
|
||||
|
||||
print("=" * 80)
|
||||
print("POSE DATA COVERAGE ANALYSIS")
|
||||
print("=" * 80)
|
||||
print(f"\nTotal faces: {total_faces}")
|
||||
print(f"Faces WITH yaw angle: {faces_with_yaw}")
|
||||
print(f"Faces WITHOUT any angle data: {faces_without_angles}")
|
||||
print(f"Coverage: {(faces_with_yaw/total_faces*100):.1f}%")
|
||||
|
||||
# Check pose_mode distribution
|
||||
print("\n" + "=" * 80)
|
||||
print("POSE_MODE DISTRIBUTION")
|
||||
print("=" * 80)
|
||||
cursor.execute("""
|
||||
SELECT pose_mode, COUNT(*) as count,
|
||||
SUM(CASE WHEN yaw_angle IS NOT NULL THEN 1 ELSE 0 END) as with_yaw,
|
||||
SUM(CASE WHEN pitch_angle IS NOT NULL THEN 1 ELSE 0 END) as with_pitch,
|
||||
SUM(CASE WHEN roll_angle IS NOT NULL THEN 1 ELSE 0 END) as with_roll
|
||||
FROM faces
|
||||
GROUP BY pose_mode
|
||||
ORDER BY count DESC
|
||||
""")
|
||||
|
||||
pose_modes = cursor.fetchall()
|
||||
for row in pose_modes:
|
||||
print(f"\n{row['pose_mode']}:")
|
||||
print(f" Total: {row['count']}")
|
||||
print(f" With yaw: {row['with_yaw']}")
|
||||
print(f" With pitch: {row['with_pitch']}")
|
||||
print(f" With roll: {row['with_roll']}")
|
||||
|
||||
# Check photos and see if some photos have pose data while others don't
|
||||
print("\n" + "=" * 80)
|
||||
print("POSE DATA BY PHOTO")
|
||||
print("=" * 80)
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
p.id as photo_id,
|
||||
p.filename,
|
||||
COUNT(f.id) as total_faces,
|
||||
SUM(CASE WHEN f.yaw_angle IS NOT NULL THEN 1 ELSE 0 END) as faces_with_yaw,
|
||||
SUM(CASE WHEN f.pitch_angle IS NOT NULL THEN 1 ELSE 0 END) as faces_with_pitch,
|
||||
SUM(CASE WHEN f.roll_angle IS NOT NULL THEN 1 ELSE 0 END) as faces_with_roll
|
||||
FROM photos p
|
||||
LEFT JOIN faces f ON f.photo_id = p.id
|
||||
GROUP BY p.id, p.filename
|
||||
HAVING COUNT(f.id) > 0
|
||||
ORDER BY faces_with_yaw DESC, total_faces DESC
|
||||
LIMIT 20
|
||||
""")
|
||||
|
||||
photos = cursor.fetchall()
|
||||
print(f"\n{'Photo ID':<10} {'Filename':<40} {'Total':<8} {'Yaw':<6} {'Pitch':<7} {'Roll':<6}")
|
||||
print("-" * 80)
|
||||
for row in photos:
|
||||
print(f"{row['photo_id']:<10} {row['filename'][:38]:<40} {row['total_faces']:<8} "
|
||||
f"{row['faces_with_yaw']:<6} {row['faces_with_pitch']:<7} {row['faces_with_roll']:<6}")
|
||||
|
||||
# Check if there's a pattern - maybe older photos don't have pose data
|
||||
print("\n" + "=" * 80)
|
||||
print("ANALYSIS")
|
||||
print("=" * 80)
|
||||
|
||||
# Check date added vs pose data
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
DATE(p.date_added) as date_added,
|
||||
COUNT(f.id) as total_faces,
|
||||
SUM(CASE WHEN f.yaw_angle IS NOT NULL THEN 1 ELSE 0 END) as faces_with_yaw
|
||||
FROM photos p
|
||||
LEFT JOIN faces f ON f.photo_id = p.id
|
||||
GROUP BY DATE(p.date_added)
|
||||
ORDER BY date_added DESC
|
||||
""")
|
||||
|
||||
dates = cursor.fetchall()
|
||||
print("\nFaces by date added:")
|
||||
print(f"{'Date':<15} {'Total':<8} {'With Yaw':<10} {'Coverage':<10}")
|
||||
print("-" * 50)
|
||||
for row in dates:
|
||||
coverage = (row['faces_with_yaw'] / row['total_faces'] * 100) if row['total_faces'] > 0 else 0
|
||||
print(f"{row['date_added'] or 'NULL':<15} {row['total_faces']:<8} {row['faces_with_yaw']:<10} {coverage:.1f}%")
|
||||
|
||||
# Check if pose detection might be failing for some photos
|
||||
print("\n" + "=" * 80)
|
||||
print("POSSIBLE REASONS FOR LOW COVERAGE")
|
||||
print("=" * 80)
|
||||
print("\n1. Pose detection might not be running for all photos")
|
||||
print("2. Matching between DeepFace and RetinaFace might be failing (IoU threshold too strict?)")
|
||||
print("3. RetinaFace might not be detecting faces in some photos")
|
||||
print("4. Photos might have been processed before pose detection was fully implemented")
|
||||
|
||||
# Check if there are photos with multiple faces where some have pose data and some don't
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
p.id as photo_id,
|
||||
p.filename,
|
||||
COUNT(f.id) as total_faces,
|
||||
SUM(CASE WHEN f.yaw_angle IS NOT NULL THEN 1 ELSE 0 END) as faces_with_yaw,
|
||||
SUM(CASE WHEN f.yaw_angle IS NULL THEN 1 ELSE 0 END) as faces_without_yaw
|
||||
FROM photos p
|
||||
JOIN faces f ON f.photo_id = p.id
|
||||
GROUP BY p.id, p.filename
|
||||
HAVING COUNT(f.id) > 1
|
||||
AND SUM(CASE WHEN f.yaw_angle IS NOT NULL THEN 1 ELSE 0 END) > 0
|
||||
AND SUM(CASE WHEN f.yaw_angle IS NULL THEN 1 ELSE 0 END) > 0
|
||||
ORDER BY total_faces DESC
|
||||
LIMIT 10
|
||||
""")
|
||||
|
||||
mixed_photos = cursor.fetchall()
|
||||
if mixed_photos:
|
||||
print("\n" + "=" * 80)
|
||||
print("PHOTOS WITH MIXED POSE DATA (some faces have it, some don't)")
|
||||
print("=" * 80)
|
||||
print(f"\n{'Photo ID':<10} {'Filename':<40} {'Total':<8} {'With Yaw':<10} {'Without Yaw':<12}")
|
||||
print("-" * 80)
|
||||
for row in mixed_photos:
|
||||
print(f"{row['photo_id']:<10} {row['filename'][:38]:<40} {row['total_faces']:<8} "
|
||||
f"{row['faces_with_yaw']:<10} {row['faces_without_yaw']:<12}")
|
||||
print("\n⚠️ This suggests matching is failing for some faces even when pose detection runs")
|
||||
else:
|
||||
print("\n✅ No photos found with mixed pose data (all or nothing per photo)")
|
||||
|
||||
conn.close()
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Analyze pose_mode values in the faces table
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
import sys
|
||||
import os
|
||||
from collections import Counter
|
||||
from typing import Dict, List, Tuple
|
||||
|
||||
# Default database path
|
||||
DEFAULT_DB_PATH = "data/photos.db"
|
||||
|
||||
|
||||
def analyze_poses(db_path: str) -> None:
|
||||
"""Analyze pose_mode values in faces table"""
|
||||
|
||||
if not os.path.exists(db_path):
|
||||
print(f"❌ Database not found: {db_path}")
|
||||
return
|
||||
|
||||
print(f"📊 Analyzing poses in database: {db_path}\n")
|
||||
|
||||
try:
|
||||
conn = sqlite3.connect(db_path)
|
||||
conn.row_factory = sqlite3.Row
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Get total number of faces
|
||||
cursor.execute("SELECT COUNT(*) FROM faces")
|
||||
total_faces = cursor.fetchone()[0]
|
||||
print(f"Total faces in database: {total_faces}\n")
|
||||
|
||||
if total_faces == 0:
|
||||
print("No faces found in database.")
|
||||
conn.close()
|
||||
return
|
||||
|
||||
# Get pose_mode distribution
|
||||
cursor.execute("""
|
||||
SELECT pose_mode, COUNT(*) as count
|
||||
FROM faces
|
||||
GROUP BY pose_mode
|
||||
ORDER BY count DESC
|
||||
""")
|
||||
|
||||
pose_modes = cursor.fetchall()
|
||||
|
||||
print("=" * 60)
|
||||
print("POSE_MODE DISTRIBUTION")
|
||||
print("=" * 60)
|
||||
for row in pose_modes:
|
||||
pose_mode = row['pose_mode'] or 'NULL'
|
||||
count = row['count']
|
||||
percentage = (count / total_faces) * 100
|
||||
print(f" {pose_mode:30s} : {count:6d} ({percentage:5.1f}%)")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("ANGLE STATISTICS")
|
||||
print("=" * 60)
|
||||
|
||||
# Yaw angle statistics
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
COUNT(*) as total,
|
||||
COUNT(yaw_angle) as with_yaw,
|
||||
MIN(yaw_angle) as min_yaw,
|
||||
MAX(yaw_angle) as max_yaw,
|
||||
AVG(yaw_angle) as avg_yaw
|
||||
FROM faces
|
||||
WHERE yaw_angle IS NOT NULL
|
||||
""")
|
||||
yaw_stats = cursor.fetchone()
|
||||
|
||||
# Pitch angle statistics
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
COUNT(*) as total,
|
||||
COUNT(pitch_angle) as with_pitch,
|
||||
MIN(pitch_angle) as min_pitch,
|
||||
MAX(pitch_angle) as max_pitch,
|
||||
AVG(pitch_angle) as avg_pitch
|
||||
FROM faces
|
||||
WHERE pitch_angle IS NOT NULL
|
||||
""")
|
||||
pitch_stats = cursor.fetchone()
|
||||
|
||||
# Roll angle statistics
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
COUNT(*) as total,
|
||||
COUNT(roll_angle) as with_roll,
|
||||
MIN(roll_angle) as min_roll,
|
||||
MAX(roll_angle) as max_roll,
|
||||
AVG(roll_angle) as avg_roll
|
||||
FROM faces
|
||||
WHERE roll_angle IS NOT NULL
|
||||
""")
|
||||
roll_stats = cursor.fetchone()
|
||||
|
||||
print(f"\nYaw Angle:")
|
||||
print(f" Faces with yaw data: {yaw_stats['with_yaw']}")
|
||||
if yaw_stats['with_yaw'] > 0:
|
||||
print(f" Min: {yaw_stats['min_yaw']:.1f}°")
|
||||
print(f" Max: {yaw_stats['max_yaw']:.1f}°")
|
||||
print(f" Avg: {yaw_stats['avg_yaw']:.1f}°")
|
||||
|
||||
print(f"\nPitch Angle:")
|
||||
print(f" Faces with pitch data: {pitch_stats['with_pitch']}")
|
||||
if pitch_stats['with_pitch'] > 0:
|
||||
print(f" Min: {pitch_stats['min_pitch']:.1f}°")
|
||||
print(f" Max: {pitch_stats['max_pitch']:.1f}°")
|
||||
print(f" Avg: {pitch_stats['avg_pitch']:.1f}°")
|
||||
|
||||
print(f"\nRoll Angle:")
|
||||
print(f" Faces with roll data: {roll_stats['with_roll']}")
|
||||
if roll_stats['with_roll'] > 0:
|
||||
print(f" Min: {roll_stats['min_roll']:.1f}°")
|
||||
print(f" Max: {roll_stats['max_roll']:.1f}°")
|
||||
print(f" Avg: {roll_stats['avg_roll']:.1f}°")
|
||||
|
||||
# Sample faces with different poses
|
||||
print("\n" + "=" * 60)
|
||||
print("SAMPLE FACES BY POSE")
|
||||
print("=" * 60)
|
||||
|
||||
for row in pose_modes[:10]: # Top 10 pose modes
|
||||
pose_mode = row['pose_mode']
|
||||
cursor.execute("""
|
||||
SELECT id, photo_id, pose_mode, yaw_angle, pitch_angle, roll_angle
|
||||
FROM faces
|
||||
WHERE pose_mode = ?
|
||||
LIMIT 3
|
||||
""", (pose_mode,))
|
||||
samples = cursor.fetchall()
|
||||
|
||||
print(f"\n{pose_mode}:")
|
||||
for sample in samples:
|
||||
yaw_str = f"{sample['yaw_angle']:.1f}°" if sample['yaw_angle'] is not None else "N/A"
|
||||
pitch_str = f"{sample['pitch_angle']:.1f}°" if sample['pitch_angle'] is not None else "N/A"
|
||||
roll_str = f"{sample['roll_angle']:.1f}°" if sample['roll_angle'] is not None else "N/A"
|
||||
print(f" Face ID {sample['id']}: "
|
||||
f"yaw={yaw_str} "
|
||||
f"pitch={pitch_str} "
|
||||
f"roll={roll_str}")
|
||||
|
||||
conn.close()
|
||||
|
||||
except sqlite3.Error as e:
|
||||
print(f"❌ Database error: {e}")
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
|
||||
|
||||
def check_web_database() -> None:
|
||||
"""Check if web database exists and analyze it"""
|
||||
# Common web database locations
|
||||
web_db_paths = [
|
||||
"data/punimtag.db", # Default web database
|
||||
"data/web_photos.db",
|
||||
"data/photos_web.db",
|
||||
"web_photos.db",
|
||||
]
|
||||
|
||||
for db_path in web_db_paths:
|
||||
if os.path.exists(db_path):
|
||||
print(f"\n{'='*60}")
|
||||
print(f"WEB DATABASE: {db_path}")
|
||||
print(f"{'='*60}\n")
|
||||
analyze_poses(db_path)
|
||||
break
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Check desktop database
|
||||
desktop_db = DEFAULT_DB_PATH
|
||||
if os.path.exists(desktop_db):
|
||||
analyze_poses(desktop_db)
|
||||
|
||||
# Check web database
|
||||
check_web_database()
|
||||
|
||||
# If no database found, list what we tried
|
||||
if not os.path.exists(desktop_db):
|
||||
print(f"❌ Desktop database not found: {desktop_db}")
|
||||
print("\nTrying to find database files...")
|
||||
for root, dirs, files in os.walk("data"):
|
||||
for file in files:
|
||||
if file.endswith(('.db', '.sqlite', '.sqlite3')):
|
||||
print(f" Found: {os.path.join(root, file)}")
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Check all identified faces for pose information"""
|
||||
|
||||
import sqlite3
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Add project root to path
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from src.core.config import DEFAULT_DB_PATH
|
||||
|
||||
def check_identified_faces(db_path: str):
|
||||
"""Check all identified faces for pose information"""
|
||||
if not os.path.exists(db_path):
|
||||
print(f"Database not found: {db_path}")
|
||||
return
|
||||
|
||||
conn = sqlite3.connect(db_path)
|
||||
conn.row_factory = sqlite3.Row
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Get all identified faces with pose information
|
||||
cursor.execute('''
|
||||
SELECT
|
||||
f.id,
|
||||
f.person_id,
|
||||
p.name || ' ' || p.last_name as person_name,
|
||||
ph.filename,
|
||||
f.pose_mode,
|
||||
f.yaw_angle,
|
||||
f.pitch_angle,
|
||||
f.roll_angle,
|
||||
f.face_confidence,
|
||||
f.quality_score,
|
||||
f.location
|
||||
FROM faces f
|
||||
JOIN people p ON f.person_id = p.id
|
||||
JOIN photos ph ON f.photo_id = ph.id
|
||||
WHERE f.person_id IS NOT NULL
|
||||
ORDER BY p.id, f.id
|
||||
''')
|
||||
|
||||
faces = cursor.fetchall()
|
||||
|
||||
if not faces:
|
||||
print("No identified faces found.")
|
||||
return
|
||||
|
||||
print(f"\n{'='*80}")
|
||||
print(f"Found {len(faces)} identified faces")
|
||||
print(f"{'='*80}\n")
|
||||
|
||||
# Group by person
|
||||
by_person = {}
|
||||
for face in faces:
|
||||
person_id = face['person_id']
|
||||
if person_id not in by_person:
|
||||
by_person[person_id] = []
|
||||
by_person[person_id].append(face)
|
||||
|
||||
# Print summary
|
||||
print("SUMMARY BY PERSON:")
|
||||
print("-" * 80)
|
||||
for person_id, person_faces in by_person.items():
|
||||
person_name = person_faces[0]['person_name']
|
||||
pose_modes = [f['pose_mode'] for f in person_faces]
|
||||
frontal_count = sum(1 for p in pose_modes if p == 'frontal')
|
||||
profile_count = sum(1 for p in pose_modes if 'profile' in p)
|
||||
other_count = len(pose_modes) - frontal_count - profile_count
|
||||
|
||||
print(f"\nPerson {person_id}: {person_name}")
|
||||
print(f" Total faces: {len(person_faces)}")
|
||||
print(f" Frontal: {frontal_count}")
|
||||
print(f" Profile: {profile_count}")
|
||||
print(f" Other: {other_count}")
|
||||
print(f" Pose modes: {set(pose_modes)}")
|
||||
|
||||
# Print detailed information
|
||||
print(f"\n{'='*80}")
|
||||
print("DETAILED FACE INFORMATION:")
|
||||
print(f"{'='*80}\n")
|
||||
|
||||
for face in faces:
|
||||
print(f"Face ID: {face['id']}")
|
||||
print(f" Person: {face['person_name']} (ID: {face['person_id']})")
|
||||
print(f" Photo: {face['filename']}")
|
||||
print(f" Pose Mode: {face['pose_mode']}")
|
||||
print(f" Yaw: {face['yaw_angle']:.2f}°" if face['yaw_angle'] is not None else " Yaw: None")
|
||||
print(f" Pitch: {face['pitch_angle']:.2f}°" if face['pitch_angle'] is not None else " Pitch: None")
|
||||
print(f" Roll: {face['roll_angle']:.2f}°" if face['roll_angle'] is not None else " Roll: None")
|
||||
print(f" Confidence: {face['face_confidence']:.3f}")
|
||||
print(f" Quality: {face['quality_score']:.3f}")
|
||||
print(f" Location: {face['location']}")
|
||||
print()
|
||||
|
||||
conn.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
db_path = sys.argv[1] if len(sys.argv) > 1 else DEFAULT_DB_PATH
|
||||
check_identified_faces(db_path)
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Check all identified faces for pose information (web database)"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Add project root to path
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from src.web.db.models import Face, Person, Photo
|
||||
from src.web.db.session import get_database_url
|
||||
|
||||
def check_identified_faces():
|
||||
"""Check all identified faces for pose information"""
|
||||
db_url = get_database_url()
|
||||
print(f"Connecting to database: {db_url}")
|
||||
|
||||
engine = create_engine(db_url)
|
||||
Session = sessionmaker(bind=engine)
|
||||
session = Session()
|
||||
|
||||
try:
|
||||
# Get all identified faces with pose information
|
||||
faces = (
|
||||
session.query(Face, Person, Photo)
|
||||
.join(Person, Face.person_id == Person.id)
|
||||
.join(Photo, Face.photo_id == Photo.id)
|
||||
.filter(Face.person_id.isnot(None))
|
||||
.order_by(Person.id, Face.id)
|
||||
.all()
|
||||
)
|
||||
|
||||
if not faces:
|
||||
print("No identified faces found.")
|
||||
return
|
||||
|
||||
print(f"\n{'='*80}")
|
||||
print(f"Found {len(faces)} identified faces")
|
||||
print(f"{'='*80}\n")
|
||||
|
||||
# Group by person
|
||||
by_person = {}
|
||||
for face, person, photo in faces:
|
||||
person_id = person.id
|
||||
if person_id not in by_person:
|
||||
by_person[person_id] = []
|
||||
by_person[person_id].append((face, person, photo))
|
||||
|
||||
# Print summary
|
||||
print("SUMMARY BY PERSON:")
|
||||
print("-" * 80)
|
||||
for person_id, person_faces in by_person.items():
|
||||
person = person_faces[0][1]
|
||||
person_name = f"{person.first_name} {person.last_name}"
|
||||
pose_modes = [f[0].pose_mode for f in person_faces]
|
||||
frontal_count = sum(1 for p in pose_modes if p == 'frontal')
|
||||
profile_count = sum(1 for p in pose_modes if 'profile' in p)
|
||||
other_count = len(pose_modes) - frontal_count - profile_count
|
||||
|
||||
print(f"\nPerson {person_id}: {person_name}")
|
||||
print(f" Total faces: {len(person_faces)}")
|
||||
print(f" Frontal: {frontal_count}")
|
||||
print(f" Profile: {profile_count}")
|
||||
print(f" Other: {other_count}")
|
||||
print(f" Pose modes: {set(pose_modes)}")
|
||||
|
||||
# Print detailed information
|
||||
print(f"\n{'='*80}")
|
||||
print("DETAILED FACE INFORMATION:")
|
||||
print(f"{'='*80}\n")
|
||||
|
||||
for face, person, photo in faces:
|
||||
person_name = f"{person.first_name} {person.last_name}"
|
||||
print(f"Face ID: {face.id}")
|
||||
print(f" Person: {person_name} (ID: {face.person_id})")
|
||||
print(f" Photo: {photo.filename}")
|
||||
print(f" Pose Mode: {face.pose_mode}")
|
||||
print(f" Yaw: {face.yaw_angle:.2f}°" if face.yaw_angle is not None else " Yaw: None")
|
||||
print(f" Pitch: {face.pitch_angle:.2f}°" if face.pitch_angle is not None else " Pitch: None")
|
||||
print(f" Roll: {face.roll_angle:.2f}°" if face.roll_angle is not None else " Roll: None")
|
||||
print(f" Confidence: {face.face_confidence:.3f}")
|
||||
print(f" Quality: {face.quality_score:.3f}")
|
||||
print(f" Location: {face.location}")
|
||||
print()
|
||||
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
check_identified_faces()
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Check yaw angles in database to see why profile faces aren't being detected
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
import os
|
||||
|
||||
db_path = "data/punimtag.db"
|
||||
|
||||
if not os.path.exists(db_path):
|
||||
print(f"❌ Database not found: {db_path}")
|
||||
exit(1)
|
||||
|
||||
conn = sqlite3.connect(db_path)
|
||||
conn.row_factory = sqlite3.Row
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Get all faces with yaw data
|
||||
cursor.execute("""
|
||||
SELECT id, pose_mode, yaw_angle, pitch_angle, roll_angle
|
||||
FROM faces
|
||||
WHERE yaw_angle IS NOT NULL
|
||||
ORDER BY ABS(yaw_angle) DESC
|
||||
""")
|
||||
|
||||
faces = cursor.fetchall()
|
||||
|
||||
print(f"Found {len(faces)} faces with yaw data\n")
|
||||
print("=" * 80)
|
||||
print("YAW ANGLE ANALYSIS")
|
||||
print("=" * 80)
|
||||
print(f"\n{'Face ID':<10} {'Pose Mode':<25} {'Yaw':<10} {'Should be Profile?'}")
|
||||
print("-" * 80)
|
||||
|
||||
PROFILE_THRESHOLD = 30.0 # From pose_detection.py
|
||||
|
||||
profile_count = 0
|
||||
for face in faces:
|
||||
yaw = face['yaw_angle']
|
||||
pose_mode = face['pose_mode']
|
||||
is_profile = abs(yaw) >= PROFILE_THRESHOLD
|
||||
should_be_profile = "YES" if is_profile else "NO"
|
||||
|
||||
if is_profile:
|
||||
profile_count += 1
|
||||
|
||||
print(f"{face['id']:<10} {pose_mode:<25} {yaw:>8.2f}° {should_be_profile}")
|
||||
|
||||
print("\n" + "=" * 80)
|
||||
print(f"Total faces with yaw data: {len(faces)}")
|
||||
print(f"Faces with |yaw| >= {PROFILE_THRESHOLD}° (should be profile): {profile_count}")
|
||||
print(f"Faces currently classified as profile: {cursor.execute('SELECT COUNT(*) FROM faces WHERE pose_mode LIKE \"profile%\"').fetchone()[0]}")
|
||||
print("=" * 80)
|
||||
|
||||
# Check yaw distribution
|
||||
print("\n" + "=" * 80)
|
||||
print("YAW ANGLE DISTRIBUTION")
|
||||
print("=" * 80)
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
CASE
|
||||
WHEN ABS(yaw_angle) < 30 THEN 'frontal (< 30°)'
|
||||
WHEN ABS(yaw_angle) >= 30 AND ABS(yaw_angle) < 60 THEN 'profile (30-60°)'
|
||||
WHEN ABS(yaw_angle) >= 60 THEN 'extreme profile (>= 60°)'
|
||||
ELSE 'unknown'
|
||||
END as category,
|
||||
COUNT(*) as count
|
||||
FROM faces
|
||||
WHERE yaw_angle IS NOT NULL
|
||||
GROUP BY category
|
||||
ORDER BY count DESC
|
||||
""")
|
||||
|
||||
distribution = cursor.fetchall()
|
||||
for row in distribution:
|
||||
print(f" {row['category']}: {row['count']} faces")
|
||||
|
||||
conn.close()
|
||||
|
||||
Regular → Executable
@@ -0,0 +1,115 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test if RetinaFace provides both eyes for profile faces or if one eye is missing
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
||||
|
||||
try:
|
||||
from src.utils.pose_detection import PoseDetector, RETINAFACE_AVAILABLE
|
||||
from pathlib import Path
|
||||
|
||||
if not RETINAFACE_AVAILABLE:
|
||||
print("❌ RetinaFace not available")
|
||||
exit(1)
|
||||
|
||||
detector = PoseDetector()
|
||||
|
||||
# Find test images
|
||||
test_image_paths = ["demo_photos", "data/uploads"]
|
||||
test_image = None
|
||||
|
||||
for path in test_image_paths:
|
||||
if os.path.exists(path):
|
||||
for ext in ['.jpg', '.jpeg', '.png']:
|
||||
for img_file in Path(path).glob(f'*{ext}'):
|
||||
test_image = str(img_file)
|
||||
break
|
||||
if test_image:
|
||||
break
|
||||
|
||||
if not test_image:
|
||||
print("❌ No test image found")
|
||||
exit(1)
|
||||
|
||||
print(f"Testing with: {test_image}\n")
|
||||
print("=" * 80)
|
||||
print("EYE VISIBILITY ANALYSIS")
|
||||
print("=" * 80)
|
||||
|
||||
faces = detector.detect_faces_with_landmarks(test_image)
|
||||
|
||||
if not faces:
|
||||
print("❌ No faces detected")
|
||||
exit(1)
|
||||
|
||||
print(f"Found {len(faces)} face(s)\n")
|
||||
|
||||
for face_key, face_data in faces.items():
|
||||
landmarks = face_data.get('landmarks', {})
|
||||
print(f"{face_key}:")
|
||||
print(f" Landmarks available: {list(landmarks.keys())}")
|
||||
|
||||
left_eye = landmarks.get('left_eye')
|
||||
right_eye = landmarks.get('right_eye')
|
||||
nose = landmarks.get('nose')
|
||||
|
||||
print(f" Left eye: {left_eye}")
|
||||
print(f" Right eye: {right_eye}")
|
||||
print(f" Nose: {nose}")
|
||||
|
||||
# Check if both eyes are present
|
||||
both_eyes_present = left_eye is not None and right_eye is not None
|
||||
only_left_eye = left_eye is not None and right_eye is None
|
||||
only_right_eye = left_eye is None and right_eye is not None
|
||||
no_eyes = left_eye is None and right_eye is None
|
||||
|
||||
print(f"\n Eye visibility:")
|
||||
print(f" Both eyes present: {both_eyes_present}")
|
||||
print(f" Only left eye: {only_left_eye}")
|
||||
print(f" Only right eye: {only_right_eye}")
|
||||
print(f" No eyes: {no_eyes}")
|
||||
|
||||
# Calculate yaw if possible
|
||||
yaw = detector.calculate_yaw_from_landmarks(landmarks)
|
||||
print(f" Yaw angle: {yaw:.2f}°" if yaw is not None else " Yaw angle: None (requires both eyes)")
|
||||
|
||||
# Calculate face width if both eyes present
|
||||
if both_eyes_present:
|
||||
face_width = abs(right_eye[0] - left_eye[0])
|
||||
print(f" Face width (eye distance): {face_width:.2f} pixels")
|
||||
|
||||
# If face width is very small, it might be a profile view
|
||||
if face_width < 20:
|
||||
print(f" ⚠️ Very small face width - likely extreme profile view")
|
||||
|
||||
# Classify pose
|
||||
pitch = detector.calculate_pitch_from_landmarks(landmarks)
|
||||
roll = detector.calculate_roll_from_landmarks(landmarks)
|
||||
pose_mode = detector.classify_pose_mode(yaw, pitch, roll)
|
||||
|
||||
print(f" Pose mode: {pose_mode}")
|
||||
print()
|
||||
|
||||
print("\n" + "=" * 80)
|
||||
print("CONCLUSION")
|
||||
print("=" * 80)
|
||||
print("""
|
||||
If RetinaFace provides both eyes even for profile faces:
|
||||
- We can use eye distance (face width) as an indicator
|
||||
- Small face width (< 20-30 pixels) suggests extreme profile
|
||||
- But we can't directly use 'missing eye' as a signal
|
||||
|
||||
If RetinaFace sometimes only provides one eye for profile faces:
|
||||
- We can check if left_eye or right_eye is None
|
||||
- If only one eye is present, it's likely a profile view
|
||||
- This would be a strong indicator for profile detection
|
||||
""")
|
||||
|
||||
except ImportError as e:
|
||||
print(f"❌ Import error: {e}")
|
||||
print("Make sure you're in the project directory and dependencies are installed")
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test pitch and roll angle calculations to investigate issues
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Add src to path
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
||||
|
||||
try:
|
||||
from src.utils.pose_detection import PoseDetector, RETINAFACE_AVAILABLE
|
||||
import sqlite3
|
||||
from pathlib import Path
|
||||
|
||||
def test_retinaface_landmarks():
|
||||
"""Test what landmarks RetinaFace actually provides"""
|
||||
if not RETINAFACE_AVAILABLE:
|
||||
print("❌ RetinaFace not available")
|
||||
return
|
||||
|
||||
print("=" * 60)
|
||||
print("TESTING RETINAFACE LANDMARKS")
|
||||
print("=" * 60)
|
||||
|
||||
# Try to find a test image
|
||||
test_image_paths = [
|
||||
"demo_photos",
|
||||
"data/uploads",
|
||||
"data"
|
||||
]
|
||||
|
||||
detector = PoseDetector()
|
||||
test_image = None
|
||||
|
||||
for path in test_image_paths:
|
||||
if os.path.exists(path):
|
||||
for ext in ['.jpg', '.jpeg', '.png']:
|
||||
for img_file in Path(path).glob(f'*{ext}'):
|
||||
test_image = str(img_file)
|
||||
break
|
||||
if test_image:
|
||||
break
|
||||
|
||||
if not test_image:
|
||||
print("❌ No test image found")
|
||||
return
|
||||
|
||||
print(f"Using test image: {test_image}")
|
||||
|
||||
# Detect faces
|
||||
faces = detector.detect_faces_with_landmarks(test_image)
|
||||
|
||||
if not faces:
|
||||
print("❌ No faces detected")
|
||||
return
|
||||
|
||||
print(f"\n✅ Found {len(faces)} face(s)")
|
||||
|
||||
for face_key, face_data in faces.items():
|
||||
print(f"\n{face_key}:")
|
||||
landmarks = face_data.get('landmarks', {})
|
||||
print(f" Landmarks keys: {list(landmarks.keys())}")
|
||||
|
||||
for landmark_name, position in landmarks.items():
|
||||
print(f" {landmark_name}: {position}")
|
||||
|
||||
# Test calculations
|
||||
yaw = detector.calculate_yaw_from_landmarks(landmarks)
|
||||
pitch = detector.calculate_pitch_from_landmarks(landmarks)
|
||||
roll = detector.calculate_roll_from_landmarks(landmarks)
|
||||
|
||||
print(f"\n Calculated angles:")
|
||||
print(f" Yaw: {yaw:.2f}°" if yaw is not None else " Yaw: None")
|
||||
print(f" Pitch: {pitch:.2f}°" if pitch is not None else " Pitch: None")
|
||||
print(f" Roll: {roll:.2f}°" if roll is not None else " Roll: None")
|
||||
|
||||
# Check which landmarks are missing for pitch
|
||||
required_for_pitch = ['left_eye', 'right_eye', 'left_mouth', 'right_mouth', 'nose']
|
||||
missing = [lm for lm in required_for_pitch if lm not in landmarks]
|
||||
if missing:
|
||||
print(f" ⚠️ Missing landmarks for pitch: {missing}")
|
||||
|
||||
# Check roll calculation
|
||||
if roll is not None:
|
||||
left_eye = landmarks.get('left_eye')
|
||||
right_eye = landmarks.get('right_eye')
|
||||
if left_eye and right_eye:
|
||||
dx = right_eye[0] - left_eye[0]
|
||||
dy = right_eye[1] - left_eye[1]
|
||||
print(f" Roll calculation details:")
|
||||
print(f" dx (right_eye[0] - left_eye[0]): {dx:.2f}")
|
||||
print(f" dy (right_eye[1] - left_eye[1]): {dy:.2f}")
|
||||
print(f" atan2(dy, dx) = {roll:.2f}°")
|
||||
|
||||
# Normalize to [-90, 90] range
|
||||
normalized_roll = roll
|
||||
if normalized_roll > 90:
|
||||
normalized_roll = normalized_roll - 180
|
||||
elif normalized_roll < -90:
|
||||
normalized_roll = normalized_roll + 180
|
||||
print(f" Normalized to [-90, 90]: {normalized_roll:.2f}°")
|
||||
|
||||
pose_mode = detector.classify_pose_mode(yaw, pitch, roll)
|
||||
print(f" Pose mode: {pose_mode}")
|
||||
|
||||
def analyze_database_angles():
|
||||
"""Analyze angles in database to find patterns"""
|
||||
db_path = "data/punimtag.db"
|
||||
|
||||
if not os.path.exists(db_path):
|
||||
print(f"❌ Database not found: {db_path}")
|
||||
return
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("ANALYZING DATABASE ANGLES")
|
||||
print("=" * 60)
|
||||
|
||||
conn = sqlite3.connect(db_path)
|
||||
conn.row_factory = sqlite3.Row
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Get faces with angle data
|
||||
cursor.execute("""
|
||||
SELECT id, pose_mode, yaw_angle, pitch_angle, roll_angle
|
||||
FROM faces
|
||||
WHERE yaw_angle IS NOT NULL OR pitch_angle IS NOT NULL OR roll_angle IS NOT NULL
|
||||
LIMIT 20
|
||||
""")
|
||||
|
||||
faces = cursor.fetchall()
|
||||
print(f"\nFound {len(faces)} faces with angle data\n")
|
||||
|
||||
for face in faces:
|
||||
print(f"Face ID {face['id']}: {face['pose_mode']}")
|
||||
print(f" Yaw: {face['yaw_angle']:.2f}°" if face['yaw_angle'] else " Yaw: None")
|
||||
print(f" Pitch: {face['pitch_angle']:.2f}°" if face['pitch_angle'] else " Pitch: None")
|
||||
print(f" Roll: {face['roll_angle']:.2f}°" if face['roll_angle'] else " Roll: None")
|
||||
|
||||
# Check roll normalization
|
||||
if face['roll_angle'] is not None:
|
||||
roll = face['roll_angle']
|
||||
normalized = roll
|
||||
if normalized > 90:
|
||||
normalized = normalized - 180
|
||||
elif normalized < -90:
|
||||
normalized = normalized + 180
|
||||
print(f" Roll normalized: {normalized:.2f}°")
|
||||
print()
|
||||
|
||||
conn.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_retinaface_landmarks()
|
||||
analyze_database_angles()
|
||||
|
||||
except ImportError as e:
|
||||
print(f"❌ Import error: {e}")
|
||||
print("Make sure you're in the project directory and dependencies are installed")
|
||||
|
||||
Reference in New Issue
Block a user