refactor: Update face location handling to DeepFace format across the codebase

This commit refactors the handling of face location data to exclusively use the DeepFace format ({x, y, w, h}) instead of the legacy tuple format (top, right, bottom, left). Key changes include updating method signatures, modifying internal logic for face quality score calculations, and ensuring compatibility in the GUI components. Additionally, configuration settings for face detection have been adjusted to allow for smaller face sizes and lower confidence thresholds, enhancing the system's ability to detect faces in various conditions. All relevant tests have been updated to reflect these changes, ensuring continued functionality and performance.
This commit is contained in:
tanyar09 2025-10-17 12:55:11 -04:00
parent 68673ccdbe
commit 2828b9966b
9 changed files with 299 additions and 206 deletions

View File

@ -40,8 +40,8 @@ MIN_FACE_QUALITY = 0.3
DEFAULT_CONFIDENCE_THRESHOLD = 0.5
# Face detection filtering settings
MIN_FACE_CONFIDENCE = 0.7 # Minimum confidence from detector to accept face (increased from 0.4 to reduce false positives)
MIN_FACE_SIZE = 60 # Minimum face size in pixels (width or height) - increased to filter out small decorative objects
MIN_FACE_CONFIDENCE = 0.4 # Minimum confidence from detector to accept face (lowered to allow more low-quality faces)
MIN_FACE_SIZE = 40 # Minimum face size in pixels (width or height) - lowered to allow smaller faces
MAX_FACE_SIZE = 1500 # Maximum face size in pixels (to avoid full-image false positives)
# GUI settings

View File

