| """ |
| Face Deduplication Module |
| Find and manage duplicate faces in image collections |
| """ |
|
|
| import os |
| from pathlib import Path |
| import numpy as np |
| from collections import defaultdict |
| from face_recognition import FaceRecognition |
| import logging |
|
|
| logging.basicConfig(level=logging.INFO) |
| logger = logging.getLogger(__name__) |
|
|
| class FaceDeduplication: |
| """Face deduplication using similarity matching""" |
|
|
| def __init__(self, model_path='best_model.pth'): |
| """Initialize with face recognition model""" |
| self.face_rec = FaceRecognition(model_path) |
|
|
| def find_duplicates(self, folder_path, threshold=0.5): |
| """ |
| Find duplicate faces in a folder |
| |
| Args: |
| folder_path: Path to folder containing images |
| threshold: Similarity threshold for considering duplicates |
| |
| Returns: |
| List of duplicate groups |
| """ |
| |
| image_extensions = {'.jpg', '.jpeg', '.png', '.bmp'} |
| image_files = [] |
|
|
| folder_path = Path(folder_path) |
| for ext in image_extensions: |
| image_files.extend(folder_path.glob(f'**/*{ext}')) |
| image_files.extend(folder_path.glob(f'**/*{ext.upper()}')) |
|
|
| image_files = list(set(image_files)) |
| logger.info(f"Found {len(image_files)} images") |
|
|
| if len(image_files) < 2: |
| return [] |
|
|
| |
| embeddings = [] |
| valid_files = [] |
|
|
| for img_path in image_files: |
| try: |
| emb = self.face_rec.extract_embedding(str(img_path)) |
| if emb is not None: |
| embeddings.append(emb) |
| valid_files.append(str(img_path)) |
| except Exception as e: |
| logger.warning(f"Error processing {img_path}: {e}") |
|
|
| if len(embeddings) < 2: |
| return [] |
|
|
| |
| embeddings = np.array(embeddings) |
| similarity_matrix = np.dot(embeddings, embeddings.T) |
|
|
| |
| duplicate_groups = [] |
| processed = set() |
|
|
| for i in range(len(valid_files)): |
| if i in processed: |
| continue |
|
|
| |
| similar_indices = np.where(similarity_matrix[i] > threshold)[0] |
|
|
| if len(similar_indices) > 1: |
| group = [valid_files[idx] for idx in similar_indices] |
| duplicate_groups.append(group) |
| processed.update(similar_indices) |
|
|
| logger.info(f"Found {len(duplicate_groups)} duplicate groups") |
| return duplicate_groups |
|
|
| def remove_duplicates(self, folder_path, threshold=0.5, keep='first', dry_run=True): |
| """ |
| Remove duplicate faces from folder |
| |
| Args: |
| folder_path: Path to folder containing images |
| threshold: Similarity threshold |
| keep: Strategy ('first', 'last', 'best') |
| dry_run: If True, only show what would be removed |
| |
| Returns: |
| Dictionary with kept and removed files |
| """ |
| duplicate_groups = self.find_duplicates(folder_path, threshold) |
|
|
| kept_files = [] |
| removed_files = [] |
|
|
| for group in duplicate_groups: |
| if keep == 'first': |
| keep_file = group[0] |
| remove_files = group[1:] |
| elif keep == 'last': |
| keep_file = group[-1] |
| remove_files = group[:-1] |
| else: |
| sizes = [(f, os.path.getsize(f)) for f in group] |
| sizes.sort(key=lambda x: x[1], reverse=True) |
| keep_file = sizes[0][0] |
| remove_files = [f for f in group if f != keep_file] |
|
|
| kept_files.append(keep_file) |
| removed_files.extend(remove_files) |
|
|
| if not dry_run: |
| for file_path in removed_files: |
| try: |
| os.remove(file_path) |
| logger.info(f"Removed: {file_path}") |
| except Exception as e: |
| logger.error(f"Could not remove {file_path}: {e}") |
| else: |
| logger.info(f"DRY RUN - Would remove {len(removed_files)} files") |
|
|
| return { |
| 'kept': kept_files, |
| 'removed': removed_files, |
| 'dry_run': dry_run |
| } |
|
|
| |
| if __name__ == '__main__': |
| dedup = FaceDeduplication('best_model.pth') |
|
|
| |
| if os.path.exists('data'): |
| duplicates = dedup.find_duplicates('data', threshold=0.5) |
| for i, group in enumerate(duplicates, 1): |
| print(f"\nDuplicate group {i}:") |
| for img in group: |
| print(f" - {img}") |
|
|
| print("Face deduplication ready!") |