Python 实现 图片批量筛选去重整理工具
Python 实现 图片批量筛选去重整理工具
flyfish
batch_image_filter_deduplicator.py
功能
1. 全目录递归扫描
自动递归遍历源文件夹下所有层级子目录,识别全部 .jpg / .jpeg 格式图片(大小写兼容),无需提前手动整理文件夹结构。
2. 指定分辨率精准筛选
仅保留设定的三种标准分辨率图片:1280×720、1920×1080、2560×1440,其余分辨率图片自动过滤,处理结束后会单独统计过滤数量。
3. 图片内容级去重
基于感知哈希(pHash)算法实现纯内容去重,不受文件名、修改时间、存储路径影响,完全根据画面内容判断重复;
重复图片会完整记录原始路径并自动导出为日志文件,相同内容的图片仅保留1份进入后续归档流程。
4. 多进程并行加速
采用多进程架构并行执行图片读取、分辨率校验、哈希计算操作,充分利用CPU多核性能,适配万级以上图片的批量处理场景。
5. 自动分文件夹归档
拷贝时自动按每1000张为一组创建子文件夹,文件夹命名格式为 sisyphus_00000001 顺序递增,避免单目录文件过多导致的管理与打开卡顿问题。
6. 统一规范重命名
所有最终保留的图片按全局顺序统一重命名为8位数字格式(从 00000001.jpg 开始连续递增),序号全局唯一无重复。
7. 完整统计与异常兼容
- 处理结束输出全量对账统计:总图片数、分辨率不符数、损坏图片数、有效唯一图片数、重复图片数,数据可一一核对
- 自动跳过损坏、无法读取的异常图片,不会导致程序中途中断
- 自动生成
重复图片列表.txt日志文件,留存所有重复文件的原始完整路径,方便后续核查处理
多进程
| 类型 | 对应类 | 本质 | 适用场景 |
|---|---|---|---|
| 进程池 | ProcessPoolExecutor |
每个 worker 是独立的系统进程,拥有独立内存空间、独立的 Python 解释器实例 | CPU 密集型任务(哈希计算、图像处理、数值运算),可绕过 Python GIL 全局解释器锁,真正利用多核 CPU 并行计算 |
| 线程池 | ThreadPoolExecutor |
每个 worker 是同一进程内的线程,共享同一份内存空间 | IO 密集型任务(网络请求、批量文件读写),受 GIL 限制,无法实现真正的计算并行 |
内部任务分配与执行流程
进程池采用 全局任务队列 + 空闲 Worker 动态取任务 的调度模式,并非预先平均分配任务
1. 任务提交阶段
当执行 executor.submit(process_single_image, p) 时:
传入的函数和参数会被序列化(pickle 编码),放入进程池内部的全局任务队列中排队
此时任务仅完成提交,尚未开始执行
通过推导式一次性提交了全部图片的处理任务,所有任务会按提交顺序进入队列等待。
2. 并行执行阶段
进程池启动 max_workers 个子进程(比如你设置的 8 个):
每个子进程是独立的 worker,循环从全局任务队列中取下一个待执行任务
哪个 worker 先跑完当前任务、进入空闲状态,就自动领取下一个任务
不会预先给每个进程分配固定数量的图片,而是动态负载均衡,保证 CPU 持续满负载运行
同一时刻,最多有 max_workers 个任务在并行运行,剩余任务在队列中排队等待
3. 结果返回阶段
子进程执行完处理函数后:
将返回值序列化,通过进程间通信(IPC 管道)传回主进程
主进程中的 as_completed(...) 会按任务完成的先后顺序逐个返回结果(不是按提交顺序)
在这个循环中串行执行去重判断、计数统计等操作
import os
import shutil
from PIL import Image
import imagehash
from concurrent.futures import ProcessPoolExecutor, as_completed
# ===================== 【配置】 =====================
SOURCE_DIR = r"D:\data\original"
TARGET_DIR = r"D:\data\SisyphusPremium"
ALLOWED_SIZES = {(1280, 720), (1920, 1080), (2560, 1440)}
BATCH_SIZE = 1000
MAX_WORKERS = 8
# ====================================================
def process_single_image(img_path):
"""单张图片处理:返回状态标记 + 路径 + hash"""
try:
with Image.open(img_path) as img:
if img.size not in ALLOWED_SIZES:
return ("size_error", img_path, None)
img_hash = str(imagehash.phash(img))
return ("success", img_path, img_hash)
except:
return ("broken", img_path, None)
def get_all_image_paths(root_dir):
"""递归获取所有jpg路径"""
paths = []
for root, _, files in os.walk(root_dir):
for f in files:
if f.lower().endswith((".jpg", ".jpeg")):
paths.append(os.path.join(root, f))
return paths
def process_images():
print("开始扫描图片...")
all_images = get_all_image_paths(SOURCE_DIR)
total = len(all_images)
print(f"扫描完成,总计图片数:{total}")
print("开始校验分辨率与去重(多进程)...")
seen_hashes = set()
valid_images = []
duplicate_images = []
broken_count = 0
size_error_count = 0
with ProcessPoolExecutor(max_workers=MAX_WORKERS) as executor:
future_to_path = {executor.submit(process_single_image, p): p for p in all_images}
count = 0
for future in as_completed(future_to_path):
count += 1
# 每1000张打印一次进度,显示更连贯
if count % 1000 == 0:
print(f"已校验:{count}/{total}")
status, path, img_hash = future.result()
if status == "broken":
broken_count += 1
continue
if status == "size_error":
size_error_count += 1
continue
# 分辨率符合,进入去重逻辑
if img_hash not in seen_hashes:
seen_hashes.add(img_hash)
valid_images.append(path)
else:
duplicate_images.append(path)
# 完整统计输出
print(f"\n================ 校验结果统计 ================")
print(f"总图片数量:{total}")
print(f"分辨率不符合:{size_error_count}")
print(f"图片损坏/无法读取:{broken_count}")
print(f"符合分辨率的图片总数:{len(valid_images) + len(duplicate_images)}")
print(f"其中:唯一有效图片:{len(valid_images)}")
print(f"其中:重复图片:{len(duplicate_images)}")
print(f"==============================================")
# 保存重复图片列表
if duplicate_images:
os.makedirs(TARGET_DIR, exist_ok=True)
dup_log = os.path.join(TARGET_DIR, "重复图片列表.txt")
with open(dup_log, "w", encoding="utf-8") as f:
for p in duplicate_images:
f.write(p + "\n")
print(f"重复列表已保存到文件:{dup_log}")
if len(valid_images) == 0:
print("没有符合条件的有效图片,程序退出")
return
print("\n开始复制文件...")
os.makedirs(TARGET_DIR, exist_ok=True)
global_count = 1
for img_path in valid_images:
folder_num = (global_count - 1) // BATCH_SIZE + 1
batch_folder = f"sisyphus_{folder_num:08d}"
batch_path = os.path.join(TARGET_DIR, batch_folder)
os.makedirs(batch_path, exist_ok=True)
new_name = f"{global_count:08d}.jpg"
target_path = os.path.join(batch_path, new_name)
shutil.copy2(img_path, target_path)
if global_count % 1000 == 0:
print(f"已复制:{global_count}")
global_count += 1
print(f"\n处理完成,总复制:{global_count - 1} 张")
if __name__ == "__main__":
process_images()
更多推荐


所有评论(0)