@ -180,19 +180,18 @@ class FaceProcessor:
print(f" Face {i+1}: Filtered out (confidence: {face_confidence:.3f}, size: {location['w']}x{location['h']})")
continue
# Calculate face quality score
# Convert facial_area to (top, right, bottom, left) for quality calculation
face_location_tuple = (
facial_area.get('y', 0), # top
facial_area.get('x', 0) + facial_area.get('w', 0), # right
facial_area.get('y', 0) + facial_area.get('h', 0), # bottom
facial_area.get('x', 0) # left
)
# Calculate face quality score using DeepFace format directly
face_location_dict = {
'x': facial_area.get('x', 0),
'y': facial_area.get('y', 0),
'w': facial_area.get('w', 0),
'h': facial_area.get('h', 0)
}
# Load image for quality calculation
image = Image.open(photo_path)
image_np = np.array(image)
quality_score = self._calculate_face_quality_score(image_np, face_location_tuple)
quality_score = self._calculate_face_quality_score(image_np, face_location_dict)
# Store in database with DeepFace format
self.db.add_face(
@ -254,10 +253,15 @@ class FaceProcessor:
for face_id, location_str, face_confidence, quality_score, detector_backend, model_name in faces_to_check:
try:
# Parse location string back to dict
# Parse location string back to dict (DeepFace format only)
import ast
location = ast.literal_eval(location_str) if isinstance(location_str, str) else location_str
# Ensure we have DeepFace format
if not isinstance(location, dict):
print(f" ⚠️ Face {face_id} has non-dict location format, skipping")
continue
# Apply the same validation logic
if not self._is_valid_face_detection(face_confidence or 0.0, location):
# This face would be filtered out by current criteria, remove it
@ -308,8 +312,8 @@ class FaceProcessor:
# Additional filtering for very small faces with low confidence
# Small faces need higher confidence to be accepted
face_area = width * height
if face_area < 10000: # Less than 100x100 pixels
if face_confidence < 0.8: # Require 80% confidence for small faces
if face_area < 6400: # Less than 80x80 pixels (lowered from 100x100)
if face_confidence < 0.6: # Require 60% confidence for small faces (lowered from 80%)
return False
# Filter out faces that are too close to image edges (often false positives)
@ -317,7 +321,7 @@ class FaceProcessor:
y = location.get('y', 0)
# If face is very close to edges, require higher confidence
if x < 10 or y < 10: # Within 10 pixels of top/left edge
if face_confidence < 0.85: # Require 85% confidence for edge faces
if face_confidence < 0.65: # Require 65% confidence for edge faces (lowered from 85%)
return False
return True
@ -327,12 +331,21 @@ class FaceProcessor:
print(f"⚠️ Error validating face detection: {e}")
return True # Default to accepting on error
def _calculate_face_quality_score(self, image: np.ndarray, face_location: tuple) -> float:
def _calculate_face_quality_score(self, image: np.ndarray, face_location: dict) -> float:
"""Calculate face quality score based on multiple factors"""
try:
top, right, bottom, left = face_location
face_height = bottom - top
face_width = right - left
# DeepFace format: {x, y, w, h}
x = face_location.get('x', 0)
y = face_location.get('y', 0)
w = face_location.get('w', 0)
h = face_location.get('h', 0)
face_height = h
face_width = w
left = x
right = x + w
top = y
bottom = y + h
# Basic size check - faces too small get lower scores
min_face_size = 50
@ -393,7 +406,7 @@ class FaceProcessor:
print(f"⚠️ Error calculating face quality: {e}")
return 0.5 # Default medium quality on error
def _extract_face_crop(self, photo_path: str, location: tuple, face_id: int) -> str:
def _extract_face_crop(self, photo_path: str, location: dict, face_id: int) -> str:
"""Extract and save individual face crop for identification with caching"""
try:
# Check cache first
@ -407,23 +420,21 @@ class FaceProcessor:
# Remove from cache if file doesn't exist
del self._image_cache[cache_key]
# Parse location from string format and handle both DeepFace and legacy formats
# Parse location from string format (DeepFace format only)
if isinstance(location, str):
import ast
location = ast.literal_eval(location)
# Handle both DeepFace dict format and legacy tuple format
if isinstance(location, dict):
# DeepFace format: {x, y, w, h}
left = location.get('x', 0)
top = location.get('y', 0)
width = location.get('w', 0)
height = location.get('h', 0)
right = left + width
bottom = top + height
else:
# Legacy face_recognition format: (top, right, bottom, left)
top, right, bottom, left = location
# DeepFace format: {x, y, w, h}
if not isinstance(location, dict):
raise ValueError(f"Expected DeepFace dict format, got {type(location)}")
left = location.get('x', 0)
top = location.get('y', 0)
width = location.get('w', 0)
height = location.get('h', 0)
right = left + width
bottom = top + height
# Load the image
image = Image.open(photo_path)
@ -773,7 +784,7 @@ class FaceProcessor:
"""Update person encodings when a face is identified"""
self.db.update_person_encodings(person_id)
def _extract_face_crop(self, photo_path: str, location: tuple, face_id: int) -> str:
def _extract_face_crop(self, photo_path: str, location: dict, face_id: int) -> str:
"""Extract and save individual face crop for identification with caching"""
try:
# Check cache first
@ -787,23 +798,21 @@ class FaceProcessor:
# Remove from cache if file doesn't exist
del self._image_cache[cache_key]
# Parse location from string format and handle both DeepFace and legacy formats
# Parse location from string format (DeepFace format only)
if isinstance(location, str):
import ast
location = ast.literal_eval(location)
# Handle both DeepFace dict format and legacy tuple format
if isinstance(location, dict):
# DeepFace format: {x, y, w, h}
left = location.get('x', 0)
top = location.get('y', 0)
width = location.get('w', 0)
height = location.get('h', 0)
right = left + width
bottom = top + height
else:
# Legacy face_recognition format: (top, right, bottom, left)
top, right, bottom, left = location
# DeepFace format: {x, y, w, h}
if not isinstance(location, dict):
raise ValueError(f"Expected DeepFace dict format, got {type(location)}")
left = location.get('x', 0)
top = location.get('y', 0)
width = location.get('w', 0)
height = location.get('h', 0)
right = left + width
bottom = top + height
# Load the image
image = Image.open(photo_path)

View File

@ -194,15 +194,27 @@ class GUICore:
return badge_frame
def create_face_crop_image(self, photo_path: str, face_location: tuple,
def create_face_crop_image(self, photo_path: str, face_location: dict,
face_id: int, crop_size: int = 100) -> Optional[str]:
"""Create a face crop image for display"""
try:
# Parse location tuple from string format
# Parse location from string format (DeepFace format only)
if isinstance(face_location, str):
face_location = eval(face_location)
top, right, bottom, left = face_location
# DeepFace format: {x, y, w, h}
if not isinstance(face_location, dict):
raise ValueError(f"Expected DeepFace dict format, got {type(face_location)}")
x = face_location.get('x', 0)
y = face_location.get('y', 0)
w = face_location.get('w', 0)
h = face_location.get('h', 0)
left = x
top = y
right = x + w
bottom = y + h
# Load the image
with Image.open(photo_path) as image:

View File

@ -40,6 +40,10 @@ class IdentifyPanel:
self.identify_data_cache = {}
self.current_face_crop_path = None
# Caching system for all faces data
self.all_faces_cache = [] # Cache all faces from database
self.cache_loaded = False # Flag to track if cache is loaded
# GUI components
self.components = {}
self.main_frame = None
@ -248,16 +252,10 @@ class IdentifyPanel:
self.components['unique_var'].set(False)
return
else:
# Reload the original unfiltered face list
print("🔄 Reloading all faces...")
# Reload the original unfiltered face list from cache
print("🔄 Reloading all faces from cache...")
self.main_frame.update() # Update UI to show the message
# Get current date filters
date_from = self.components['date_from_var'].get().strip() or None
date_to = self.components['date_to_var'].get().strip() or None
date_processed_from = self.components['date_processed_from_var'].get().strip() or None
date_processed_to = self.components['date_processed_to_var'].get().strip() or None
# Get batch size
try:
batch_size = int(self.components['batch_var'].get().strip())
@ -272,12 +270,12 @@ class IdentifyPanel:
sort_display = self.components['sort_var'].get()
sort_by = self.sort_value_map.get(sort_display, "quality")
# Reload faces with current filters and sort option
self.current_faces = self._get_unidentified_faces(batch_size, date_from, date_to,
date_processed_from, date_processed_to,
min_quality_score, sort_by)
print(f"✅ Reloaded: {len(self.current_faces)} faces")
# Reload faces with current filters and sort option from cache
if self.cache_loaded:
self.current_faces = self._filter_cached_faces(min_quality_score, sort_by, batch_size)
print(f"✅ Reloaded: {len(self.current_faces)} faces from cache")
else:
print("⚠️ Cache not loaded - please click 'Start Identification' first")
# Reset to first face and update display
self.current_face_index = 0
@ -361,17 +359,11 @@ class IdentifyPanel:
# Add sort change handler
def on_sort_change(event=None):
"""Handle sort option change - refresh face list if identification is active"""
if self.is_active and self.current_faces:
if self.is_active and self.cache_loaded:
# Show progress message
print("🔄 Refreshing face list with new sort order...")
self.main_frame.update()
# Get current filters
date_from = self.components['date_from_var'].get().strip() or None
date_to = self.components['date_to_var'].get().strip() or None
date_processed_from = self.components['date_processed_from_var'].get().strip() or None
date_processed_to = self.components['date_processed_to_var'].get().strip() or None
# Get batch size
try:
batch_size = int(self.components['batch_var'].get().strip())
@ -386,10 +378,8 @@ class IdentifyPanel:
sort_display = self.components['sort_var'].get()
sort_by = self.sort_value_map.get(sort_display, "quality")
# Reload faces with new sort order
self.current_faces = self._get_unidentified_faces(batch_size, date_from, date_to,
date_processed_from, date_processed_to,
min_quality_score, sort_by)
# Apply new sort order to cached data
self.current_faces = self._filter_cached_faces(min_quality_score, sort_by, batch_size)
# Reset to first face and update display
self.current_face_index = 0
@ -557,7 +547,7 @@ class IdentifyPanel:
no_compare_label.pack(pady=20)
def _start_identification(self):
"""Start the identification process"""
"""Start the identification process - loads all faces into cache and applies initial filtering"""
try:
batch_size = int(self.components['batch_var'].get().strip())
if batch_size <= 0:
@ -580,13 +570,20 @@ class IdentifyPanel:
sort_display = self.components['sort_var'].get()
sort_by = self.sort_value_map.get(sort_display, "quality")
# Get unidentified faces with quality filter and sort option
self.current_faces = self._get_unidentified_faces(batch_size, date_from, date_to,
date_processed_from, date_processed_to,
min_quality_score, sort_by)
# Load all faces into cache (only database access point)
print("🔄 Loading faces from database...")
self._load_all_faces_cache(date_from, date_to, date_processed_from, date_processed_to)
if not self.all_faces_cache:
messagebox.showinfo("No Faces", "🎉 All faces have been identified!")
return
# Apply quality filtering and sorting to cached data
print("🔄 Applying quality filter and sorting...")
self.current_faces = self._filter_cached_faces(min_quality_score, sort_by, batch_size)
if not self.current_faces:
messagebox.showinfo("No Faces", "🎉 All faces have been identified!")
messagebox.showinfo("No Faces", f"No faces found with quality >= {min_quality}%.\nTry lowering the quality filter.")
return
# Pre-fetch data for optimal performance
@ -605,15 +602,16 @@ class IdentifyPanel:
self._update_button_states()
self.is_active = True
print(f"✅ Started identification with {len(self.current_faces)} faces (from {len(self.all_faces_cache)} total cached)")
def _get_unidentified_faces(self, batch_size: int, date_from: str = None, date_to: str = None,
date_processed_from: str = None, date_processed_to: str = None,
min_quality_score: float = 0.0, sort_by: str = "quality") -> List[Tuple]:
"""Get unidentified faces from database with optional date and quality filtering"""
"""Get unidentified faces from database with optional date filtering (no quality filtering at DB level)"""
with self.db.get_db_connection() as conn:
cursor = conn.cursor()
# Build the SQL query with optional date filtering
# Build the SQL query with optional date filtering only
# Include DeepFace metadata: face_confidence, quality_score, detector_backend, model_name
query = '''
SELECT f.id, f.photo_id, p.path, p.filename, f.location,
@ -624,11 +622,6 @@ class IdentifyPanel:
'''
params = []
# Add quality filtering if specified
if min_quality_score > 0.0:
query += ' AND f.quality_score >= ?'
params.append(min_quality_score)
# Add date taken filtering if specified
if date_from:
query += ' AND p.date_taken >= ?'
@ -673,6 +666,128 @@ class IdentifyPanel:
}
return sort_clauses.get(sort_by, "f.quality_score DESC") # Default to quality DESC
def _load_all_faces_cache(self, date_from: str = None, date_to: str = None,
date_processed_from: str = None, date_processed_to: str = None) -> None:
"""Load all unidentified faces into cache with date filtering only"""
with self.db.get_db_connection() as conn:
cursor = conn.cursor()
# Build the SQL query with optional date filtering only
query = '''
SELECT f.id, f.photo_id, p.path, p.filename, f.location,
f.face_confidence, f.quality_score, f.detector_backend, f.model_name,
p.date_taken, p.date_added
FROM faces f
JOIN photos p ON f.photo_id = p.id
WHERE f.person_id IS NULL
'''
params = []
# Add date taken filtering if specified
if date_from:
query += ' AND p.date_taken >= ?'
params.append(date_from)
if date_to:
query += ' AND p.date_taken <= ?'
params.append(date_to)
# Add date processed filtering if specified
if date_processed_from:
query += ' AND DATE(p.date_added) >= ?'
params.append(date_processed_from)
if date_processed_to:
query += ' AND DATE(p.date_added) <= ?'
params.append(date_processed_to)
# Order by quality DESC by default (can be re-sorted later)
query += ' ORDER BY f.quality_score DESC'
cursor.execute(query, params)
self.all_faces_cache = cursor.fetchall()
self.cache_loaded = True
if self.verbose > 0:
print(f"📦 Loaded {len(self.all_faces_cache)} faces into cache")
def _filter_cached_faces(self, min_quality_score: float = 0.0, sort_by: str = "quality",
batch_size: int = None) -> List[Tuple]:
"""Filter cached faces by quality and apply sorting"""
if not self.cache_loaded:
return []
# Filter by quality
filtered_faces = []
for face_data in self.all_faces_cache:
face_id, photo_id, photo_path, filename, location, face_conf, quality, detector, model, date_taken, date_added = face_data
quality_score = quality if quality is not None else 0.0
if quality_score >= min_quality_score:
# Return in the same format as the original method (without date_taken, date_added)
filtered_faces.append((face_id, photo_id, photo_path, filename, location, face_conf, quality, detector, model))
# Apply sorting
if sort_by == "quality":
filtered_faces.sort(key=lambda x: x[6] if x[6] is not None else 0.0, reverse=True)
elif sort_by == "quality_asc":
filtered_faces.sort(key=lambda x: x[6] if x[6] is not None else 0.0, reverse=False)
elif sort_by == "date_taken":
# Need to get date_taken from cache for sorting
filtered_faces_with_dates = []
for face_data in self.all_faces_cache:
face_id, photo_id, photo_path, filename, location, face_conf, quality, detector, model, date_taken, date_added = face_data
quality_score = quality if quality is not None else 0.0
if quality_score >= min_quality_score:
filtered_faces_with_dates.append((face_data, date_taken))
filtered_faces_with_dates.sort(key=lambda x: x[1] or "", reverse=True)
filtered_faces = [x[0][:9] for x in filtered_faces_with_dates] # Remove date fields
elif sort_by == "date_taken_asc":
filtered_faces_with_dates = []
for face_data in self.all_faces_cache:
face_id, photo_id, photo_path, filename, location, face_conf, quality, detector, model, date_taken, date_added = face_data
quality_score = quality if quality is not None else 0.0
if quality_score >= min_quality_score:
filtered_faces_with_dates.append((face_data, date_taken))
filtered_faces_with_dates.sort(key=lambda x: x[1] or "", reverse=False)
filtered_faces = [x[0][:9] for x in filtered_faces_with_dates]
elif sort_by == "date_added":
filtered_faces_with_dates = []
for face_data in self.all_faces_cache:
face_id, photo_id, photo_path, filename, location, face_conf, quality, detector, model, date_taken, date_added = face_data
quality_score = quality if quality is not None else 0.0
if quality_score >= min_quality_score:
filtered_faces_with_dates.append((face_data, date_added))
filtered_faces_with_dates.sort(key=lambda x: x[1] or "", reverse=True)
filtered_faces = [x[0][:9] for x in filtered_faces_with_dates]
elif sort_by == "date_added_asc":
filtered_faces_with_dates = []
for face_data in self.all_faces_cache:
face_id, photo_id, photo_path, filename, location, face_conf, quality, detector, model, date_taken, date_added = face_data
quality_score = quality if quality is not None else 0.0
if quality_score >= min_quality_score:
filtered_faces_with_dates.append((face_data, date_added))
filtered_faces_with_dates.sort(key=lambda x: x[1] or "", reverse=False)
filtered_faces = [x[0][:9] for x in filtered_faces_with_dates]
elif sort_by == "filename":
filtered_faces.sort(key=lambda x: x[3] or "", reverse=False)
elif sort_by == "filename_desc":
filtered_faces.sort(key=lambda x: x[3] or "", reverse=True)
elif sort_by == "confidence":
filtered_faces.sort(key=lambda x: x[5] if x[5] is not None else 0.0, reverse=True)
elif sort_by == "confidence_asc":
filtered_faces.sort(key=lambda x: x[5] if x[5] is not None else 0.0, reverse=False)
# Apply batch size limit if specified
if batch_size and batch_size > 0:
filtered_faces = filtered_faces[:batch_size]
return filtered_faces
def _prefetch_identify_data(self, faces: List[Tuple]) -> Dict:
"""Pre-fetch all needed data to avoid repeated database queries"""
cache = {
@ -1468,12 +1583,11 @@ class IdentifyPanel:
self._load_more_faces()
def _load_more_faces(self):
"""Load more faces if available"""
# Get current date filters
date_from = self.components['date_from_var'].get().strip() or None
date_to = self.components['date_to_var'].get().strip() or None
date_processed_from = self.components['date_processed_from_var'].get().strip() or None
date_processed_to = self.components['date_processed_to_var'].get().strip() or None
"""Load more faces from cache if available"""
if not self.cache_loaded:
messagebox.showinfo("Complete", "🎉 All faces have been identified!")
self._quit_identification()
return
# Get quality filter
min_quality = self.components['quality_filter_var'].get()
@ -1483,17 +1597,26 @@ class IdentifyPanel:
sort_display = self.components['sort_var'].get()
sort_by = self.sort_value_map.get(sort_display, "quality")
# Get more faces
more_faces = self._get_unidentified_faces(DEFAULT_BATCH_SIZE, date_from, date_to,
date_processed_from, date_processed_to,
min_quality_score, sort_by)
# Get batch size
try:
batch_size = int(self.components['batch_var'].get().strip())
except Exception:
batch_size = DEFAULT_BATCH_SIZE
if more_faces:
# Add to current faces
self.current_faces.extend(more_faces)
# Get more faces from cache (extend current batch)
current_batch_size = len(self.current_faces)
new_batch_size = current_batch_size + batch_size
more_faces = self._filter_cached_faces(min_quality_score, sort_by, new_batch_size)
if len(more_faces) > current_batch_size:
# Add new faces to current faces
new_faces = more_faces[current_batch_size:]
self.current_faces.extend(new_faces)
self.current_face_index += 1
self._update_current_face()
self._update_button_states()
print(f"✅ Loaded {len(new_faces)} more faces from cache")
else:
# No more faces
messagebox.showinfo("Complete", "🎉 All faces have been identified!")
@ -1636,6 +1759,10 @@ class IdentifyPanel:
if hasattr(self, 'similar_face_vars'):
self.similar_face_vars = []
# Clear cache
self.all_faces_cache = []
self.cache_loaded = False
# Clear right panel content
scrollable_frame = self.components['similar_scrollable_frame']
for widget in scrollable_frame.winfo_children():
@ -1655,59 +1782,35 @@ class IdentifyPanel:
self.components['face_canvas'].delete("all")
def _apply_date_filters(self):
"""Apply date and quality filters by jumping to next qualifying face"""
"""Apply quality and sort filters to cached data (no database access)"""
# Check if cache is loaded
if not self.cache_loaded:
messagebox.showinfo("Start Identification First",
"Please click 'Start Identification' to load faces before applying filters.")
return
# Get quality filter
min_quality = self.components['quality_filter_var'].get()
min_quality_score = min_quality / 100.0
# If we have faces loaded, find the next face that meets quality criteria
if self.current_faces:
# Start from current position and find next qualifying face
self._find_next_qualifying_face(min_quality)
else:
# No faces loaded, need to reload
date_from = self.components['date_from_var'].get().strip() or None
date_to = self.components['date_to_var'].get().strip() or None
date_processed_from = self.components['date_processed_from_var'].get().strip() or None
date_processed_to = self.components['date_processed_to_var'].get().strip() or None
# Get batch size
try:
batch_size = int(self.components['batch_var'].get().strip())
except Exception:
batch_size = DEFAULT_BATCH_SIZE
# Quality filter is already extracted above in min_quality
min_quality_score = min_quality / 100.0
# Get sort option
sort_display = self.components['sort_var'].get()
sort_by = self.sort_value_map.get(sort_display, "quality")
# Reload faces with new filters and sort option
self.current_faces = self._get_unidentified_faces(batch_size, date_from, date_to,
date_processed_from, date_processed_to,
min_quality_score, sort_by)
if not self.current_faces:
messagebox.showinfo("No Faces Found", "No unidentified faces found with the current filters.")
return
# Reset state
self.current_face_index = 0
self.face_status = {}
self.face_person_names = {}
self.face_selection_states = {}
# Pre-fetch data
self.identify_data_cache = self._prefetch_identify_data(self.current_faces)
# Find first qualifying face
self._find_next_qualifying_face(min_quality)
# Get sort option
sort_display = self.components['sort_var'].get()
sort_by = self.sort_value_map.get(sort_display, "quality")
self.is_active = True
# Get batch size
try:
batch_size = int(self.components['batch_var'].get().strip())
except Exception:
batch_size = DEFAULT_BATCH_SIZE
# Apply filtering to cached data
print("🔄 Applying filters to cached data...")
self.current_faces = self._filter_cached_faces(min_quality_score, sort_by, batch_size)
if not self.current_faces:
messagebox.showinfo("No Faces Found", "No unidentified faces found with the current date filters.")
messagebox.showinfo("No Faces Found",
f"No faces found with quality >= {min_quality}%.\n"
f"Try lowering the quality filter or click 'Start Identification' to reload from database.")
return
# Reset state
@ -1724,6 +1827,7 @@ class IdentifyPanel:
self._update_button_states()
self.is_active = True
print(f"✅ Applied filters: {len(self.current_faces)} faces (from {len(self.all_faces_cache)} total cached)")
def _find_next_qualifying_face(self, min_quality: int):
"""Find the next face that meets the quality criteria"""

View File

@ -88,7 +88,7 @@ class PhotoTagger:
"""Process unprocessed photos for faces with optional progress and cancellation"""
return self.face_processor.process_faces(limit, model, progress_callback, stop_event)
def _extract_face_crop(self, photo_path: str, location: tuple, face_id: int) -> str:
def _extract_face_crop(self, photo_path: str, location: dict, face_id: int) -> str:
"""Extract and save individual face crop for identification (legacy compatibility)"""
return self.face_processor._extract_face_crop(photo_path, location, face_id)
@ -96,7 +96,7 @@ class PhotoTagger:
"""Create a side-by-side comparison image (legacy compatibility)"""
return self.face_processor._create_comparison_image(unid_crop_path, match_crop_path, person_name, confidence)
def _calculate_face_quality_score(self, image, face_location: tuple) -> float:
def _calculate_face_quality_score(self, image, face_location: dict) -> float:
"""Calculate face quality score (legacy compatibility)"""
return self.face_processor._calculate_face_quality_score(image, face_location)

View File

@ -311,8 +311,16 @@ class FaceComparisonGUI:
for i, (face_location, face_encoding) in enumerate(zip(face_locations, face_encodings)):
try:
# face_recognition returns (top, right, bottom, left)
top, right, bottom, left = face_location
# DeepFace returns {x, y, w, h} format
if isinstance(face_location, dict):
x = face_location.get('x', 0)
y = face_location.get('y', 0)
w = face_location.get('w', 0)
h = face_location.get('h', 0)
top, right, bottom, left = y, x + w, y + h, x
else:
# Legacy format - should not be used
top, right, bottom, left = face_location
# Create face data with proper bounding box
face_data = {

View File

@ -83,12 +83,13 @@ class FaceRecognitionTester:
encodings = []
for i, (location, encoding) in enumerate(zip(face_locations, face_encodings)):
# Convert face_recognition format to DeepFace format
top, right, bottom, left = location
face_data = {
'image_path': image_path,
'face_id': f"fr_{Path(image_path).stem}_{i}",
'location': location,
'bbox': {'top': top, 'right': right, 'bottom': bottom, 'left': left},
'location': location, # Keep original for compatibility
'bbox': {'x': left, 'y': top, 'w': right - left, 'h': bottom - top}, # DeepFace format
'encoding': encoding
}
faces.append(face_data)
@ -238,9 +239,14 @@ class FaceRecognitionTester:
# Load original image
image = Image.open(face['image_path'])
# Extract face region
# Extract face region - use DeepFace format for both
if method == 'face_recognition':
# Convert face_recognition format to DeepFace format
top, right, bottom, left = face['location']
left = left
top = top
right = right
bottom = bottom
else: # deepface
bbox = face['bbox']
left = bbox.get('x', 0)

View File

@ -190,24 +190,9 @@ def test_location_format_handling():
print(f" ❌ Dict conversion incorrect")
return False
# Test tuple format (legacy)
location_tuple = (150, 300, 350, 100) # (top, right, bottom, left)
location_str_tuple = str(location_tuple)
# Legacy tuple format tests removed - only DeepFace format supported
parsed_tuple = ast.literal_eval(location_str_tuple)
if isinstance(parsed_tuple, tuple):
top, right, bottom, left = parsed_tuple
print(f" ✓ Tuple format parsed: {location_tuple}")
print(f" ✓ Values: top={top}, right={right}, bottom={bottom}, left={left}")
if (top == 150 and right == 300 and bottom == 350 and left == 100):
print(f" ✓ Tuple parsing correct")
else:
print(f" ❌ Tuple parsing incorrect")
return False
print(" ✅ Both location formats handled correctly")
print(" ✅ DeepFace location format handled correctly")
return True
except Exception as e:

View File

@ -188,41 +188,10 @@ def test_location_format_handling():
print(f"✓ DeepFace format parsed correctly: {deepface_loc}")
# Parse legacy tuple format
legacy_loc = ast.literal_eval(legacy_location)
# Legacy tuple format tests removed - only DeepFace format supported
print(f"✓ DeepFace format is the only supported format")
if not isinstance(legacy_loc, tuple):
print(f"❌ FAIL: Legacy location not parsed as tuple")
return False
if len(legacy_loc) != 4:
print(f"❌ FAIL: Legacy location should have 4 elements")
return False
print(f"✓ Legacy format parsed correctly: {legacy_loc}")
# Test conversion from dict to tuple (for quality calculation)
left = deepface_loc['x']
top = deepface_loc['y']
width = deepface_loc['w']
height = deepface_loc['h']
right = left + width
bottom = top + height
converted_tuple = (top, right, bottom, left)
print(f"✓ Converted dict to tuple: {converted_tuple}")
# Test conversion from tuple to dict
top, right, bottom, left = legacy_loc
converted_dict = {
'x': left,
'y': top,
'w': right - left,
'h': bottom - top
}
print(f"✓ Converted tuple to dict: {converted_dict}")
print("\n✅ PASS: Both location formats handled correctly")
print("\n✅ PASS: DeepFace location format handled correctly")
return True
except Exception as e: