feat: Add PostgreSQL support and configuration setup for PunimTag

This commit introduces PostgreSQL as the default database for the PunimTag application, along with a new `.env.example` file for configuration. A setup script for PostgreSQL has been added to automate the installation and database creation process. The README has been updated to reflect these changes, including instructions for setting up PostgreSQL and using the `.env` file for configuration. Additionally, the database session management has been enhanced to support PostgreSQL connection pooling. Documentation has been updated accordingly.
This commit is contained in:
tanyar09
2025-11-14 12:44:12 -05:00
parent c661aeeda6
commit 8caa9e192b
6 changed files with 400 additions and 16 deletions
+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 src.web.db.models import Face, Person, Photo
from src.web.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()
+59
View File
@@ -0,0 +1,59 @@
#!/bin/bash
# Setup script for PostgreSQL database for PunimTag
set -e
echo "🔧 Setting up PostgreSQL for PunimTag..."
# Check if PostgreSQL is installed
if ! command -v psql &> /dev/null; then
echo "📦 Installing PostgreSQL..."
sudo apt update
sudo apt install -y postgresql postgresql-contrib
echo "✅ PostgreSQL installed"
else
echo "✅ PostgreSQL is already installed"
fi
# Start PostgreSQL service
echo "🚀 Starting PostgreSQL service..."
sudo systemctl start postgresql
sudo systemctl enable postgresql
# Create database and user
echo "📝 Creating database and user..."
sudo -u postgres psql << EOF
-- Create user if it doesn't exist
DO \$\$
BEGIN
IF NOT EXISTS (SELECT FROM pg_user WHERE usename = 'punimtag') THEN
CREATE USER punimtag WITH PASSWORD 'punimtag_password';
END IF;
END
\$\$;
-- Create database if it doesn't exist
SELECT 'CREATE DATABASE punimtag OWNER punimtag'
WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'punimtag')\gexec
-- Grant privileges
GRANT ALL PRIVILEGES ON DATABASE punimtag TO punimtag;
\q
EOF
echo "✅ Database and user created"
echo ""
echo "📋 Database connection details:"
echo " Host: localhost"
echo " Port: 5432"
echo " Database: punimtag"
echo " User: punimtag"
echo " Password: punimtag_password"
echo ""
echo "✅ PostgreSQL setup complete!"
echo ""
echo "Next steps:"
echo "1. Install python-dotenv: pip install python-dotenv"
echo "2. The .env file is already configured with the connection string"
echo "3. Run your application - it will connect to PostgreSQL automatically"