feat: Enhance API startup script and add file hash management for photos
This commit improves the `run_api_with_worker.sh` script by ensuring the virtual environment is created if it doesn't exist and dependencies are installed. It also adds a check to ensure the database schema is up to date. Additionally, new functionality has been introduced to calculate and store file hashes for uploaded photos, preventing duplicates. The database schema has been updated to include a `file_hash` column in the `photos` table, along with an index for efficient querying. The frontend has been updated to handle warnings for duplicate photos during the review process. Documentation has been updated to reflect these changes.
This commit is contained in:
@@ -1,81 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script to demonstrate the difference between old and new confidence calculations
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from src.core.config import DEFAULT_FACE_TOLERANCE, USE_CALIBRATED_CONFIDENCE, CONFIDENCE_CALIBRATION_METHOD
|
||||
from src.core.face_processing import FaceProcessor
|
||||
from src.core.database import DatabaseManager
|
||||
|
||||
def test_confidence_calibration():
|
||||
"""Test and compare old vs new confidence calculations"""
|
||||
|
||||
print("🔍 Confidence Calibration Test")
|
||||
print("=" * 50)
|
||||
|
||||
# Initialize face processor (we don't need database for this test)
|
||||
db_manager = DatabaseManager(":memory:") # In-memory database for testing
|
||||
face_processor = FaceProcessor(db_manager, verbose=1)
|
||||
|
||||
# Test different distance values
|
||||
test_distances = [0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.2, 1.5, 2.0]
|
||||
tolerance = DEFAULT_FACE_TOLERANCE
|
||||
|
||||
print(f"Tolerance threshold: {tolerance}")
|
||||
print(f"Calibration enabled: {USE_CALIBRATED_CONFIDENCE}")
|
||||
print(f"Calibration method: {CONFIDENCE_CALIBRATION_METHOD}")
|
||||
print()
|
||||
|
||||
print("Distance | Old Linear | New Calibrated | Difference | Description")
|
||||
print("-" * 70)
|
||||
|
||||
for distance in test_distances:
|
||||
# Old linear calculation
|
||||
old_confidence = (1 - distance) * 100
|
||||
|
||||
# New calibrated calculation
|
||||
new_confidence, description = face_processor._get_calibrated_confidence(distance, tolerance)
|
||||
|
||||
difference = new_confidence - old_confidence
|
||||
|
||||
print(f"{distance:8.1f} | {old_confidence:10.1f}% | {new_confidence:13.1f}% | {difference:+9.1f}% | {description}")
|
||||
|
||||
print()
|
||||
print("📊 Key Differences:")
|
||||
print("- Old method: Simple linear transformation (1 - distance) * 100")
|
||||
print("- New method: Empirical calibration based on DeepFace ArcFace characteristics")
|
||||
print("- New method provides more realistic match probabilities")
|
||||
print()
|
||||
|
||||
# Test different calibration methods
|
||||
print("🔧 Testing Different Calibration Methods:")
|
||||
print("-" * 50)
|
||||
|
||||
# Temporarily change calibration method to test different approaches
|
||||
original_method = CONFIDENCE_CALIBRATION_METHOD
|
||||
|
||||
test_distance = 0.4 # Example distance
|
||||
print(f"Distance: {test_distance}, Tolerance: {tolerance}")
|
||||
print()
|
||||
|
||||
methods = ["linear", "sigmoid", "empirical"]
|
||||
for method in methods:
|
||||
# Update the global config (this is just for testing)
|
||||
import src.core.config
|
||||
src.core.config.CONFIDENCE_CALIBRATION_METHOD = method
|
||||
|
||||
confidence, desc = face_processor._get_calibrated_confidence(test_distance, tolerance)
|
||||
print(f"{method:10}: {confidence:6.1f}% - {desc}")
|
||||
|
||||
# Restore original method
|
||||
src.core.config.CONFIDENCE_CALIBRATION_METHOD = original_method
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_confidence_calibration()
|
||||
|
||||
|
||||
|
||||
@@ -1,685 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
DeepFace Integration Test Suite for PunimTag
|
||||
Tests the complete integration of DeepFace into the application
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Add parent directory to path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
# Suppress TensorFlow warnings
|
||||
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'
|
||||
import warnings
|
||||
warnings.filterwarnings('ignore')
|
||||
|
||||
from src.core.database import DatabaseManager
|
||||
from src.core.face_processing import FaceProcessor
|
||||
from src.core.config import DEEPFACE_DETECTOR_BACKEND, DEEPFACE_MODEL_NAME
|
||||
|
||||
|
||||
def test_face_detection():
|
||||
"""Test 1: Face detection with DeepFace"""
|
||||
print("\n" + "="*60)
|
||||
print("Test 1: DeepFace Face Detection")
|
||||
print("="*60)
|
||||
|
||||
try:
|
||||
db = DatabaseManager(":memory:", verbose=0) # In-memory database for testing
|
||||
processor = FaceProcessor(db, verbose=1)
|
||||
|
||||
# Test with a sample image
|
||||
test_image = "demo_photos/2019-11-22_0011.jpg"
|
||||
if not os.path.exists(test_image):
|
||||
print(f"❌ Test image not found: {test_image}")
|
||||
print(" Please ensure demo photos are available")
|
||||
return False
|
||||
|
||||
print(f"Testing with image: {test_image}")
|
||||
|
||||
# Add photo to database
|
||||
photo_id = db.add_photo(test_image, Path(test_image).name, None)
|
||||
print(f"✓ Added photo to database (ID: {photo_id})")
|
||||
|
||||
# Process faces
|
||||
count = processor.process_faces(limit=1)
|
||||
print(f"✓ Processed {count} photos")
|
||||
|
||||
# Verify results
|
||||
stats = db.get_statistics()
|
||||
print(f"✓ Found {stats['total_faces']} faces in the photo")
|
||||
|
||||
if stats['total_faces'] == 0:
|
||||
print("❌ FAIL: No faces detected")
|
||||
return False
|
||||
|
||||
# Verify face encodings are 512-dimensional (ArcFace)
|
||||
with db.get_db_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT encoding FROM faces LIMIT 1")
|
||||
encoding_blob = cursor.fetchone()[0]
|
||||
encoding_size = len(encoding_blob)
|
||||
expected_size = 512 * 8 # 512 floats * 8 bytes per float
|
||||
|
||||
print(f"✓ Encoding size: {encoding_size} bytes (expected: {expected_size})")
|
||||
|
||||
if encoding_size != expected_size:
|
||||
print(f"❌ FAIL: Wrong encoding size (expected {expected_size}, got {encoding_size})")
|
||||
return False
|
||||
|
||||
print("\n✅ PASS: Face detection working correctly")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ FAIL: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
|
||||
def test_face_matching():
|
||||
"""Test 2: Face matching with DeepFace"""
|
||||
print("\n" + "="*60)
|
||||
print("Test 2: DeepFace Face Matching")
|
||||
print("="*60)
|
||||
|
||||
try:
|
||||
db = DatabaseManager(":memory:", verbose=0)
|
||||
processor = FaceProcessor(db, verbose=1)
|
||||
|
||||
# Test with multiple images
|
||||
test_images = [
|
||||
"demo_photos/2019-11-22_0011.jpg",
|
||||
"demo_photos/2019-11-22_0012.jpg"
|
||||
]
|
||||
|
||||
# Check if test images exist
|
||||
available_images = [img for img in test_images if os.path.exists(img)]
|
||||
|
||||
if len(available_images) < 2:
|
||||
print(f"⚠️ Only {len(available_images)} test images available")
|
||||
print(" Skipping face matching test (need at least 2 images)")
|
||||
return True # Skip but don't fail
|
||||
|
||||
print(f"Testing with {len(available_images)} images")
|
||||
|
||||
# Add photos to database
|
||||
for img in available_images:
|
||||
photo_id = db.add_photo(img, Path(img).name, None)
|
||||
print(f"✓ Added {Path(img).name} (ID: {photo_id})")
|
||||
|
||||
# Process all faces
|
||||
count = processor.process_faces(limit=10)
|
||||
print(f"✓ Processed {count} photos")
|
||||
|
||||
# Get statistics
|
||||
stats = db.get_statistics()
|
||||
print(f"✓ Found {stats['total_faces']} total faces")
|
||||
|
||||
if stats['total_faces'] < 2:
|
||||
print("⚠️ Not enough faces for matching test")
|
||||
return True # Skip but don't fail
|
||||
|
||||
# Find similar faces
|
||||
faces = db.get_all_face_encodings()
|
||||
if len(faces) >= 2:
|
||||
face_id = faces[0][0]
|
||||
print(f"✓ Testing similarity for face ID {face_id}")
|
||||
|
||||
matches = processor.find_similar_faces(face_id, tolerance=0.4)
|
||||
print(f"✓ Found {len(matches)} similar faces (within tolerance)")
|
||||
|
||||
# Display match details
|
||||
if matches:
|
||||
for i, match in enumerate(matches[:3], 1): # Show top 3 matches
|
||||
confidence_pct = (1 - match['distance']) * 100
|
||||
print(f" Match {i}: Face {match['face_id']}, Confidence: {confidence_pct:.1f}%")
|
||||
|
||||
print("\n✅ PASS: Face matching working correctly")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ FAIL: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
|
||||
def test_deepface_metadata():
|
||||
"""Test 3: DeepFace metadata storage and retrieval"""
|
||||
print("\n" + "="*60)
|
||||
print("Test 3: DeepFace Metadata Storage")
|
||||
print("="*60)
|
||||
|
||||
try:
|
||||
db = DatabaseManager(":memory:", verbose=0)
|
||||
processor = FaceProcessor(db, verbose=1)
|
||||
|
||||
# Test with a sample image
|
||||
test_image = "demo_photos/2019-11-22_0011.jpg"
|
||||
if not os.path.exists(test_image):
|
||||
print(f"⚠️ Test image not found: {test_image}")
|
||||
return True # Skip but don't fail
|
||||
|
||||
# Add photo and process
|
||||
photo_id = db.add_photo(test_image, Path(test_image).name, None)
|
||||
processor.process_faces(limit=1)
|
||||
|
||||
# Query face metadata
|
||||
with db.get_db_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("""
|
||||
SELECT face_confidence, quality_score, detector_backend, model_name
|
||||
FROM faces
|
||||
LIMIT 1
|
||||
""")
|
||||
result = cursor.fetchone()
|
||||
|
||||
if not result:
|
||||
print("❌ FAIL: No face metadata found")
|
||||
return False
|
||||
|
||||
face_conf, quality, detector, model = result
|
||||
|
||||
print(f"✓ Face Confidence: {face_conf}")
|
||||
print(f"✓ Quality Score: {quality}")
|
||||
print(f"✓ Detector Backend: {detector}")
|
||||
print(f"✓ Model Name: {model}")
|
||||
|
||||
# Verify metadata is present
|
||||
if detector is None:
|
||||
print("❌ FAIL: Detector backend not stored")
|
||||
return False
|
||||
|
||||
if model is None:
|
||||
print("❌ FAIL: Model name not stored")
|
||||
return False
|
||||
|
||||
# Verify detector matches configuration
|
||||
if detector != DEEPFACE_DETECTOR_BACKEND:
|
||||
print(f"⚠️ Warning: Detector mismatch (expected {DEEPFACE_DETECTOR_BACKEND}, got {detector})")
|
||||
|
||||
# Verify model matches configuration
|
||||
if model != DEEPFACE_MODEL_NAME:
|
||||
print(f"⚠️ Warning: Model mismatch (expected {DEEPFACE_MODEL_NAME}, got {model})")
|
||||
|
||||
print("\n✅ PASS: DeepFace metadata stored correctly")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ FAIL: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
|
||||
def test_configuration():
|
||||
"""Test 4: FaceProcessor configuration with different backends"""
|
||||
print("\n" + "="*60)
|
||||
print("Test 4: FaceProcessor Configuration")
|
||||
print("="*60)
|
||||
|
||||
try:
|
||||
db = DatabaseManager(":memory:", verbose=0)
|
||||
|
||||
# Test default configuration
|
||||
processor_default = FaceProcessor(db, verbose=0)
|
||||
print(f"✓ Default detector: {processor_default.detector_backend}")
|
||||
print(f"✓ Default model: {processor_default.model_name}")
|
||||
|
||||
if processor_default.detector_backend != DEEPFACE_DETECTOR_BACKEND:
|
||||
print(f"❌ FAIL: Default detector mismatch")
|
||||
return False
|
||||
|
||||
if processor_default.model_name != DEEPFACE_MODEL_NAME:
|
||||
print(f"❌ FAIL: Default model mismatch")
|
||||
return False
|
||||
|
||||
# Test custom configuration
|
||||
custom_configs = [
|
||||
('mtcnn', 'Facenet512'),
|
||||
('opencv', 'VGG-Face'),
|
||||
('ssd', 'ArcFace'),
|
||||
]
|
||||
|
||||
for detector, model in custom_configs:
|
||||
processor = FaceProcessor(db, verbose=0,
|
||||
detector_backend=detector,
|
||||
model_name=model)
|
||||
print(f"✓ Custom config: {detector}/{model}")
|
||||
|
||||
if processor.detector_backend != detector:
|
||||
print(f"❌ FAIL: Custom detector not applied")
|
||||
return False
|
||||
|
||||
if processor.model_name != model:
|
||||
print(f"❌ FAIL: Custom model not applied")
|
||||
return False
|
||||
|
||||
print("\n✅ PASS: FaceProcessor configuration working correctly")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ FAIL: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
|
||||
def test_cosine_similarity():
|
||||
"""Test 5: Cosine similarity calculation"""
|
||||
print("\n" + "="*60)
|
||||
print("Test 5: Cosine Similarity Calculation")
|
||||
print("="*60)
|
||||
|
||||
try:
|
||||
import numpy as np
|
||||
|
||||
db = DatabaseManager(":memory:", verbose=0)
|
||||
processor = FaceProcessor(db, verbose=0)
|
||||
|
||||
# Test with identical encodings
|
||||
encoding1 = np.random.rand(512).astype(np.float64)
|
||||
encoding2 = encoding1.copy()
|
||||
|
||||
distance = processor._calculate_cosine_similarity(encoding1, encoding2)
|
||||
print(f"✓ Identical encodings distance: {distance:.6f}")
|
||||
|
||||
if distance > 0.01: # Should be very close to 0
|
||||
print(f"❌ FAIL: Identical encodings should have distance near 0")
|
||||
return False
|
||||
|
||||
# Test with different encodings
|
||||
encoding3 = np.random.rand(512).astype(np.float64)
|
||||
distance2 = processor._calculate_cosine_similarity(encoding1, encoding3)
|
||||
print(f"✓ Different encodings distance: {distance2:.6f}")
|
||||
|
||||
if distance2 < 0.1: # Should be significantly different
|
||||
print(f"⚠️ Warning: Random encodings have low distance (might be coincidence)")
|
||||
|
||||
# Test with mismatched lengths
|
||||
encoding4 = np.random.rand(128).astype(np.float64)
|
||||
distance3 = processor._calculate_cosine_similarity(encoding1, encoding4)
|
||||
print(f"✓ Mismatched lengths distance: {distance3:.6f}")
|
||||
|
||||
if distance3 != 2.0: # Should return max distance
|
||||
print(f"❌ FAIL: Mismatched lengths should return 2.0")
|
||||
return False
|
||||
|
||||
print("\n✅ PASS: Cosine similarity calculation working correctly")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ FAIL: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
|
||||
def test_database_schema():
|
||||
"""Test 6: Database schema validation"""
|
||||
print("\n" + "="*60)
|
||||
print("Test 6: Database Schema Validation")
|
||||
print("="*60)
|
||||
|
||||
try:
|
||||
db = DatabaseManager(":memory:", verbose=0)
|
||||
|
||||
# Check if new DeepFace columns exist
|
||||
with db.get_db_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Get faces table schema
|
||||
cursor.execute("PRAGMA table_info(faces)")
|
||||
columns = {row[1]: row[2] for row in cursor.fetchall()}
|
||||
|
||||
print("✓ Faces table columns:")
|
||||
for col_name in columns:
|
||||
print(f" - {col_name}")
|
||||
|
||||
# Verify DeepFace columns
|
||||
required_columns = {
|
||||
'detector_backend': 'TEXT',
|
||||
'model_name': 'TEXT',
|
||||
'face_confidence': 'REAL'
|
||||
}
|
||||
|
||||
for col, dtype in required_columns.items():
|
||||
if col not in columns:
|
||||
print(f"❌ FAIL: Missing column '{col}' in faces table")
|
||||
return False
|
||||
print(f"✓ Column '{col}' exists with type {columns[col]}")
|
||||
|
||||
# Check person_encodings table
|
||||
cursor.execute("PRAGMA table_info(person_encodings)")
|
||||
pe_columns = {row[1]: row[2] for row in cursor.fetchall()}
|
||||
|
||||
print("\n✓ Person_encodings table columns:")
|
||||
for col_name in pe_columns:
|
||||
print(f" - {col_name}")
|
||||
|
||||
# Verify DeepFace columns in person_encodings
|
||||
pe_required = {
|
||||
'detector_backend': 'TEXT',
|
||||
'model_name': 'TEXT',
|
||||
}
|
||||
|
||||
for col, dtype in pe_required.items():
|
||||
if col not in pe_columns:
|
||||
print(f"❌ FAIL: Missing column '{col}' in person_encodings table")
|
||||
return False
|
||||
print(f"✓ Column '{col}' exists in person_encodings")
|
||||
|
||||
print("\n✅ PASS: Database schema is correct")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ FAIL: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
|
||||
def test_face_location_format():
|
||||
"""Test 7: Face location format validation"""
|
||||
print("\n" + "="*60)
|
||||
print("Test 7: Face Location Format")
|
||||
print("="*60)
|
||||
|
||||
try:
|
||||
import ast
|
||||
|
||||
db = DatabaseManager(":memory:", verbose=0)
|
||||
processor = FaceProcessor(db, verbose=1)
|
||||
|
||||
# Test with a sample image
|
||||
test_image = "demo_photos/2019-11-22_0011.jpg"
|
||||
if not os.path.exists(test_image):
|
||||
print(f"⚠️ Test image not found: {test_image}")
|
||||
return True # Skip but don't fail
|
||||
|
||||
# Add photo and process
|
||||
photo_id = db.add_photo(test_image, Path(test_image).name, None)
|
||||
processor.process_faces(limit=1)
|
||||
|
||||
# Check face location format
|
||||
with db.get_db_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT location FROM faces LIMIT 1")
|
||||
result = cursor.fetchone()
|
||||
|
||||
if not result:
|
||||
print("⚠️ No faces found")
|
||||
return True
|
||||
|
||||
location_str = result[0]
|
||||
print(f"✓ Raw location: {location_str}")
|
||||
|
||||
# Parse location
|
||||
try:
|
||||
location = ast.literal_eval(location_str)
|
||||
print(f"✓ Parsed location: {location}")
|
||||
|
||||
# Check if it's DeepFace format (dict with x, y, w, h)
|
||||
if isinstance(location, dict):
|
||||
required_keys = ['x', 'y', 'w', 'h']
|
||||
for key in required_keys:
|
||||
if key not in location:
|
||||
print(f"❌ FAIL: Missing key '{key}' in location dict")
|
||||
return False
|
||||
print("✓ Location is in DeepFace dict format {x, y, w, h}")
|
||||
else:
|
||||
print(f"❌ FAIL: Location is not a dict, got {type(location)}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ FAIL: Could not parse location: {e}")
|
||||
return False
|
||||
|
||||
print("\n✅ PASS: Face location format is correct")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ FAIL: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
|
||||
def test_performance_benchmark():
|
||||
"""Test 8: Performance benchmarking"""
|
||||
print("\n" + "="*60)
|
||||
print("Test 8: Performance Benchmark")
|
||||
print("="*60)
|
||||
|
||||
try:
|
||||
import time
|
||||
|
||||
db = DatabaseManager(":memory:", verbose=0)
|
||||
processor = FaceProcessor(db, verbose=0)
|
||||
|
||||
# Test with multiple images
|
||||
test_images = [
|
||||
"demo_photos/2019-11-22_0011.jpg",
|
||||
"demo_photos/2019-11-22_0012.jpg",
|
||||
"demo_photos/2019-11-22_0015.jpg",
|
||||
]
|
||||
|
||||
available_images = [img for img in test_images if os.path.exists(img)]
|
||||
|
||||
if not available_images:
|
||||
print("⚠️ No test images available")
|
||||
return True # Skip but don't fail
|
||||
|
||||
print(f"Testing with {len(available_images)} images")
|
||||
|
||||
# Add photos to database
|
||||
for img in available_images:
|
||||
db.add_photo(img, Path(img).name, None)
|
||||
|
||||
# Benchmark face detection
|
||||
start_time = time.time()
|
||||
count = processor.process_faces(limit=len(available_images))
|
||||
detection_time = time.time() - start_time
|
||||
|
||||
print(f"✓ Processed {count} photos in {detection_time:.2f}s")
|
||||
print(f"✓ Average time per photo: {detection_time/max(count, 1):.2f}s")
|
||||
|
||||
# Get statistics
|
||||
stats = db.get_statistics()
|
||||
total_faces = stats['total_faces']
|
||||
print(f"✓ Found {total_faces} total faces")
|
||||
|
||||
if total_faces > 0:
|
||||
print(f"✓ Average time per face: {detection_time/total_faces:.2f}s")
|
||||
|
||||
# Benchmark similarity calculation
|
||||
if total_faces >= 2:
|
||||
faces = db.get_all_face_encodings()
|
||||
face_id = faces[0][0]
|
||||
|
||||
start_time = time.time()
|
||||
matches = processor.find_similar_faces(face_id, tolerance=0.4)
|
||||
matching_time = time.time() - start_time
|
||||
|
||||
print(f"✓ Similarity search completed in {matching_time:.2f}s")
|
||||
print(f"✓ Found {len(matches)} matches")
|
||||
|
||||
print("\n✅ PASS: Performance benchmark completed")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ FAIL: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
|
||||
def test_adaptive_tolerance():
|
||||
"""Test 9: Adaptive tolerance calculation"""
|
||||
print("\n" + "="*60)
|
||||
print("Test 9: Adaptive Tolerance")
|
||||
print("="*60)
|
||||
|
||||
try:
|
||||
db = DatabaseManager(":memory:", verbose=0)
|
||||
processor = FaceProcessor(db, verbose=0)
|
||||
|
||||
# Test with different quality scores
|
||||
base_tolerance = 0.4
|
||||
|
||||
test_cases = [
|
||||
(0.1, "Low quality"),
|
||||
(0.5, "Medium quality"),
|
||||
(0.9, "High quality"),
|
||||
]
|
||||
|
||||
print(f"Base tolerance: {base_tolerance}")
|
||||
|
||||
for quality, desc in test_cases:
|
||||
tolerance = processor._calculate_adaptive_tolerance(base_tolerance, quality)
|
||||
print(f"✓ {desc} ({quality:.1f}): tolerance = {tolerance:.3f}")
|
||||
|
||||
# Verify tolerance is within bounds
|
||||
if tolerance < 0.2 or tolerance > 0.6:
|
||||
print(f"❌ FAIL: Tolerance {tolerance} out of bounds [0.2, 0.6]")
|
||||
return False
|
||||
|
||||
# Test with match confidence
|
||||
tolerance_with_conf = processor._calculate_adaptive_tolerance(
|
||||
base_tolerance, 0.7, match_confidence=0.8
|
||||
)
|
||||
print(f"✓ With match confidence: tolerance = {tolerance_with_conf:.3f}")
|
||||
|
||||
print("\n✅ PASS: Adaptive tolerance working correctly")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ FAIL: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
|
||||
def test_multiple_detectors():
|
||||
"""Test 10: Multiple detector backends"""
|
||||
print("\n" + "="*60)
|
||||
print("Test 10: Multiple Detector Backends")
|
||||
print("="*60)
|
||||
|
||||
try:
|
||||
# Test different detector backends
|
||||
detectors = ['opencv', 'ssd'] # Skip retinaface and mtcnn for speed
|
||||
test_image = "demo_photos/2019-11-22_0011.jpg"
|
||||
|
||||
if not os.path.exists(test_image):
|
||||
print("⚠️ Test image not found")
|
||||
return True # Skip but don't fail
|
||||
|
||||
results = {}
|
||||
|
||||
for detector in detectors:
|
||||
print(f"\n Testing with {detector} detector:")
|
||||
|
||||
try:
|
||||
db = DatabaseManager(":memory:", verbose=0)
|
||||
processor = FaceProcessor(db, verbose=0,
|
||||
detector_backend=detector,
|
||||
model_name='ArcFace')
|
||||
|
||||
photo_id = db.add_photo(test_image, Path(test_image).name, None)
|
||||
count = processor.process_faces(limit=1)
|
||||
|
||||
stats = db.get_statistics()
|
||||
faces_found = stats['total_faces']
|
||||
|
||||
results[detector] = faces_found
|
||||
print(f"✓ {detector}: Found {faces_found} faces")
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ {detector} failed: {e}")
|
||||
results[detector] = 0
|
||||
|
||||
# Verify at least one detector worked
|
||||
if sum(results.values()) == 0:
|
||||
print("\n❌ FAIL: No detectors found any faces")
|
||||
return False
|
||||
|
||||
print("\n✅ PASS: Multiple detectors tested")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ FAIL: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
|
||||
def run_all_tests():
|
||||
"""Run all DeepFace integration tests"""
|
||||
print("\n" + "="*70)
|
||||
print("DEEPFACE INTEGRATION TEST SUITE - PHASE 6")
|
||||
print("="*70)
|
||||
print()
|
||||
print("Testing complete DeepFace integration in PunimTag")
|
||||
print("This comprehensive test suite validates all aspects of the migration")
|
||||
print()
|
||||
|
||||
tests = [
|
||||
("Face Detection", test_face_detection),
|
||||
("Face Matching", test_face_matching),
|
||||
("Metadata Storage", test_deepface_metadata),
|
||||
("Configuration", test_configuration),
|
||||
("Cosine Similarity", test_cosine_similarity),
|
||||
("Database Schema", test_database_schema),
|
||||
("Face Location Format", test_face_location_format),
|
||||
("Performance Benchmark", test_performance_benchmark),
|
||||
("Adaptive Tolerance", test_adaptive_tolerance),
|
||||
("Multiple Detectors", test_multiple_detectors),
|
||||
]
|
||||
|
||||
results = []
|
||||
for test_name, test_func in tests:
|
||||
try:
|
||||
result = test_func()
|
||||
results.append((test_name, result))
|
||||
except Exception as e:
|
||||
print(f"\n❌ Test '{test_name}' crashed: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
results.append((test_name, False))
|
||||
|
||||
# Print summary
|
||||
print("\n" + "="*70)
|
||||
print("TEST SUMMARY")
|
||||
print("="*70)
|
||||
|
||||
passed = 0
|
||||
failed = 0
|
||||
for test_name, result in results:
|
||||
status = "✅ PASS" if result else "❌ FAIL"
|
||||
print(f"{status}: {test_name}")
|
||||
if result:
|
||||
passed += 1
|
||||
else:
|
||||
failed += 1
|
||||
|
||||
print("="*70)
|
||||
print(f"Tests passed: {passed}/{len(tests)}")
|
||||
print(f"Tests failed: {failed}/{len(tests)}")
|
||||
print("="*70)
|
||||
|
||||
if failed == 0:
|
||||
print("\n🎉 ALL TESTS PASSED! DeepFace integration is working correctly!")
|
||||
return 0
|
||||
else:
|
||||
print(f"\n⚠️ {failed} test(s) failed. Please review the errors above.")
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(run_all_tests())
|
||||
|
||||
@@ -1,329 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test Phase 1: Database Schema Updates for DeepFace Migration
|
||||
|
||||
This test verifies that:
|
||||
1. Database schema includes new DeepFace columns
|
||||
2. Method signatures accept new parameters
|
||||
3. Data can be inserted with DeepFace-specific fields
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import sqlite3
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
# Add parent directory to path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from src.core.database import DatabaseManager
|
||||
from src.core.config import (
|
||||
DEEPFACE_DETECTOR_BACKEND,
|
||||
DEEPFACE_MODEL_NAME,
|
||||
DEFAULT_FACE_TOLERANCE,
|
||||
DEEPFACE_SIMILARITY_THRESHOLD
|
||||
)
|
||||
|
||||
|
||||
def test_schema_has_deepface_columns():
|
||||
"""Test that database schema includes DeepFace columns"""
|
||||
print("\n🧪 Test 1: Verify schema has DeepFace columns")
|
||||
|
||||
# Create temporary database
|
||||
with tempfile.NamedTemporaryFile(delete=False, suffix='.db') as tmp:
|
||||
tmp_db_path = tmp.name
|
||||
|
||||
try:
|
||||
# Initialize database
|
||||
db = DatabaseManager(tmp_db_path, verbose=0)
|
||||
|
||||
# Connect and check schema
|
||||
conn = sqlite3.connect(tmp_db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Check faces table
|
||||
cursor.execute("PRAGMA table_info(faces)")
|
||||
faces_columns = {row[1]: row[2] for row in cursor.fetchall()}
|
||||
|
||||
required_columns = {
|
||||
'detector_backend': 'TEXT',
|
||||
'model_name': 'TEXT',
|
||||
'face_confidence': 'REAL'
|
||||
}
|
||||
|
||||
print(" Checking 'faces' table columns:")
|
||||
for col_name, col_type in required_columns.items():
|
||||
if col_name in faces_columns:
|
||||
print(f" ✓ {col_name} ({faces_columns[col_name]})")
|
||||
else:
|
||||
print(f" ❌ {col_name} - MISSING!")
|
||||
return False
|
||||
|
||||
# Check person_encodings table
|
||||
cursor.execute("PRAGMA table_info(person_encodings)")
|
||||
pe_columns = {row[1]: row[2] for row in cursor.fetchall()}
|
||||
|
||||
required_pe_columns = {
|
||||
'detector_backend': 'TEXT',
|
||||
'model_name': 'TEXT'
|
||||
}
|
||||
|
||||
print(" Checking 'person_encodings' table columns:")
|
||||
for col_name, col_type in required_pe_columns.items():
|
||||
if col_name in pe_columns:
|
||||
print(f" ✓ {col_name} ({pe_columns[col_name]})")
|
||||
else:
|
||||
print(f" ❌ {col_name} - MISSING!")
|
||||
return False
|
||||
|
||||
conn.close()
|
||||
print(" ✅ All schema columns present")
|
||||
return True
|
||||
|
||||
finally:
|
||||
# Cleanup
|
||||
if os.path.exists(tmp_db_path):
|
||||
os.unlink(tmp_db_path)
|
||||
|
||||
|
||||
def test_add_face_with_deepface_params():
|
||||
"""Test that add_face() accepts DeepFace parameters"""
|
||||
print("\n🧪 Test 2: Test add_face() with DeepFace parameters")
|
||||
|
||||
# Create temporary database
|
||||
with tempfile.NamedTemporaryFile(delete=False, suffix='.db') as tmp:
|
||||
tmp_db_path = tmp.name
|
||||
|
||||
try:
|
||||
# Initialize database
|
||||
db = DatabaseManager(tmp_db_path, verbose=0)
|
||||
|
||||
# Add a test photo
|
||||
photo_id = db.add_photo(
|
||||
photo_path="/test/photo.jpg",
|
||||
filename="photo.jpg",
|
||||
date_taken="2025-10-16"
|
||||
)
|
||||
|
||||
if not photo_id:
|
||||
print(" ❌ Failed to add photo")
|
||||
return False
|
||||
|
||||
print(f" ✓ Added test photo (ID: {photo_id})")
|
||||
|
||||
# Create dummy 512-dimensional encoding (ArcFace)
|
||||
import numpy as np
|
||||
dummy_encoding = np.random.rand(512).astype(np.float64)
|
||||
encoding_bytes = dummy_encoding.tobytes()
|
||||
|
||||
# Add face with DeepFace parameters
|
||||
face_id = db.add_face(
|
||||
photo_id=photo_id,
|
||||
encoding=encoding_bytes,
|
||||
location="{'x': 100, 'y': 150, 'w': 200, 'h': 200}",
|
||||
confidence=0.0,
|
||||
quality_score=0.85,
|
||||
person_id=None,
|
||||
detector_backend='retinaface',
|
||||
model_name='ArcFace',
|
||||
face_confidence=0.99
|
||||
)
|
||||
|
||||
if not face_id:
|
||||
print(" ❌ Failed to add face")
|
||||
return False
|
||||
|
||||
print(f" ✓ Added face with DeepFace params (ID: {face_id})")
|
||||
|
||||
# Verify data was stored correctly
|
||||
conn = sqlite3.connect(tmp_db_path)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('''
|
||||
SELECT detector_backend, model_name, face_confidence, quality_score
|
||||
FROM faces WHERE id = ?
|
||||
''', (face_id,))
|
||||
|
||||
result = cursor.fetchone()
|
||||
conn.close()
|
||||
|
||||
if not result:
|
||||
print(" ❌ Face data not found in database")
|
||||
return False
|
||||
|
||||
detector, model, face_conf, quality = result
|
||||
|
||||
print(f" ✓ Verified stored data:")
|
||||
print(f" - detector_backend: {detector}")
|
||||
print(f" - model_name: {model}")
|
||||
print(f" - face_confidence: {face_conf}")
|
||||
print(f" - quality_score: {quality}")
|
||||
|
||||
if detector != 'retinaface' or model != 'ArcFace' or face_conf != 0.99:
|
||||
print(" ❌ Stored data doesn't match input")
|
||||
return False
|
||||
|
||||
print(" ✅ add_face() works with DeepFace parameters")
|
||||
return True
|
||||
|
||||
finally:
|
||||
# Cleanup
|
||||
if os.path.exists(tmp_db_path):
|
||||
os.unlink(tmp_db_path)
|
||||
|
||||
|
||||
def test_add_person_encoding_with_deepface_params():
|
||||
"""Test that add_person_encoding() accepts DeepFace parameters"""
|
||||
print("\n🧪 Test 3: Test add_person_encoding() with DeepFace parameters")
|
||||
|
||||
# Create temporary database
|
||||
with tempfile.NamedTemporaryFile(delete=False, suffix='.db') as tmp:
|
||||
tmp_db_path = tmp.name
|
||||
|
||||
try:
|
||||
# Initialize database
|
||||
db = DatabaseManager(tmp_db_path, verbose=0)
|
||||
|
||||
# Add a test person
|
||||
person_id = db.add_person(
|
||||
first_name="Test",
|
||||
last_name="Person",
|
||||
middle_name="",
|
||||
maiden_name="",
|
||||
date_of_birth=""
|
||||
)
|
||||
|
||||
print(f" ✓ Added test person (ID: {person_id})")
|
||||
|
||||
# Add a test photo and face
|
||||
photo_id = db.add_photo("/test/photo.jpg", "photo.jpg")
|
||||
|
||||
import numpy as np
|
||||
dummy_encoding = np.random.rand(512).astype(np.float64)
|
||||
encoding_bytes = dummy_encoding.tobytes()
|
||||
|
||||
face_id = db.add_face(
|
||||
photo_id=photo_id,
|
||||
encoding=encoding_bytes,
|
||||
location="{'x': 100, 'y': 150, 'w': 200, 'h': 200}",
|
||||
quality_score=0.85,
|
||||
detector_backend='retinaface',
|
||||
model_name='ArcFace'
|
||||
)
|
||||
|
||||
print(f" ✓ Added test face (ID: {face_id})")
|
||||
|
||||
# Add person encoding with DeepFace parameters
|
||||
db.add_person_encoding(
|
||||
person_id=person_id,
|
||||
face_id=face_id,
|
||||
encoding=encoding_bytes,
|
||||
quality_score=0.85,
|
||||
detector_backend='retinaface',
|
||||
model_name='ArcFace'
|
||||
)
|
||||
|
||||
# Verify data was stored
|
||||
conn = sqlite3.connect(tmp_db_path)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('''
|
||||
SELECT detector_backend, model_name, quality_score
|
||||
FROM person_encodings WHERE person_id = ? AND face_id = ?
|
||||
''', (person_id, face_id))
|
||||
|
||||
result = cursor.fetchone()
|
||||
conn.close()
|
||||
|
||||
if not result:
|
||||
print(" ❌ Person encoding not found in database")
|
||||
return False
|
||||
|
||||
detector, model, quality = result
|
||||
|
||||
print(f" ✓ Verified stored data:")
|
||||
print(f" - detector_backend: {detector}")
|
||||
print(f" - model_name: {model}")
|
||||
print(f" - quality_score: {quality}")
|
||||
|
||||
if detector != 'retinaface' or model != 'ArcFace':
|
||||
print(" ❌ Stored data doesn't match input")
|
||||
return False
|
||||
|
||||
print(" ✅ add_person_encoding() works with DeepFace parameters")
|
||||
return True
|
||||
|
||||
finally:
|
||||
# Cleanup
|
||||
if os.path.exists(tmp_db_path):
|
||||
os.unlink(tmp_db_path)
|
||||
|
||||
|
||||
def test_config_constants():
|
||||
"""Test that config.py has DeepFace constants"""
|
||||
print("\n🧪 Test 4: Verify DeepFace configuration constants")
|
||||
|
||||
print(f" ✓ DEEPFACE_DETECTOR_BACKEND = {DEEPFACE_DETECTOR_BACKEND}")
|
||||
print(f" ✓ DEEPFACE_MODEL_NAME = {DEEPFACE_MODEL_NAME}")
|
||||
print(f" ✓ DEFAULT_FACE_TOLERANCE = {DEFAULT_FACE_TOLERANCE}")
|
||||
print(f" ✓ DEEPFACE_SIMILARITY_THRESHOLD = {DEEPFACE_SIMILARITY_THRESHOLD}")
|
||||
|
||||
if DEEPFACE_DETECTOR_BACKEND != 'retinaface':
|
||||
print(f" ⚠️ Warning: Expected detector 'retinaface', got '{DEEPFACE_DETECTOR_BACKEND}'")
|
||||
|
||||
if DEEPFACE_MODEL_NAME != 'ArcFace':
|
||||
print(f" ⚠️ Warning: Expected model 'ArcFace', got '{DEEPFACE_MODEL_NAME}'")
|
||||
|
||||
if DEFAULT_FACE_TOLERANCE != 0.4:
|
||||
print(f" ⚠️ Warning: Expected tolerance 0.4, got {DEFAULT_FACE_TOLERANCE}")
|
||||
|
||||
print(" ✅ Configuration constants loaded")
|
||||
return True
|
||||
|
||||
|
||||
def run_all_tests():
|
||||
"""Run all Phase 1 tests"""
|
||||
print("=" * 70)
|
||||
print("Phase 1 Schema Tests - DeepFace Migration")
|
||||
print("=" * 70)
|
||||
|
||||
tests = [
|
||||
("Schema Columns", test_schema_has_deepface_columns),
|
||||
("add_face() Method", test_add_face_with_deepface_params),
|
||||
("add_person_encoding() Method", test_add_person_encoding_with_deepface_params),
|
||||
("Config Constants", test_config_constants)
|
||||
]
|
||||
|
||||
results = []
|
||||
for test_name, test_func in tests:
|
||||
try:
|
||||
result = test_func()
|
||||
results.append((test_name, result))
|
||||
except Exception as e:
|
||||
print(f" ❌ Test failed with exception: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
results.append((test_name, False))
|
||||
|
||||
print("\n" + "=" * 70)
|
||||
print("Test Results Summary")
|
||||
print("=" * 70)
|
||||
|
||||
for test_name, result in results:
|
||||
status = "✅ PASS" if result else "❌ FAIL"
|
||||
print(f" {status}: {test_name}")
|
||||
|
||||
passed = sum(1 for _, result in results if result)
|
||||
total = len(results)
|
||||
|
||||
print()
|
||||
print(f"Tests passed: {passed}/{total}")
|
||||
print("=" * 70)
|
||||
|
||||
return all(result for _, result in results)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
success = run_all_tests()
|
||||
sys.exit(0 if success else 1)
|
||||
|
||||
|
||||
@@ -1,232 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test Phase 2: Configuration Updates for DeepFace Migration
|
||||
|
||||
This test verifies that:
|
||||
1. TensorFlow suppression is in place
|
||||
2. FaceProcessor accepts detector_backend and model_name
|
||||
3. Configuration constants are accessible
|
||||
4. Entry points properly suppress warnings
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Add parent directory to path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
|
||||
def test_tensorflow_suppression():
|
||||
"""Test that TensorFlow warnings are suppressed"""
|
||||
print("\n🧪 Test 1: Verify TensorFlow suppression in config")
|
||||
|
||||
# Import config which sets the environment variable
|
||||
from src.core import config
|
||||
|
||||
# Check environment variable is set (config.py sets it on import)
|
||||
tf_log_level = os.environ.get('TF_CPP_MIN_LOG_LEVEL')
|
||||
|
||||
if tf_log_level == '3':
|
||||
print(" ✓ TF_CPP_MIN_LOG_LEVEL = 3 (suppressed by config.py)")
|
||||
print(" ✓ Entry points also set this before imports")
|
||||
return True
|
||||
else:
|
||||
print(f" ❌ TF_CPP_MIN_LOG_LEVEL = {tf_log_level} (expected '3')")
|
||||
return False
|
||||
|
||||
|
||||
def test_faceprocessor_initialization():
|
||||
"""Test that FaceProcessor accepts DeepFace parameters"""
|
||||
print("\n🧪 Test 2: Test FaceProcessor with DeepFace parameters")
|
||||
|
||||
import tempfile
|
||||
from src.core.database import DatabaseManager
|
||||
from src.core.face_processing import FaceProcessor
|
||||
|
||||
try:
|
||||
# Create temporary database
|
||||
with tempfile.NamedTemporaryFile(delete=False, suffix='.db') as tmp:
|
||||
tmp_db_path = tmp.name
|
||||
|
||||
# Initialize database and face processor
|
||||
db = DatabaseManager(tmp_db_path, verbose=0)
|
||||
|
||||
# Test with custom detector and model
|
||||
processor = FaceProcessor(
|
||||
db,
|
||||
verbose=0,
|
||||
detector_backend='mtcnn',
|
||||
model_name='Facenet'
|
||||
)
|
||||
|
||||
print(f" ✓ FaceProcessor initialized")
|
||||
print(f" - detector_backend: {processor.detector_backend}")
|
||||
print(f" - model_name: {processor.model_name}")
|
||||
|
||||
if processor.detector_backend != 'mtcnn':
|
||||
print(" ❌ Detector backend not set correctly")
|
||||
return False
|
||||
|
||||
if processor.model_name != 'Facenet':
|
||||
print(" ❌ Model name not set correctly")
|
||||
return False
|
||||
|
||||
# Test with defaults
|
||||
processor2 = FaceProcessor(db, verbose=0)
|
||||
print(f" ✓ FaceProcessor with defaults:")
|
||||
print(f" - detector_backend: {processor2.detector_backend}")
|
||||
print(f" - model_name: {processor2.model_name}")
|
||||
|
||||
# Cleanup
|
||||
if os.path.exists(tmp_db_path):
|
||||
os.unlink(tmp_db_path)
|
||||
|
||||
print(" ✅ FaceProcessor accepts and uses DeepFace parameters")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f" ❌ Error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
|
||||
def test_config_imports():
|
||||
"""Test that all DeepFace config constants can be imported"""
|
||||
print("\n🧪 Test 3: Test configuration imports")
|
||||
|
||||
try:
|
||||
from src.core.config import (
|
||||
DEEPFACE_DETECTOR_BACKEND,
|
||||
DEEPFACE_MODEL_NAME,
|
||||
DEEPFACE_DETECTOR_OPTIONS,
|
||||
DEEPFACE_MODEL_OPTIONS,
|
||||
DEEPFACE_DISTANCE_METRIC,
|
||||
DEEPFACE_ENFORCE_DETECTION,
|
||||
DEEPFACE_ALIGN_FACES,
|
||||
DEEPFACE_SIMILARITY_THRESHOLD
|
||||
)
|
||||
|
||||
print(" ✓ All DeepFace config constants imported:")
|
||||
print(f" - DEEPFACE_DETECTOR_BACKEND = {DEEPFACE_DETECTOR_BACKEND}")
|
||||
print(f" - DEEPFACE_MODEL_NAME = {DEEPFACE_MODEL_NAME}")
|
||||
print(f" - DEEPFACE_DETECTOR_OPTIONS = {DEEPFACE_DETECTOR_OPTIONS}")
|
||||
print(f" - DEEPFACE_MODEL_OPTIONS = {DEEPFACE_MODEL_OPTIONS}")
|
||||
print(f" - DEEPFACE_DISTANCE_METRIC = {DEEPFACE_DISTANCE_METRIC}")
|
||||
print(f" - DEEPFACE_ENFORCE_DETECTION = {DEEPFACE_ENFORCE_DETECTION}")
|
||||
print(f" - DEEPFACE_ALIGN_FACES = {DEEPFACE_ALIGN_FACES}")
|
||||
print(f" - DEEPFACE_SIMILARITY_THRESHOLD = {DEEPFACE_SIMILARITY_THRESHOLD}")
|
||||
|
||||
print(" ✅ All configuration constants accessible")
|
||||
return True
|
||||
|
||||
except ImportError as e:
|
||||
print(f" ❌ Failed to import config: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def test_entry_point_imports():
|
||||
"""Test that main entry points can be imported without errors"""
|
||||
print("\n🧪 Test 4: Test entry point imports (with TF suppression)")
|
||||
|
||||
try:
|
||||
# Desktop GUI has been archived - skip this test
|
||||
print(" ⚠️ Desktop GUI entry points have been archived")
|
||||
print(" ⚠️ Skipping desktop entry point import test")
|
||||
print(" ✓ Web version entry points are available via API")
|
||||
|
||||
print(" ✅ Entry point test skipped (desktop archived)")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f" ❌ Import error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
|
||||
def test_gui_config_constants():
|
||||
"""Test that GUI can access DeepFace options"""
|
||||
print("\n🧪 Test 5: Test GUI access to DeepFace options")
|
||||
|
||||
try:
|
||||
from src.core.config import DEEPFACE_DETECTOR_OPTIONS, DEEPFACE_MODEL_OPTIONS
|
||||
|
||||
# Verify options are lists
|
||||
if not isinstance(DEEPFACE_DETECTOR_OPTIONS, list):
|
||||
print(" ❌ DEEPFACE_DETECTOR_OPTIONS is not a list")
|
||||
return False
|
||||
|
||||
if not isinstance(DEEPFACE_MODEL_OPTIONS, list):
|
||||
print(" ❌ DEEPFACE_MODEL_OPTIONS is not a list")
|
||||
return False
|
||||
|
||||
print(f" ✓ Detector options ({len(DEEPFACE_DETECTOR_OPTIONS)}): {DEEPFACE_DETECTOR_OPTIONS}")
|
||||
print(f" ✓ Model options ({len(DEEPFACE_MODEL_OPTIONS)}): {DEEPFACE_MODEL_OPTIONS}")
|
||||
|
||||
# Verify expected values
|
||||
expected_detectors = ["retinaface", "mtcnn", "opencv", "ssd"]
|
||||
expected_models = ["ArcFace", "Facenet", "Facenet512", "VGG-Face"]
|
||||
|
||||
if set(DEEPFACE_DETECTOR_OPTIONS) != set(expected_detectors):
|
||||
print(f" ⚠️ Detector options don't match expected: {expected_detectors}")
|
||||
|
||||
if set(DEEPFACE_MODEL_OPTIONS) != set(expected_models):
|
||||
print(f" ⚠️ Model options don't match expected: {expected_models}")
|
||||
|
||||
print(" ✅ GUI can access DeepFace options for dropdowns")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f" ❌ Error: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def run_all_tests():
|
||||
"""Run all Phase 2 tests"""
|
||||
print("=" * 70)
|
||||
print("Phase 2 Configuration Tests - DeepFace Migration")
|
||||
print("=" * 70)
|
||||
|
||||
tests = [
|
||||
("TensorFlow Suppression", test_tensorflow_suppression),
|
||||
("FaceProcessor Initialization", test_faceprocessor_initialization),
|
||||
("Config Imports", test_config_imports),
|
||||
("Entry Point Imports", test_entry_point_imports),
|
||||
("GUI Config Constants", test_gui_config_constants)
|
||||
]
|
||||
|
||||
results = []
|
||||
for test_name, test_func in tests:
|
||||
try:
|
||||
result = test_func()
|
||||
results.append((test_name, result))
|
||||
except Exception as e:
|
||||
print(f" ❌ Test failed with exception: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
results.append((test_name, False))
|
||||
|
||||
print("\n" + "=" * 70)
|
||||
print("Test Results Summary")
|
||||
print("=" * 70)
|
||||
|
||||
for test_name, result in results:
|
||||
status = "✅ PASS" if result else "❌ FAIL"
|
||||
print(f" {status}: {test_name}")
|
||||
|
||||
passed = sum(1 for _, result in results if result)
|
||||
total = len(results)
|
||||
|
||||
print()
|
||||
print(f"Tests passed: {passed}/{total}")
|
||||
print("=" * 70)
|
||||
|
||||
return all(result for _, result in results)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
success = run_all_tests()
|
||||
sys.exit(0 if success else 1)
|
||||
|
||||
@@ -1,326 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test Phase 3: Core Face Processing with DeepFace
|
||||
|
||||
This test verifies that:
|
||||
1. DeepFace can be imported and used
|
||||
2. Face detection works with DeepFace
|
||||
3. Face encodings are 512-dimensional (ArcFace)
|
||||
4. Cosine similarity calculation works
|
||||
5. Location format handling works (dict vs tuple)
|
||||
6. Full end-to-end processing works
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import numpy as np
|
||||
from pathlib import Path
|
||||
|
||||
# Suppress TensorFlow warnings
|
||||
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'
|
||||
import warnings
|
||||
warnings.filterwarnings('ignore')
|
||||
|
||||
# Add parent directory to path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
|
||||
def test_deepface_import():
|
||||
"""Test that DeepFace can be imported"""
|
||||
print("\n🧪 Test 1: DeepFace Import")
|
||||
|
||||
try:
|
||||
from deepface import DeepFace
|
||||
print(f" ✓ DeepFace imported successfully")
|
||||
print(f" ✓ Version: {DeepFace.__version__ if hasattr(DeepFace, '__version__') else 'unknown'}")
|
||||
return True
|
||||
except ImportError as e:
|
||||
print(f" ❌ Failed to import DeepFace: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def test_deepface_detection():
|
||||
"""Test DeepFace face detection"""
|
||||
print("\n🧪 Test 2: DeepFace Face Detection")
|
||||
|
||||
try:
|
||||
from deepface import DeepFace
|
||||
|
||||
# Check for test images
|
||||
test_folder = Path("demo_photos/testdeepface")
|
||||
if not test_folder.exists():
|
||||
test_folder = Path("demo_photos")
|
||||
|
||||
test_images = list(test_folder.glob("*.jpg")) + list(test_folder.glob("*.JPG"))
|
||||
if not test_images:
|
||||
print(" ⚠️ No test images found, skipping")
|
||||
return True
|
||||
|
||||
test_image = str(test_images[0])
|
||||
print(f" Testing with: {Path(test_image).name}")
|
||||
|
||||
# Try to detect faces
|
||||
results = DeepFace.represent(
|
||||
img_path=test_image,
|
||||
model_name='ArcFace',
|
||||
detector_backend='retinaface',
|
||||
enforce_detection=False,
|
||||
align=True
|
||||
)
|
||||
|
||||
if results:
|
||||
print(f" ✓ Found {len(results)} face(s)")
|
||||
|
||||
# Check encoding dimensions
|
||||
encoding = np.array(results[0]['embedding'])
|
||||
print(f" ✓ Encoding shape: {encoding.shape}")
|
||||
|
||||
if len(encoding) == 512:
|
||||
print(f" ✓ Correct encoding size (512-dimensional for ArcFace)")
|
||||
else:
|
||||
print(f" ⚠️ Unexpected encoding size: {len(encoding)}")
|
||||
|
||||
# Check facial_area format
|
||||
facial_area = results[0].get('facial_area', {})
|
||||
print(f" ✓ Facial area: {facial_area}")
|
||||
|
||||
if all(k in facial_area for k in ['x', 'y', 'w', 'h']):
|
||||
print(f" ✓ Correct facial area format (x, y, w, h)")
|
||||
else:
|
||||
print(f" ⚠️ Unexpected facial area format")
|
||||
|
||||
return True
|
||||
else:
|
||||
print(f" ⚠️ No faces detected (image may have no faces)")
|
||||
return True # Not a failure, just no faces
|
||||
|
||||
except Exception as e:
|
||||
print(f" ❌ Error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
|
||||
def test_cosine_similarity():
|
||||
"""Test cosine similarity calculation"""
|
||||
print("\n🧪 Test 3: Cosine Similarity Calculation")
|
||||
|
||||
try:
|
||||
from src.core.database import DatabaseManager
|
||||
from src.core.face_processing import FaceProcessor
|
||||
|
||||
# Create temporary database
|
||||
with tempfile.NamedTemporaryFile(delete=False, suffix='.db') as tmp:
|
||||
tmp_db_path = tmp.name
|
||||
|
||||
db = DatabaseManager(tmp_db_path, verbose=0)
|
||||
processor = FaceProcessor(db, verbose=0)
|
||||
|
||||
# Test with identical encodings
|
||||
enc1 = np.random.rand(512)
|
||||
distance_identical = processor._calculate_cosine_similarity(enc1, enc1)
|
||||
print(f" ✓ Identical encodings distance: {distance_identical:.6f}")
|
||||
|
||||
if distance_identical < 0.01: # Should be very close to 0
|
||||
print(f" ✓ Identical encodings produce near-zero distance")
|
||||
else:
|
||||
print(f" ⚠️ Identical encodings distance higher than expected")
|
||||
|
||||
# Test with different encodings
|
||||
enc2 = np.random.rand(512)
|
||||
distance_different = processor._calculate_cosine_similarity(enc1, enc2)
|
||||
print(f" ✓ Different encodings distance: {distance_different:.6f}")
|
||||
|
||||
if 0 < distance_different < 2: # Should be in valid range
|
||||
print(f" ✓ Different encodings produce valid distance")
|
||||
else:
|
||||
print(f" ⚠️ Distance out of expected range [0, 2]")
|
||||
|
||||
# Test with length mismatch
|
||||
enc3 = np.random.rand(128) # Different length
|
||||
distance_mismatch = processor._calculate_cosine_similarity(enc1, enc3)
|
||||
print(f" ✓ Mismatched length distance: {distance_mismatch:.6f}")
|
||||
|
||||
if distance_mismatch == 2.0: # Should return max distance
|
||||
print(f" ✓ Mismatched lengths handled correctly")
|
||||
else:
|
||||
print(f" ⚠️ Mismatch handling unexpected")
|
||||
|
||||
# Cleanup
|
||||
if os.path.exists(tmp_db_path):
|
||||
os.unlink(tmp_db_path)
|
||||
|
||||
print(" ✅ Cosine similarity calculation works correctly")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f" ❌ Error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
|
||||
def test_location_format_handling():
|
||||
"""Test handling of both dict and tuple location formats"""
|
||||
print("\n🧪 Test 4: Location Format Handling")
|
||||
|
||||
try:
|
||||
# Test dict format (DeepFace)
|
||||
location_dict = {'x': 100, 'y': 150, 'w': 200, 'h': 200}
|
||||
location_str_dict = str(location_dict)
|
||||
|
||||
import ast
|
||||
parsed_dict = ast.literal_eval(location_str_dict)
|
||||
|
||||
if isinstance(parsed_dict, dict):
|
||||
left = parsed_dict.get('x', 0)
|
||||
top = parsed_dict.get('y', 0)
|
||||
width = parsed_dict.get('w', 0)
|
||||
height = parsed_dict.get('h', 0)
|
||||
right = left + width
|
||||
bottom = top + height
|
||||
|
||||
print(f" ✓ Dict format parsed: {location_dict}")
|
||||
print(f" ✓ Converted to box: top={top}, right={right}, bottom={bottom}, left={left}")
|
||||
|
||||
if (left == 100 and top == 150 and right == 300 and bottom == 350):
|
||||
print(f" ✓ Dict conversion correct")
|
||||
else:
|
||||
print(f" ❌ Dict conversion incorrect")
|
||||
return False
|
||||
|
||||
# Legacy tuple format tests removed - only DeepFace format supported
|
||||
|
||||
print(" ✅ DeepFace location format handled correctly")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f" ❌ Error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
|
||||
def test_end_to_end_processing():
|
||||
"""Test end-to-end face processing with DeepFace"""
|
||||
print("\n🧪 Test 5: End-to-End Processing")
|
||||
|
||||
try:
|
||||
from src.core.database import DatabaseManager
|
||||
from src.core.face_processing import FaceProcessor
|
||||
|
||||
# Check for test images
|
||||
test_folder = Path("demo_photos/testdeepface")
|
||||
if not test_folder.exists():
|
||||
test_folder = Path("demo_photos")
|
||||
|
||||
test_images = list(test_folder.glob("*.jpg")) + list(test_folder.glob("*.JPG"))
|
||||
if not test_images:
|
||||
print(" ⚠️ No test images found, skipping")
|
||||
return True
|
||||
|
||||
# Create temporary database
|
||||
with tempfile.NamedTemporaryFile(delete=False, suffix='.db') as tmp:
|
||||
tmp_db_path = tmp.name
|
||||
|
||||
db = DatabaseManager(tmp_db_path, verbose=0)
|
||||
processor = FaceProcessor(db, verbose=1,
|
||||
detector_backend='retinaface',
|
||||
model_name='ArcFace')
|
||||
|
||||
# Add a test photo
|
||||
test_image = str(test_images[0])
|
||||
photo_id = db.add_photo(test_image, Path(test_image).name, None)
|
||||
|
||||
if not photo_id:
|
||||
print(f" ❌ Failed to add photo")
|
||||
return False
|
||||
|
||||
print(f" ✓ Added test photo (ID: {photo_id})")
|
||||
|
||||
# Process faces
|
||||
print(f" Processing faces...")
|
||||
count = processor.process_faces(limit=1)
|
||||
|
||||
print(f" ✓ Processed {count} photo(s)")
|
||||
|
||||
# Verify results
|
||||
stats = db.get_statistics()
|
||||
print(f" ✓ Statistics: {stats['total_faces']} faces found")
|
||||
|
||||
if stats['total_faces'] > 0:
|
||||
# Check encoding size
|
||||
faces = db.get_all_face_encodings()
|
||||
if faces:
|
||||
face_id, encoding_bytes, person_id, quality = faces[0]
|
||||
encoding = np.frombuffer(encoding_bytes, dtype=np.float64)
|
||||
print(f" ✓ Encoding size: {len(encoding)} dimensions")
|
||||
|
||||
if len(encoding) == 512:
|
||||
print(f" ✅ Correct encoding size (512-dim ArcFace)")
|
||||
else:
|
||||
print(f" ⚠️ Unexpected encoding size: {len(encoding)}")
|
||||
|
||||
# Cleanup
|
||||
if os.path.exists(tmp_db_path):
|
||||
os.unlink(tmp_db_path)
|
||||
|
||||
print(" ✅ End-to-end processing successful")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f" ❌ Error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
|
||||
def run_all_tests():
|
||||
"""Run all Phase 3 tests"""
|
||||
print("=" * 70)
|
||||
print("Phase 3 DeepFace Integration Tests")
|
||||
print("=" * 70)
|
||||
|
||||
tests = [
|
||||
("DeepFace Import", test_deepface_import),
|
||||
("DeepFace Detection", test_deepface_detection),
|
||||
("Cosine Similarity", test_cosine_similarity),
|
||||
("Location Format Handling", test_location_format_handling),
|
||||
("End-to-End Processing", test_end_to_end_processing)
|
||||
]
|
||||
|
||||
results = []
|
||||
for test_name, test_func in tests:
|
||||
try:
|
||||
result = test_func()
|
||||
results.append((test_name, result))
|
||||
except Exception as e:
|
||||
print(f" ❌ Test failed with exception: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
results.append((test_name, False))
|
||||
|
||||
print("\n" + "=" * 70)
|
||||
print("Test Results Summary")
|
||||
print("=" * 70)
|
||||
|
||||
for test_name, result in results:
|
||||
status = "✅ PASS" if result else "❌ FAIL"
|
||||
print(f" {status}: {test_name}")
|
||||
|
||||
passed = sum(1 for _, result in results if result)
|
||||
total = len(results)
|
||||
|
||||
print()
|
||||
print(f"Tests passed: {passed}/{total}")
|
||||
print("=" * 70)
|
||||
|
||||
return all(result for _, result in results)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
success = run_all_tests()
|
||||
sys.exit(0 if success else 1)
|
||||
|
||||
|
||||
@@ -1,427 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Phase 4 Integration Test: GUI Updates for DeepFace
|
||||
Tests that all GUI panels correctly handle DeepFace metadata and location formats
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import sqlite3
|
||||
from pathlib import Path
|
||||
|
||||
# Add parent directory to path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
# Suppress TensorFlow warnings
|
||||
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'
|
||||
import warnings
|
||||
warnings.filterwarnings('ignore')
|
||||
|
||||
from src.core.database import DatabaseManager
|
||||
from src.core.face_processing import FaceProcessor
|
||||
from src.core.config import DEEPFACE_DETECTOR_BACKEND, DEEPFACE_MODEL_NAME
|
||||
|
||||
|
||||
def test_database_schema():
|
||||
"""Test 1: Verify database schema has DeepFace columns"""
|
||||
print("\n" + "="*60)
|
||||
print("Test 1: Database Schema with DeepFace Columns")
|
||||
print("="*60)
|
||||
|
||||
try:
|
||||
# Create in-memory database
|
||||
db = DatabaseManager(":memory:", verbose=0)
|
||||
|
||||
# Check faces table schema
|
||||
with db.get_db_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("PRAGMA table_info(faces)")
|
||||
columns = {row[1]: row[2] for row in cursor.fetchall()}
|
||||
|
||||
# Verify DeepFace columns exist
|
||||
required_columns = {
|
||||
'id': 'INTEGER',
|
||||
'photo_id': 'INTEGER',
|
||||
'person_id': 'INTEGER',
|
||||
'encoding': 'BLOB',
|
||||
'location': 'TEXT',
|
||||
'confidence': 'REAL',
|
||||
'quality_score': 'REAL',
|
||||
'detector_backend': 'TEXT',
|
||||
'model_name': 'TEXT',
|
||||
'face_confidence': 'REAL'
|
||||
}
|
||||
|
||||
missing_columns = []
|
||||
for col_name, col_type in required_columns.items():
|
||||
if col_name not in columns:
|
||||
missing_columns.append(col_name)
|
||||
else:
|
||||
print(f"✓ Column '{col_name}' exists with type '{columns[col_name]}'")
|
||||
|
||||
if missing_columns:
|
||||
print(f"\n❌ FAIL: Missing columns: {missing_columns}")
|
||||
return False
|
||||
|
||||
print("\n✅ PASS: All DeepFace columns present in database schema")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ FAIL: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
|
||||
def test_face_data_retrieval():
|
||||
"""Test 2: Verify face data retrieval includes DeepFace metadata"""
|
||||
print("\n" + "="*60)
|
||||
print("Test 2: Face Data Retrieval with DeepFace Metadata")
|
||||
print("="*60)
|
||||
|
||||
try:
|
||||
# Create in-memory database
|
||||
db = DatabaseManager(":memory:", verbose=0)
|
||||
|
||||
# Create a test photo
|
||||
test_photo_path = "/tmp/test_photo.jpg"
|
||||
photo_id = db.add_photo(test_photo_path, "test_photo.jpg", None)
|
||||
|
||||
# Create a test face with DeepFace metadata
|
||||
import numpy as np
|
||||
test_encoding = np.random.rand(512).astype(np.float64) # 512-dim for ArcFace
|
||||
test_location = "{'x': 100, 'y': 100, 'w': 50, 'h': 50}"
|
||||
|
||||
face_id = db.add_face(
|
||||
photo_id=photo_id,
|
||||
encoding=test_encoding.tobytes(),
|
||||
location=test_location,
|
||||
confidence=0.0,
|
||||
quality_score=0.85,
|
||||
person_id=None,
|
||||
detector_backend='retinaface',
|
||||
model_name='ArcFace',
|
||||
face_confidence=0.95
|
||||
)
|
||||
|
||||
print(f"✓ Created test face with ID {face_id}")
|
||||
|
||||
# Query the face data (simulating GUI panel queries)
|
||||
with db.get_db_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("""
|
||||
SELECT f.id, f.photo_id, p.path, p.filename, f.location,
|
||||
f.face_confidence, f.quality_score, f.detector_backend, f.model_name
|
||||
FROM faces f
|
||||
JOIN photos p ON f.photo_id = p.id
|
||||
WHERE f.id = ?
|
||||
""", (face_id,))
|
||||
|
||||
result = cursor.fetchone()
|
||||
|
||||
if not result:
|
||||
print("\n❌ FAIL: Could not retrieve face data")
|
||||
return False
|
||||
|
||||
# Unpack the result (9 fields)
|
||||
face_id_ret, photo_id_ret, path, filename, location, face_conf, quality, detector, model = result
|
||||
|
||||
print(f"✓ Retrieved face data:")
|
||||
print(f" - Face ID: {face_id_ret}")
|
||||
print(f" - Photo ID: {photo_id_ret}")
|
||||
print(f" - Location: {location}")
|
||||
print(f" - Face Confidence: {face_conf}")
|
||||
print(f" - Quality Score: {quality}")
|
||||
print(f" - Detector: {detector}")
|
||||
print(f" - Model: {model}")
|
||||
|
||||
# Verify the metadata
|
||||
if face_conf != 0.95:
|
||||
print(f"\n❌ FAIL: Face confidence mismatch: expected 0.95, got {face_conf}")
|
||||
return False
|
||||
|
||||
if quality != 0.85:
|
||||
print(f"\n❌ FAIL: Quality score mismatch: expected 0.85, got {quality}")
|
||||
return False
|
||||
|
||||
if detector != 'retinaface':
|
||||
print(f"\n❌ FAIL: Detector mismatch: expected 'retinaface', got {detector}")
|
||||
return False
|
||||
|
||||
if model != 'ArcFace':
|
||||
print(f"\n❌ FAIL: Model mismatch: expected 'ArcFace', got {model}")
|
||||
return False
|
||||
|
||||
print("\n✅ PASS: Face data retrieval includes all DeepFace metadata")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ FAIL: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
|
||||
def test_location_format_handling():
|
||||
"""Test 3: Verify both location formats are handled correctly"""
|
||||
print("\n" + "="*60)
|
||||
print("Test 3: Location Format Handling (Dict & Tuple)")
|
||||
print("="*60)
|
||||
|
||||
try:
|
||||
# Test both location formats
|
||||
deepface_location = "{'x': 100, 'y': 150, 'w': 80, 'h': 90}"
|
||||
legacy_location = "(150, 180, 240, 100)"
|
||||
|
||||
# Parse DeepFace dict format
|
||||
import ast
|
||||
deepface_loc = ast.literal_eval(deepface_location)
|
||||
|
||||
if not isinstance(deepface_loc, dict):
|
||||
print(f"❌ FAIL: DeepFace location not parsed as dict")
|
||||
return False
|
||||
|
||||
if 'x' not in deepface_loc or 'y' not in deepface_loc or 'w' not in deepface_loc or 'h' not in deepface_loc:
|
||||
print(f"❌ FAIL: DeepFace location missing required keys")
|
||||
return False
|
||||
|
||||
print(f"✓ DeepFace format parsed correctly: {deepface_loc}")
|
||||
|
||||
# Legacy tuple format tests removed - only DeepFace format supported
|
||||
print(f"✓ DeepFace format is the only supported format")
|
||||
|
||||
print("\n✅ PASS: DeepFace location format handled correctly")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ FAIL: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
|
||||
def test_face_processor_configuration():
|
||||
"""Test 4: Verify FaceProcessor accepts DeepFace configuration"""
|
||||
print("\n" + "="*60)
|
||||
print("Test 4: FaceProcessor DeepFace Configuration")
|
||||
print("="*60)
|
||||
|
||||
try:
|
||||
# Create in-memory database
|
||||
db = DatabaseManager(":memory:", verbose=0)
|
||||
|
||||
# Create FaceProcessor with default config
|
||||
processor_default = FaceProcessor(db, verbose=0)
|
||||
|
||||
print(f"✓ Default detector: {processor_default.detector_backend}")
|
||||
print(f"✓ Default model: {processor_default.model_name}")
|
||||
|
||||
if processor_default.detector_backend != DEEPFACE_DETECTOR_BACKEND:
|
||||
print(f"❌ FAIL: Default detector mismatch")
|
||||
return False
|
||||
|
||||
if processor_default.model_name != DEEPFACE_MODEL_NAME:
|
||||
print(f"❌ FAIL: Default model mismatch")
|
||||
return False
|
||||
|
||||
# Create FaceProcessor with custom config
|
||||
processor_custom = FaceProcessor(db, verbose=0,
|
||||
detector_backend='mtcnn',
|
||||
model_name='Facenet512')
|
||||
|
||||
print(f"✓ Custom detector: {processor_custom.detector_backend}")
|
||||
print(f"✓ Custom model: {processor_custom.model_name}")
|
||||
|
||||
if processor_custom.detector_backend != 'mtcnn':
|
||||
print(f"❌ FAIL: Custom detector not applied")
|
||||
return False
|
||||
|
||||
if processor_custom.model_name != 'Facenet512':
|
||||
print(f"❌ FAIL: Custom model not applied")
|
||||
return False
|
||||
|
||||
print("\n✅ PASS: FaceProcessor correctly configured with DeepFace settings")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ FAIL: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
|
||||
def test_gui_panel_compatibility():
|
||||
"""Test 5: Verify GUI panels can unpack face data correctly"""
|
||||
print("\n" + "="*60)
|
||||
print("Test 5: GUI Panel Data Unpacking")
|
||||
print("="*60)
|
||||
|
||||
try:
|
||||
# Create in-memory database
|
||||
db = DatabaseManager(":memory:", verbose=0)
|
||||
|
||||
# Create test photo and face
|
||||
test_photo_path = "/tmp/test_photo.jpg"
|
||||
photo_id = db.add_photo(test_photo_path, "test_photo.jpg", None)
|
||||
|
||||
import numpy as np
|
||||
test_encoding = np.random.rand(512).astype(np.float64)
|
||||
test_location = "{'x': 100, 'y': 100, 'w': 50, 'h': 50}"
|
||||
|
||||
face_id = db.add_face(
|
||||
photo_id=photo_id,
|
||||
encoding=test_encoding.tobytes(),
|
||||
location=test_location,
|
||||
confidence=0.0,
|
||||
quality_score=0.85,
|
||||
person_id=None,
|
||||
detector_backend='retinaface',
|
||||
model_name='ArcFace',
|
||||
face_confidence=0.95
|
||||
)
|
||||
|
||||
# Simulate identify_panel query
|
||||
with db.get_db_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("""
|
||||
SELECT f.id, f.photo_id, p.path, p.filename, f.location,
|
||||
f.face_confidence, f.quality_score, f.detector_backend, f.model_name
|
||||
FROM faces f
|
||||
JOIN photos p ON f.photo_id = p.id
|
||||
WHERE f.person_id IS NULL
|
||||
""")
|
||||
|
||||
faces = cursor.fetchall()
|
||||
|
||||
if not faces:
|
||||
print("❌ FAIL: No faces retrieved")
|
||||
return False
|
||||
|
||||
# Simulate unpacking in identify_panel
|
||||
for face_tuple in faces:
|
||||
face_id, photo_id, photo_path, filename, location, face_conf, quality, detector, model = face_tuple
|
||||
|
||||
print(f"✓ Unpacked identify_panel data:")
|
||||
print(f" - Face ID: {face_id}")
|
||||
print(f" - Photo ID: {photo_id}")
|
||||
print(f" - Location: {location}")
|
||||
print(f" - Face Confidence: {face_conf}")
|
||||
print(f" - Quality: {quality}")
|
||||
print(f" - Detector/Model: {detector}/{model}")
|
||||
|
||||
# Simulate auto_match_panel query
|
||||
with db.get_db_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("""
|
||||
SELECT f.id, f.person_id, f.photo_id, f.location, p.filename, f.quality_score,
|
||||
f.face_confidence, f.detector_backend, f.model_name
|
||||
FROM faces f
|
||||
JOIN photos p ON f.photo_id = p.id
|
||||
""")
|
||||
|
||||
faces = cursor.fetchall()
|
||||
|
||||
# Simulate unpacking in auto_match_panel (uses tuple indexing)
|
||||
for face in faces:
|
||||
face_id = face[0]
|
||||
person_id = face[1]
|
||||
photo_id = face[2]
|
||||
location = face[3]
|
||||
filename = face[4]
|
||||
quality = face[5]
|
||||
face_conf = face[6]
|
||||
detector = face[7]
|
||||
model = face[8]
|
||||
|
||||
print(f"✓ Unpacked auto_match_panel data (tuple indexing):")
|
||||
print(f" - Face ID: {face_id}")
|
||||
print(f" - Quality: {quality}")
|
||||
print(f" - Face Confidence: {face_conf}")
|
||||
|
||||
# Simulate modify_panel query
|
||||
with db.get_db_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("""
|
||||
SELECT f.id, f.photo_id, p.path, p.filename, f.location,
|
||||
f.face_confidence, f.quality_score, f.detector_backend, f.model_name
|
||||
FROM faces f
|
||||
JOIN photos p ON f.photo_id = p.id
|
||||
""")
|
||||
|
||||
faces = cursor.fetchall()
|
||||
|
||||
# Simulate unpacking in modify_panel
|
||||
for face_tuple in faces:
|
||||
face_id, photo_id, photo_path, filename, location, face_conf, quality, detector, model = face_tuple
|
||||
|
||||
print(f"✓ Unpacked modify_panel data:")
|
||||
print(f" - Face ID: {face_id}")
|
||||
print(f" - Quality: {quality}")
|
||||
|
||||
print("\n✅ PASS: All GUI panels can correctly unpack face data")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ FAIL: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
"""Run all Phase 4 tests"""
|
||||
print("\n" + "="*70)
|
||||
print("PHASE 4 INTEGRATION TEST SUITE: GUI Updates for DeepFace")
|
||||
print("="*70)
|
||||
|
||||
tests = [
|
||||
("Database Schema", test_database_schema),
|
||||
("Face Data Retrieval", test_face_data_retrieval),
|
||||
("Location Format Handling", test_location_format_handling),
|
||||
("FaceProcessor Configuration", test_face_processor_configuration),
|
||||
("GUI Panel Compatibility", test_gui_panel_compatibility),
|
||||
]
|
||||
|
||||
results = []
|
||||
for test_name, test_func in tests:
|
||||
try:
|
||||
result = test_func()
|
||||
results.append((test_name, result))
|
||||
except Exception as e:
|
||||
print(f"\n❌ Test '{test_name}' crashed: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
results.append((test_name, False))
|
||||
|
||||
# Print summary
|
||||
print("\n" + "="*70)
|
||||
print("TEST SUMMARY")
|
||||
print("="*70)
|
||||
|
||||
passed = 0
|
||||
failed = 0
|
||||
for test_name, result in results:
|
||||
status = "✅ PASS" if result else "❌ FAIL"
|
||||
print(f"{status}: {test_name}")
|
||||
if result:
|
||||
passed += 1
|
||||
else:
|
||||
failed += 1
|
||||
|
||||
print("="*70)
|
||||
print(f"Tests passed: {passed}/{len(tests)}")
|
||||
print(f"Tests failed: {failed}/{len(tests)}")
|
||||
print("="*70)
|
||||
|
||||
if failed == 0:
|
||||
print("\n🎉 ALL TESTS PASSED! Phase 4 GUI integration is complete!")
|
||||
return 0
|
||||
else:
|
||||
print(f"\n⚠️ {failed} test(s) failed. Please review the errors above.")
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
|
||||
Reference in New Issue
Block a user