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:
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 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()
|
||||
|
||||
Executable
+59
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user