feat: Add new scripts and update project structure for database management and user authentication

This commit introduces several new scripts for managing database operations, including user creation, permission grants, and data migrations. It also adds new documentation files to guide users through the setup and configuration processes. Additionally, the project structure is updated to enhance organization and maintainability, ensuring a smoother development experience for contributors. These changes support the ongoing transition to a web-based architecture and improve overall project functionality.
This commit is contained in:
Tanya
2026-01-06 13:53:24 -05:00
parent 713584dc04
commit de2144be2a
175 changed files with 35854 additions and 0 deletions
+83
View File
@@ -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()
+156
View File
@@ -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()
+192
View File
@@ -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)}")
+135
View File
@@ -0,0 +1,135 @@
#!/usr/bin/env python3
"""
Check what tables exist in the punimtag main database and their record counts.
"""
from __future__ import annotations
import sys
from pathlib import Path
# Add project root to path
project_root = Path(__file__).parent.parent
sys.path.insert(0, str(project_root))
from sqlalchemy import create_engine, inspect, text
from backend.db.session import get_database_url
def check_database_tables() -> None:
"""Check all tables in the database and their record counts."""
database_url = get_database_url()
print("=" * 80)
print("PUNIMTAG MAIN DATABASE - TABLE INFORMATION")
print("=" * 80)
print(f"\nDatabase URL: {database_url.replace('://', '://****') if '://' in database_url else database_url}\n")
# Create engine
connect_args = {}
if database_url.startswith("sqlite"):
connect_args = {"check_same_thread": False}
engine = create_engine(database_url, connect_args=connect_args)
try:
# Get inspector to list tables
inspector = inspect(engine)
all_tables = inspector.get_table_names()
if not all_tables:
print("❌ No tables found in database.")
return
print(f"Found {len(all_tables)} tables:\n")
# Expected tables from models
expected_tables = {
"photos",
"people",
"faces",
"person_encodings",
"tags",
"phototaglinkage",
"photo_favorites",
"users",
"photo_person_linkage",
"role_permissions",
}
# Connect and query each table
with engine.connect() as conn:
print(f"{'Table Name':<30} {'Record Count':<15} {'Status'}")
print("-" * 80)
for table_name in sorted(all_tables):
# Skip SQLite system tables
if table_name.startswith("sqlite_"):
continue
try:
# Get record count
if database_url.startswith("sqlite"):
result = conn.execute(text(f"SELECT COUNT(*) FROM {table_name}"))
else:
result = conn.execute(text(f'SELECT COUNT(*) FROM "{table_name}"'))
count = result.scalar()
# Check if it's an expected table
status = "✅ Expected" if table_name in expected_tables else "⚠️ Unexpected"
print(f"{table_name:<30} {count:<15} {status}")
except Exception as e:
print(f"{table_name:<30} {'ERROR':<15} ❌ {str(e)[:50]}")
print("-" * 80)
# Summary
print("\n📊 Summary:")
with engine.connect() as conn:
total_records = 0
tables_with_data = 0
for table_name in sorted(all_tables):
if table_name.startswith("sqlite_"):
continue
try:
if database_url.startswith("sqlite"):
result = conn.execute(text(f"SELECT COUNT(*) FROM {table_name}"))
else:
result = conn.execute(text(f'SELECT COUNT(*) FROM "{table_name}"'))
count = result.scalar()
total_records += count
if count > 0:
tables_with_data += 1
except:
pass
print(f" Total tables: {len([t for t in all_tables if not t.startswith('sqlite_')])}")
print(f" Tables with records: {tables_with_data}")
print(f" Total records across all tables: {total_records:,}")
# Check for missing expected tables
missing_tables = expected_tables - set(all_tables)
if missing_tables:
print(f"\n⚠️ Missing expected tables: {', '.join(sorted(missing_tables))}")
# Check for unexpected tables
unexpected_tables = set(all_tables) - expected_tables - {"alembic_version"}
unexpected_tables = {t for t in unexpected_tables if not t.startswith("sqlite_")}
if unexpected_tables:
print(f"\nℹ️ Additional tables found: {', '.join(sorted(unexpected_tables))}")
except Exception as e:
print(f"❌ Error connecting to database: {e}")
import traceback
traceback.print_exc()
return
print("\n" + "=" * 80)
if __name__ == "__main__":
check_database_tables()
@@ -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 backend.db.models import Face, Person, Photo
from backend.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)
+188
View File
@@ -0,0 +1,188 @@
#!/usr/bin/env python3
"""Check two identified faces and analyze why their pose modes are wrong"""
import sys
import os
import json
# 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 backend.db.models import Face, Person, Photo
from backend.db.session import get_database_url
from src.utils.pose_detection import PoseDetector
def check_two_faces(face_id1: int = None, face_id2: int = None):
"""Check two identified faces and analyze their pose modes"""
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
query = (
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(Face.id)
)
if face_id1:
query = query.filter(Face.id == face_id1)
elif face_id2:
query = query.filter(Face.id == face_id2)
faces = query.limit(2).all()
if len(faces) < 2:
print(f"Found {len(faces)} identified face(s). Need 2 faces to compare.")
if len(faces) == 0:
print("No identified faces found.")
return
print("\nShowing available identified faces:")
all_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(Face.id)
.limit(10)
.all()
)
for face, person, photo in all_faces:
print(f" Face ID: {face.id}, Person: {person.first_name} {person.last_name}, Photo: {photo.filename}, Pose: {face.pose_mode}")
return
print(f"\n{'='*80}")
print("ANALYZING TWO IDENTIFIED FACES")
print(f"{'='*80}\n")
for idx, (face, person, photo) in enumerate(faces, 1):
person_name = f"{person.first_name} {person.last_name}"
print(f"{'='*80}")
print(f"FACE {idx}: ID {face.id}")
print(f"{'='*80}")
print(f"Person: {person_name} (ID: {face.person_id})")
print(f"Photo: {photo.filename}")
print(f"Current 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"Face Width: {face.face_width if hasattr(face, 'face_width') else 'N/A'}")
print(f"Confidence: {face.face_confidence:.3f}")
print(f"Quality: {face.quality_score:.3f}")
print(f"Location: {face.location}")
# Parse landmarks if available
landmarks = None
if face.landmarks:
try:
landmarks = json.loads(face.landmarks)
print(f"\nLandmarks:")
for key, value in landmarks.items():
print(f" {key}: {value}")
except json.JSONDecodeError:
print(f"\nLandmarks: (invalid JSON)")
# Recalculate pose mode using current logic
print(f"\n{'─'*80}")
print("RECALCULATING POSE MODE:")
print(f"{'─'*80}")
# Calculate face width from landmarks if available
face_width = None
if landmarks:
face_width = PoseDetector.calculate_face_width_from_landmarks(landmarks)
print(f"Calculated face_width from landmarks: {face_width}")
# Recalculate pose mode
recalculated_pose = PoseDetector.classify_pose_mode(
face.yaw_angle,
face.pitch_angle,
face.roll_angle,
face_width,
landmarks
)
print(f"Recalculated Pose Mode: {recalculated_pose}")
if recalculated_pose != face.pose_mode:
print(f"⚠️ MISMATCH! Current: '{face.pose_mode}' vs Recalculated: '{recalculated_pose}'")
# Analyze why
print(f"\nAnalysis:")
if face.yaw_angle is None:
print(f" - Yaw is None")
if landmarks:
left_eye = landmarks.get('left_eye')
right_eye = landmarks.get('right_eye')
nose = landmarks.get('nose')
missing = []
if not left_eye:
missing.append('left_eye')
if not right_eye:
missing.append('right_eye')
if not nose:
missing.append('nose')
if missing:
print(f" - Missing landmarks: {', '.join(missing)}")
print(f" - Should be classified as profile (missing landmarks)")
else:
print(f" - All landmarks present")
if face_width:
print(f" - Face width: {face_width}px")
if face_width < 25.0:
print(f" - Face width < 25px, should be profile")
else:
print(f" - Face width >= 25px, should be frontal")
else:
print(f" - No landmarks available")
else:
abs_yaw = abs(face.yaw_angle)
print(f" - Yaw angle: {face.yaw_angle:.2f}° (abs: {abs_yaw:.2f}°)")
if abs_yaw >= 30.0:
expected = "profile_left" if face.yaw_angle < 0 else "profile_right"
print(f" - |yaw| >= 30°, should be '{expected}'")
else:
print(f" - |yaw| < 30°, should be 'frontal'")
else:
print(f"✓ Pose mode matches recalculated value")
print()
finally:
session.close()
if __name__ == "__main__":
face_id1 = None
face_id2 = None
if len(sys.argv) > 1:
try:
face_id1 = int(sys.argv[1])
except ValueError:
print(f"Invalid face ID: {sys.argv[1]}")
sys.exit(1)
if len(sys.argv) > 2:
try:
face_id2 = int(sys.argv[2])
except ValueError:
print(f"Invalid face ID: {sys.argv[2]}")
sys.exit(1)
try:
check_two_faces(face_id1, face_id2)
except Exception as e:
print(f"❌ Error: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
+80
View File
@@ -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()
+253
View File
@@ -0,0 +1,253 @@
#!/usr/bin/env python3
"""Debug pose classification for identified faces
This script helps identify why poses might be incorrectly classified.
It shows detailed pose information and can recalculate poses from photos.
"""
import sys
import os
import json
from typing import Optional, List, Tuple
# 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 backend.db.models import Face, Person, Photo
from backend.db.session import get_database_url
from src.utils.pose_detection import PoseDetector
def analyze_pose_classification(
face_id: Optional[int] = None,
person_id: Optional[int] = None,
recalculate: bool = False,
) -> None:
"""Analyze pose classification for identified faces.
Args:
face_id: Specific face ID to check (None = all identified faces)
person_id: Specific person ID to check (None = all persons)
recalculate: If True, recalculate pose from photo to verify classification
"""
db_url = get_database_url()
print(f"Connecting to database: {db_url}")
engine = create_engine(db_url)
Session = sessionmaker(bind=engine)
session = Session()
try:
# Build query
query = (
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))
)
if face_id:
query = query.filter(Face.id == face_id)
if person_id:
query = query.filter(Person.id == person_id)
faces = query.order_by(Person.id, Face.id).all()
if not faces:
print("No identified faces found matching criteria.")
return
print(f"\n{'='*80}")
print(f"Found {len(faces)} identified face(s)")
print(f"{'='*80}\n")
pose_detector = None
if recalculate:
try:
pose_detector = PoseDetector()
print("Pose detector initialized for recalculation\n")
except Exception as e:
print(f"Warning: Could not initialize pose detector: {e}")
print("Skipping recalculation\n")
recalculate = False
for face, person, photo in faces:
person_name = f"{person.first_name} {person.last_name}"
print(f"{'='*80}")
print(f"Face ID: {face.id}")
print(f"Person: {person_name} (ID: {person.id})")
print(f"Photo: {photo.filename}")
print(f"Photo Path: {photo.path}")
print(f"{'-'*80}")
# Current stored pose information
print("STORED POSE INFORMATION:")
print(f" Pose Mode: {face.pose_mode}")
print(f" Yaw Angle: {face.yaw_angle:.2f}°" if face.yaw_angle is not None else " Yaw Angle: None")
print(f" Pitch Angle: {face.pitch_angle:.2f}°" if face.pitch_angle is not None else " Pitch Angle: None")
print(f" Roll Angle: {face.roll_angle:.2f}°" if face.roll_angle is not None else " Roll Angle: None")
print(f" Face Confidence: {face.face_confidence:.3f}")
print(f" Quality Score: {face.quality_score:.3f}")
# Parse location
try:
location = json.loads(face.location) if isinstance(face.location, str) else face.location
print(f" Location: {location}")
except:
print(f" Location: {face.location}")
# Analyze classification
print(f"\nPOSE CLASSIFICATION ANALYSIS:")
yaw = face.yaw_angle
pitch = face.pitch_angle
roll = face.roll_angle
if yaw is not None:
abs_yaw = abs(yaw)
print(f" Yaw: {yaw:.2f}° (absolute: {abs_yaw:.2f}°)")
if abs_yaw < 30.0:
expected_mode = "frontal"
print(f" → Expected: {expected_mode} (yaw < 30°)")
elif yaw <= -30.0:
expected_mode = "profile_left"
print(f" → Expected: {expected_mode} (yaw <= -30°, face turned left)")
elif yaw >= 30.0:
expected_mode = "profile_right"
print(f" → Expected: {expected_mode} (yaw >= 30°, face turned right)")
else:
expected_mode = "unknown"
print(f" → Expected: {expected_mode} (edge case)")
if face.pose_mode != expected_mode:
print(f" ⚠️ MISMATCH: Stored pose_mode='{face.pose_mode}' but expected '{expected_mode}'")
else:
print(f" ✓ Classification matches expected mode")
else:
print(f" Yaw: None (cannot determine pose from yaw)")
print(f" ⚠️ Warning: Yaw angle is missing, pose classification may be unreliable")
# Recalculate if requested
if recalculate and pose_detector and photo.path and os.path.exists(photo.path):
print(f"\nRECALCULATING POSE FROM PHOTO:")
try:
pose_faces = pose_detector.detect_pose_faces(photo.path)
if not pose_faces:
print(" No faces detected in photo")
else:
# Try to match face by location
face_location = location if isinstance(location, dict) else json.loads(face.location) if isinstance(face.location, str) else {}
face_x = face_location.get('x', 0)
face_y = face_location.get('y', 0)
face_w = face_location.get('w', 0)
face_h = face_location.get('h', 0)
face_center_x = face_x + face_w / 2
face_center_y = face_y + face_h / 2
best_match = None
best_distance = float('inf')
for pose_face in pose_faces:
pose_area = pose_face.get('facial_area', {})
if isinstance(pose_area, dict):
pose_x = pose_area.get('x', 0)
pose_y = pose_area.get('y', 0)
pose_w = pose_area.get('w', 0)
pose_h = pose_area.get('h', 0)
pose_center_x = pose_x + pose_w / 2
pose_center_y = pose_y + pose_h / 2
# Calculate distance between centers
distance = ((face_center_x - pose_center_x) ** 2 +
(face_center_y - pose_center_y) ** 2) ** 0.5
if distance < best_distance:
best_distance = distance
best_match = pose_face
if best_match:
recalc_yaw = best_match.get('yaw_angle')
recalc_pitch = best_match.get('pitch_angle')
recalc_roll = best_match.get('roll_angle')
recalc_face_width = best_match.get('face_width')
recalc_pose_mode = best_match.get('pose_mode')
print(f" Recalculated Yaw: {recalc_yaw:.2f}°" if recalc_yaw is not None else " Recalculated Yaw: None")
print(f" Recalculated Pitch: {recalc_pitch:.2f}°" if recalc_pitch is not None else " Recalculated Pitch: None")
print(f" Recalculated Roll: {recalc_roll:.2f}°" if recalc_roll is not None else " Recalculated Roll: None")
print(f" Face Width: {recalc_face_width:.2f}px" if recalc_face_width is not None else " Face Width: None")
print(f" Recalculated Pose Mode: {recalc_pose_mode}")
# Compare
if recalc_pose_mode != face.pose_mode:
print(f" ⚠️ MISMATCH: Stored='{face.pose_mode}' vs Recalculated='{recalc_pose_mode}'")
if recalc_yaw is not None and face.yaw_angle is not None:
# Convert Decimal to float for comparison
stored_yaw = float(face.yaw_angle)
yaw_diff = abs(recalc_yaw - stored_yaw)
if yaw_diff > 1.0: # More than 1 degree difference
print(f" ⚠️ Yaw difference: {yaw_diff:.2f}°")
else:
print(" Could not match face location to detected faces")
except Exception as e:
print(f" Error recalculating: {e}")
import traceback
traceback.print_exc()
print()
print(f"{'='*80}")
print("Analysis complete")
print(f"{'='*80}\n")
finally:
session.close()
def main():
"""Main entry point"""
import argparse
parser = argparse.ArgumentParser(
description="Debug pose classification for identified faces"
)
parser.add_argument(
"--face-id",
type=int,
help="Specific face ID to check"
)
parser.add_argument(
"--person-id",
type=int,
help="Specific person ID to check"
)
parser.add_argument(
"--recalculate",
action="store_true",
help="Recalculate pose from photo to verify classification"
)
args = parser.parse_args()
try:
analyze_pose_classification(
face_id=args.face_id,
person_id=args.person_id,
recalculate=args.recalculate,
)
except Exception as e:
print(f"❌ Error: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
if __name__ == "__main__":
main()
+160
View File
@@ -0,0 +1,160 @@
#!/usr/bin/env python3
"""
Diagnose frontend issues:
1. Check if backend API is running and accessible
2. Check database connection
3. Test search endpoint
"""
import os
import sys
import requests
from pathlib import Path
# Add project root to path
project_root = Path(__file__).parent.parent
sys.path.insert(0, str(project_root))
from backend.db.session import get_database_url, engine
from sqlalchemy import text
def check_backend_api():
"""Check if backend API is running."""
print("=" * 80)
print("BACKEND API CHECK")
print("=" * 80)
try:
# Check if docs endpoint is accessible
response = requests.get("http://127.0.0.1:8000/docs", timeout=5)
if response.status_code == 200:
print("✅ Backend API is running (docs accessible)")
else:
print(f"⚠️ Backend API returned status {response.status_code}")
except requests.exceptions.ConnectionError:
print("❌ Backend API is NOT running or not accessible")
print(" Start it with: cd backend && uvicorn app:app --reload")
return False
except Exception as e:
print(f"❌ Error checking backend API: {e}")
return False
# Check search endpoint (requires auth)
try:
response = requests.get(
"http://127.0.0.1:8000/api/v1/photos",
params={"search_type": "processed", "page": 1, "page_size": 1},
timeout=5
)
if response.status_code == 200:
print("✅ Search endpoint is accessible (no auth required for this query)")
elif response.status_code == 401:
print("⚠️ Search endpoint requires authentication")
print(" User needs to log in through admin frontend")
else:
print(f"⚠️ Search endpoint returned status {response.status_code}")
except Exception as e:
print(f"⚠️ Error checking search endpoint: {e}")
return True
def check_database():
"""Check database connection and photo count."""
print("\n" + "=" * 80)
print("DATABASE CHECK")
print("=" * 80)
db_url = get_database_url()
print(f"Database URL: {db_url.replace('://', '://****') if '://' in db_url else db_url}")
try:
with engine.connect() as conn:
# Check photo count
result = conn.execute(text("SELECT COUNT(*) FROM photos WHERE processed = 1"))
count = result.scalar()
print(f"✅ Database connection successful")
print(f" Processed photos: {count}")
if count == 0:
print("⚠️ No processed photos found in database")
print(" This explains why viewer frontend shows 0 photos")
else:
print(f" Database has {count} processed photos")
except Exception as e:
print(f"❌ Database connection error: {e}")
return False
return True
def check_viewer_frontend_config():
"""Check viewer frontend configuration."""
print("\n" + "=" * 80)
print("VIEWER FRONTEND CONFIGURATION")
print("=" * 80)
viewer_env = project_root / "viewer-frontend" / ".env"
if not viewer_env.exists():
print("❌ viewer-frontend/.env file not found")
return False
with open(viewer_env) as f:
content = f.read()
if "DATABASE_URL" in content:
# Extract DATABASE_URL
for line in content.split("\n"):
if line.startswith("DATABASE_URL="):
db_url = line.split("=", 1)[1].strip().strip('"')
print(f"Viewer frontend DATABASE_URL: {db_url.replace('://', '://****') if '://' in db_url else db_url}")
# Check if it matches actual database
actual_db = get_database_url()
if "postgresql" in db_url and "sqlite" in actual_db:
print("❌ MISMATCH: Viewer frontend configured for PostgreSQL")
print(" but actual database is SQLite")
print("\n SOLUTION OPTIONS:")
print(" 1. Change viewer-frontend/.env DATABASE_URL to SQLite:")
print(f' DATABASE_URL="file:../data/punimtag.db"')
print(" 2. Update Prisma schema to use SQLite provider")
print(" 3. Migrate database to PostgreSQL")
return False
elif "sqlite" in db_url and "sqlite" in actual_db:
print("✅ Viewer frontend configured for SQLite (matches actual database)")
else:
print("⚠️ Database type mismatch or unclear")
else:
print("⚠️ DATABASE_URL not found in viewer-frontend/.env")
return True
def main():
print("\n🔍 DIAGNOSING FRONTEND ISSUES\n")
backend_ok = check_backend_api()
db_ok = check_database()
viewer_config_ok = check_viewer_frontend_config()
print("\n" + "=" * 80)
print("SUMMARY")
print("=" * 80)
if not backend_ok:
print("❌ Backend API is not running - admin frontend search will fail")
else:
print("✅ Backend API is running")
if not db_ok:
print("❌ Database connection issue")
else:
print("✅ Database connection OK")
if not viewer_config_ok:
print("❌ Viewer frontend configuration issue - needs to be fixed")
else:
print("✅ Viewer frontend configuration OK")
print("\n" + "=" * 80)
if __name__ == "__main__":
main()
+115
View File
@@ -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")
+161
View File
@@ -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")