From 2828b9966bdabc8b6ba453428cc09883e5436204 Mon Sep 17 00:00:00 2001 From: tanyar09 Date: Fri, 17 Oct 2025 12:55:11 -0400 Subject: [PATCH] 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. --- src/core/config.py | 4 +- src/core/face_processing.py | 99 ++++++----- src/gui/gui_core.py | 18 +- src/gui/identify_panel.py | 300 ++++++++++++++++++++++----------- src/photo_tagger.py | 4 +- tests/test_deepface_gui.py | 12 +- tests/test_face_recognition.py | 12 +- tests/test_phase3_deepface.py | 19 +-- tests/test_phase4_gui.py | 37 +--- 9 files changed, 299 insertions(+), 206 deletions(-) diff --git a/src/core/config.py b/src/core/config.py index 641aaea..0bc69de 100644 --- a/src/core/config.py +++ b/src/core/config.py @@ -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 diff --git a/src/core/face_processing.py b/src/core/face_processing.py index 7591e45..6f2939e 100644 --- a/src/core/face_processing.py +++ b/src/core/face_processing.py @@ -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) diff --git a/src/gui/gui_core.py b/src/gui/gui_core.py index df8c854..a3abd97 100644 --- a/src/gui/gui_core.py +++ b/src/gui/gui_core.py @@ -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: diff --git a/src/gui/identify_panel.py b/src/gui/identify_panel.py index fa47d58..c29c9ca 100644 --- a/src/gui/identify_panel.py +++ b/src/gui/identify_panel.py @@ -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""" diff --git a/src/photo_tagger.py b/src/photo_tagger.py index 22fa6bf..a42770a 100644 --- a/src/photo_tagger.py +++ b/src/photo_tagger.py @@ -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) diff --git a/tests/test_deepface_gui.py b/tests/test_deepface_gui.py index f2e9770..0ae2257 100644 --- a/tests/test_deepface_gui.py +++ b/tests/test_deepface_gui.py @@ -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 = { diff --git a/tests/test_face_recognition.py b/tests/test_face_recognition.py index ac294e1..aeb7b43 100755 --- a/tests/test_face_recognition.py +++ b/tests/test_face_recognition.py @@ -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) diff --git a/tests/test_phase3_deepface.py b/tests/test_phase3_deepface.py index cacc97c..cf8435c 100755 --- a/tests/test_phase3_deepface.py +++ b/tests/test_phase3_deepface.py @@ -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: diff --git a/tests/test_phase4_gui.py b/tests/test_phase4_gui.py index 92fa46e..1558e03 100644 --- a/tests/test_phase4_gui.py +++ b/tests/test_phase4_gui.py @@ -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: