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:
@@ -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,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)
|
||||
|
||||
Executable
+188
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
Executable
+253
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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