""" 检查并删除重复的照片 通过文件内容(MD5)或文件名来识别重复 """ import os import hashlib from pathlib import Path from collections import defaultdict import shutil def calculate_md5(file_path: Path) -> str: """计算文件的MD5值""" hash_md5 = hashlib.md5() with open(file_path, "rb") as f: for chunk in iter(lambda: f.read(4096), b""): hash_md5.update(chunk) return hash_md5.hexdigest() def find_duplicates_by_content(dataset_root: str, dry_run: bool = True): """ 通过文件内容(MD5)查找重复照片 Args: dataset_root: 数据集根目录 dry_run: 是否只是预览,不实际删除 """ dataset_root = Path(dataset_root) print("="*80) print("检查重复照片(基于文件内容MD5)") print("="*80) print(f"数据集根目录: {dataset_root}") print(f"模式: {'预览模式(不实际删除)' if dry_run else '执行模式'}") print("="*80 + "\n") # 存储所有文件的MD5值 md5_to_files = defaultdict(list) # 遍历所有图片文件 image_extensions = ['.jpg', '.jpeg', '.png', '.JPG', '.JPEG', '.PNG'] total_files = 0 print("正在计算文件MD5值...") for root, dirs, files in os.walk(dataset_root): for file in files: if any(file.endswith(ext) for ext in image_extensions): file_path = Path(root) / file try: md5_hash = calculate_md5(file_path) md5_to_files[md5_hash].append(file_path) total_files += 1 if total_files % 100 == 0: print(f" 已处理: {total_files} 个文件...") except Exception as e: print(f" ⚠ 处理文件失败 {file_path}: {e}") print(f"\n✓ 共处理 {total_files} 个文件\n") # 找出重复的文件 duplicates = {md5: files for md5, files in md5_to_files.items() if len(files) > 1} if not duplicates: print("✓ 没有发现重复的照片") return print(f"发现 {len(duplicates)} 组重复照片:\n") total_duplicates = 0 total_to_delete = 0 for md5_hash, files in sorted(duplicates.items(), key=lambda x: len(x[1]), reverse=True): # 按路径排序,保留第一个(通常保留train目录的) files_sorted = sorted(files) # 优先保留train目录的文件 keep_file = None for f in files_sorted: if 'train' in str(f): keep_file = f break if keep_file is None: keep_file = files_sorted[0] # 如果没有train目录的,保留第一个 files_to_delete = [f for f in files_sorted if f != keep_file] print(f"MD5: {md5_hash[:8]}...") print(f" 保留: {keep_file}") for f in files_to_delete: print(f" 删除: {f}") total_duplicates += len(files) total_to_delete += len(files_to_delete) # 实际删除 if not dry_run: for f in files_to_delete: try: f.unlink() print(f" ✓ 已删除: {f.name}") except Exception as e: print(f" ✗ 删除失败: {e}") print() print("="*80) print("统计:") print("="*80) print(f"重复文件组数: {len(duplicates)}") print(f"重复文件总数: {total_duplicates}") print(f"将删除文件数: {total_to_delete}") print(f"将保留文件数: {len(duplicates)}") if dry_run: print("\n使用 --execute 参数来实际执行删除操作") else: print("\n✓ 删除完成!") def find_duplicates_by_name(dataset_root: str, dry_run: bool = True): """ 通过文件名查找重复照片(同一文件名出现在多个目录) Args: dataset_root: 数据集根目录 dry_run: 是否只是预览,不实际删除 """ dataset_root = Path(dataset_root) print("="*80) print("检查重复照片(基于文件名)") print("="*80) print(f"数据集根目录: {dataset_root}") print(f"模式: {'预览模式(不实际删除)' if dry_run else '执行模式'}") print("="*80 + "\n") # 存储文件名到文件路径的映射 name_to_files = defaultdict(list) # 遍历所有图片文件 image_extensions = ['.jpg', '.jpeg', '.png', '.JPG', '.JPEG', '.PNG'] for root, dirs, files in os.walk(dataset_root): for file in files: if any(file.endswith(ext) for ext in image_extensions): file_path = Path(root) / file name_to_files[file].append(file_path) # 找出重复的文件名 duplicates = {name: files for name, files in name_to_files.items() if len(files) > 1} if not duplicates: print("✓ 没有发现同名重复的照片") return print(f"发现 {len(duplicates)} 组同名重复照片:\n") total_to_delete = 0 for file_name, files in sorted(duplicates.items(), key=lambda x: len(x[1]), reverse=True): # 按路径排序,优先保留train目录的 files_sorted = sorted(files) keep_file = None for f in files_sorted: if 'train' in str(f): keep_file = f break if keep_file is None: keep_file = files_sorted[0] files_to_delete = [f for f in files_sorted if f != keep_file] print(f"文件名: {file_name}") print(f" 保留: {keep_file}") for f in files_to_delete: print(f" 删除: {f}") total_to_delete += len(files_to_delete) # 实际删除 if not dry_run: for f in files_to_delete: try: f.unlink() print(f" ✓ 已删除: {f.name}") except Exception as e: print(f" ✗ 删除失败: {e}") print() print("="*80) print(f"将删除文件数: {total_to_delete}") if dry_run: print("\n使用 --execute 参数来实际执行删除操作") else: print("\n✓ 删除完成!") if __name__ == "__main__": import argparse parser = argparse.ArgumentParser(description='检查并删除重复的照片') parser.add_argument('--dataset', type=str, default='dataset/WholeIngredientRecognition', help='数据集根目录(默认: dataset/WholeIngredientRecognition)') parser.add_argument('--method', type=str, choices=['content', 'name', 'both'], default='both', help='检查方法: content(基于MD5), name(基于文件名), both(两种都检查)') parser.add_argument('--dry-run', action='store_true', help='预览模式,不实际删除') parser.add_argument('--execute', action='store_true', help='实际执行删除(需要明确指定)') args = parser.parse_args() # 获取项目根目录 script_dir = Path(__file__).parent project_root = script_dir.parent # 处理相对路径 if not os.path.isabs(args.dataset): if args.dataset.startswith('../'): dataset_path = (script_dir / args.dataset).resolve() else: dataset_path = (project_root / args.dataset).resolve() else: dataset_path = Path(args.dataset).resolve() # 如果没有指定execute,默认是dry-run if not args.execute and not args.dry_run: print("⚠ 警告: 默认是预览模式,不会实际删除") print("使用 --execute 参数来实际执行删除操作\n") args.dry_run = True if args.method in ['content', 'both']: find_duplicates_by_content(str(dataset_path), dry_run=args.dry_run) if args.method == 'both': print("\n" + "="*80 + "\n") if args.method in ['name', 'both']: find_duplicates_by_name(str(dataset_path), dry_run=args.dry_run)