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: