怎样实现图片去重脚本

wen 实用脚本 29

从原理到实战,彻底告别重复图片

目录导读

  • 为什么需要图片去重?——解决存储与效率痛点
  • 图片去重的核心原理:感知哈希算法详解
  • 三种主流实现方式:Python脚本、现成工具、云端API
  • 实战:手写一个图片去重脚本(含完整代码)
  • Q&A:高频问题解答与避坑指南
  • 结语与扩展建议

为什么需要图片去重?

在日常工作中,你是否遇到过以下场景:

怎样实现图片去重脚本

  • 从手机、相机、微信等多个渠道备份照片,发现大量重复图片
  • 网站素材库中,同一张图片被不同设计师重复上传
  • 爬虫抓取数据时,页面包含相同图片的不同尺寸或水印版本
  • 硬盘里存了上百G的“新建文件夹(2)”和“微信图片_2019”

根据研究,普通用户手机中平均有15%-25%的图片是重复或高度相似的,对于企业和开发者,重复图片不仅浪费存储成本,还会导致检索效率下降、数据集训练出错等问题。

图片去重脚本的核心价值:通过自动化算法,快速识别内容相同或高度相似的图片,并支持删除、移动或标记重复项。


图片去重核心原理:感知哈希算法

想要实现图片去重,必须理解感知哈希(Perceptual Hashing,简称pHash),它不同于MD5这种基于文件二进制内容的哈希,而是基于图像视觉特征生成“指纹”。

工作流程(以最常用的pHash为例):

  1. 缩小尺寸:将图片统一缩放到8x8像素(64个像素点),用于消除不同尺寸、比例带来的差异
  2. 简化色彩:将图片转为灰度图,仅保留亮度信息(去色板差异)
  3. 计算像素平均值:算出这64个像素点的亮度平均值
  4. 生成哈希值:遍历每个像素,高于平均值的标记为1,低于平均值的标记为0,最终得到一个64位的二进制字符串(如“101010100101...”)
  5. 比较:计算两张图片哈希值的汉明距离(不同位数),距离越小,相似度越高;通常阈值设为5-10,大于该值的视为不同图片。

注意:这种方法可以识别经过缩放、旋转、轻微裁剪、添加水印、改变亮度/对比度的重复图片,但不适用于彻底改变构图(如翻页、换物体)的图片。

其他常用算法对比:

算法 速度 抗干扰性 适用场景
Average Hash (aHash) 最快 一般 无修改的完全重复图
Perceptual Hash (pHash) 适中 有缩放、水印、调色的相似图
Difference Hash (dHash) 中等 梯度变化明显的图片
深度学习嵌入 最强 复杂场景(如不同角度拍的同一物体)

对于大多数去重需求,pHash是最佳平衡点


三种主流实现方式

Python脚本(推荐)

适合人群:开发人员、有Python环境的管理员 优点:可控性强,可自定义阈值,支持批量处理 缺点:需基础编程能力

现成桌面工具

适合人群:普通用户 优点:无需代码,图形化界面,一键去重 缺点:功能固定,无法处理100万+级别的超大量图片

典型工具如:Duplicate Photo Finder(Windows/Mac)、Similar Images Finder(全平台)、Cisdem Duplicate Finder(Mac)。

云端API服务

适合人群:需要大规模、高精度去重的企业 优点:无服务器压力,算法先进 缺点:按量收费,受网络限制

AWS Rekognition、Google Cloud Vision、百度AI图像去重。


实战:手写一个图片去重脚本(Python)

下面是一个完整的去重脚本,结合了pHash实现,我已经在搜索引擎中综合了多篇优质教程,进行了去伪存真和优化。

环境准备

pip install pillow imagehash

完整代码

import os
import shutil
from PIL import Image
import imagehash
# 配置参数
SOURCE_DIR = "./images"       # 待处理图片文件夹
DEST_DIR = "./duplicates"     # 重复图片移动到此文件夹
THRESHOLD = 8                 # 汉明距离阈值(越小判断越严格)
def get_image_hashes(folder):
    """扫描文件夹,返回图片路径与哈希值的字典"""
    hash_dict = {}
    supported_formats = ('.jpg', '.jpeg', '.png', '.webp', '.bmp', '.tiff')
    for root, dirs, files in os.walk(folder):
        for file in files:
            if not file.lower().endswith(supported_formats):
                continue
            filepath = os.path.join(root, file)
            try:
                with Image.open(filepath) as img:
                    # 使用perceptual hash (pHash)
                    phash = imagehash.phash(img)
                    hash_dict[phash] = hash_dict.get(phash, []) + [filepath]
            except Exception as e:
                print(f"⚠️ 无法处理 {filepath}: {e}")
    return hash_dict
def find_duplicates(hash_dict):
    """通过哈希比较,找出重复组(汉明距离小于阈值的归为一组)"""
    duplicate_groups = []
    hashes_list = list(hash_dict.keys())
    for i in range(len(hashes_list)):
        for j in range(i+1, len(hashes_list)):
            distance = hashes_list[i] - hashes_list[j]  # 汉明距离
            if distance <= THRESHOLD:
                # 合并两个哈希对应的图片列表
                group = set(hash_dict[hashes_list[i]] + hash_dict[hashes_list[j]])
                duplicate_groups.append(group)
    return duplicate_groups
def move_duplicates(groups, dest_dir):
    """将每组中的多余图片移动到目标文件夹"""
    if not os.path.exists(dest_dir):
        os.makedirs(dest_dir)
    moved_count = 0
    for group in groups:
        group_list = list(group)
        # 保留第一张(原版),其余视为重复
        for filepath in group_list[1:]:
            try:
                dest_path = os.path.join(dest_dir, os.path.basename(filepath))
                shutil.move(filepath, dest_path)
                print(f"✅ 移动重复图片: {filepath} -> {dest_path}")
                moved_count += 1
            except Exception as e:
                print(f"❌ 移动失败 {filepath}: {e}")
    return moved_count
if __name__ == "__main__":
    print("🔍 正在计算所有图片的哈希值...")
    hash_dict = get_image_hashes(SOURCE_DIR)
    print(f"📊 共发现 {len(hash_dict)} 个唯一哈希")
    print("🔁 正在比对相似图片...")
    duplicate_groups = find_duplicates(hash_dict)
    print(f"🔗 发现 {len(duplicate_groups)} 组重复图片")
    if duplicate_groups:
        confirm = input("确认移动重复图片?(y/n): ")
        if confirm.lower() == 'y':
            moved = move_duplicates(duplicate_groups, DEST_DIR)
            print(f"✅ 完成!共移动了 {moved} 张重复图片到 {DEST_DIR}")
    else:
        print("✨ 未发现重复图片")

代码关键点说明

  • 遍历子文件夹:使用os.walk递归扫描,适合多层目录
  • 异常处理:遇到损坏图片不会导致程序崩溃
  • 汉明距离计算imagehash库已支持直接相减
  • 分批保留策略:每组中保留路径排在第一的图片,其他视为重复

Q&A:高频问题解答

Q1:这个脚本会误删图片吗?

A:不会,脚本只将重复图片移动到指定文件夹(DEST_DIR),而不是直接删除,你可以先检查该文件夹,再决定是否手动删除。建议第一次使用时将阈值设为5,减少误判。

Q2:为什么有些相似图片没有被识别?

A:可能原因:

  1. 阈值设置太小(比如设为5),可以尝试增加到10-15
  2. 图片经过了大幅度旋转、裁剪或添加了巨大水印,pHash无法处理
  3. 图片分辨率极端,很小(如32x32)或非常大(2K/4K)——可尝试先统一尺寸再哈希

Q3:如何处理视频帧或GIF动图?

A:本脚本不直接支持视频,对于GIF,可以提取关键帧,可以用ffmpeg将视频拆帧后,再用本脚本识别。

Q4:图片数量超过10万张时性能如何?

A:上面的O(n²)双循环在10万级别会非常慢,优化方案:

  1. 使用哈希桶:将相似哈希放入同一桶中,再在桶内比较
  2. 使用imagehashhex_to_hash快速比较
  3. 先用文件名/尺寸做第一轮过滤
  4. 改用深度学习特征向量配合Faiss向量检索库

Q5:能识别不同格式(如JPG和PNG)的相同图片吗?

A:可以,因为算法基于内容,而非文件格式,只要内容相同(比如同一张图分别保存为JPG和PNG),哈希值会非常接近。


结语与扩展建议

图片去重脚本是高效管理图片资源的利器,从本文中你学到了:

  1. 感知哈希原理——算法的核心
  2. 三种实现方式——按需选择
  3. 完整代码——可直接运行,或根据需要调整

进阶方向

  • 将脚本改为删除模式(慎用):直接删除重复,节省一步操作
  • 添加日志记录:记录每次去重的操作历史
  • 集成GUI界面:用Tkinter或PyQt让非技术用户也能使用
  • 分布式去重:针对海量图片,使用多线程/多进程加速

如果你需要针对特定场景(如电商商品图、医学影像)的去重方案,可以在算法层面换成深度学习的SimCLR对比学习模型,准确率会更高。

立即行动:拷贝上面的代码到你的Python环境中,修改SOURCE_DIR路径,运行试试看,你会发现,原来整理上万张图片可以在一杯咖啡的时间里完成。

抱歉,评论功能暂时关闭!