本文最后更新于 2025-12-27,文章内容可能已经过时。

这是一个基于 Python 开发的图形界面应用程序,用于检测和删除文件夹中的重复图片文件。该工具使用感知哈希算法(Perceptual Hash)来识别视觉上相似或完全相同的图片,并提供一键删除重复项的功能。

功能特点

  • 图形用户界面:基于 tkinter 构建,操作简单直观

  • 智能检测:使用 imagehash 库的感知哈希算法检测重复图片

  • 批量处理:支持递归扫描整个文件夹及其子文件夹

  • 安全删除:每组重复图片中只保留一张,其余删除

  • 格式支持:支持 JPG、JPEG、PNG、GIF、BMP、TIFF、WebP 等常见图片格式

使用方法

  1. 运行程序:python 1.py

  2. 点击“浏览...”按钮选择包含图片的文件夹

  3. 点击“开始检测重复图片”按钮开始扫描

  4. 程序会显示检测到的重复图片组

  5. 确认后选择是否删除重复项

工作原理

  1. 程序遍历选定文件夹中的所有图片文件

  2. 为每张图片计算感知哈希值

  3. 将具有相同哈希值的图片归为一组(即重复图片)

  4. 每组中保留第一张图片,删除其余重复项

依赖库

  • Python 3.x

  • Pillow (PIL)

  • imagehash

  • tkinter (通常随 Python 一起安装)

安装依赖

pip install Pillow imagehash

注意事项

  • 删除操作不可逆,请在重要文件夹使用前备份

  • 程序根据感知哈希算法判断图片相似性,极少数情况下可能误判

  • 仅保留每组重复图片中的第一张,如需保留特定图片,请在处理前重命名

许可证

本项目为开源软件,遵循 MIT 许可证。

代码

GitHub链接:点我

import os
import tkinter as tk
from tkinter import filedialog, messagebox, ttk
from PIL import Image
import imagehash

class ImageDuplicateRemover:
    def __init__(self, root):
        self.root = root
        self.root.title("图片重复检测与删除工具")
        self.root.geometry("700x500")
        self.root.resizable(True, True)
        
        # 设置界面样式
        self.style = ttk.Style()
        self.style.configure("TButton", font=("微软雅黑", 10))
        self.style.configure("TLabel", font=("微软雅黑", 10))
        self.style.configure("Accent.TButton", font=("微软雅黑", 10, "bold"))
        
        # 文件夹路径变量
        self.folder_path = tk.StringVar()
        
        # 创建界面元素
        self.create_widgets()
        
    def create_widgets(self):
        # 顶部框架:选择文件夹
        top_frame = ttk.Frame(self.root, padding="10")
        top_frame.pack(fill=tk.X)
        
        ttk.Label(top_frame, text="选择图片文件夹:").pack(side=tk.LEFT, padx=5)
        ttk.Entry(top_frame, textvariable=self.folder_path, width=50).pack(side=tk.LEFT, padx=5, fill=tk.X, expand=True)
        ttk.Button(top_frame, text="浏览...", command=self.browse_folder).pack(side=tk.LEFT, padx=5)
        
        # 中间按钮:开始检测
        mid_frame = ttk.Frame(self.root, padding="10")
        mid_frame.pack()
        
        ttk.Button(mid_frame, text="开始检测重复图片", command=self.start_detection, style="Accent.TButton").pack()
        
        # 底部框架:结果显示
        bottom_frame = ttk.Frame(self.root, padding="10")
        bottom_frame.pack(fill=tk.BOTH, expand=True)
        
        ttk.Label(bottom_frame, text="处理结果:").pack(anchor=tk.W)
        
        # 滚动条和文本框
        scrollbar = ttk.Scrollbar(bottom_frame)
        scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
        
        self.result_text = tk.Text(bottom_frame, wrap=tk.WORD, yscrollcommand=scrollbar.set, height=15)
        self.result_text.pack(fill=tk.BOTH, expand=True)
        scrollbar.config(command=self.result_text.yview)
        
        # 禁止编辑文本框
        self.result_text.config(state=tk.DISABLED)
    
    def browse_folder(self):
        """打开文件夹选择对话框"""
        folder_selected = filedialog.askdirectory()
        if folder_selected:
            self.folder_path.set(folder_selected)
    
    def log(self, message):
        """在结果文本框中添加日志信息"""
        self.result_text.config(state=tk.NORMAL)
        self.result_text.insert(tk.END, message + "\n")
        self.result_text.see(tk.END)  # 滚动到最后一行
        self.result_text.config(state=tk.DISABLED)
        self.root.update_idletasks()  # 更新界面
    
    def get_image_hash(self, image_path):
        """计算图片的感知哈希值"""
        try:
            with Image.open(image_path) as img:
                # 转换为灰度图并计算哈希值
                return imagehash.phash(img)
        except Exception as e:
            self.log(f"无法处理图片 {os.path.basename(image_path)}: {str(e)}")
            return None
    
    def is_image_file(self, filename):
        """判断文件是否为图片"""
        image_extensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.tiff', '.webp']
        ext = os.path.splitext(filename)[1].lower()
        return ext in image_extensions
    
    def start_detection(self):
        """开始检测并删除重复图片"""
        folder = self.folder_path.get()
        if not folder or not os.path.isdir(folder):
            messagebox.showerror("错误", "请选择有效的文件夹")
            return
        
        # 清空之前的结果
        self.result_text.config(state=tk.NORMAL)
        self.result_text.delete(1.0, tk.END)
        self.result_text.config(state=tk.DISABLED)
        
        self.log(f"开始检测文件夹: {folder}")
        
        # 获取所有图片文件
        image_files = []
        for root_dir, _, files in os.walk(folder):
            for file in files:
                if self.is_image_file(file):
                    image_files.append(os.path.join(root_dir, file))
        
        self.log(f"找到 {len(image_files)} 个图片文件")
        
        if len(image_files) == 0:
            self.log("没有找到图片文件")
            return
        
        # 计算哈希值并分组重复图片
        hash_dict = {}
        for img_path in image_files:
            img_hash = self.get_image_hash(img_path)
            if img_hash:
                if img_hash not in hash_dict:
                    hash_dict[img_hash] = []
                hash_dict[img_hash].append(img_path)
        
        # 处理重复图片
        duplicate_groups = [group for group in hash_dict.values() if len(group) > 1]
        self.log(f"发现 {len(duplicate_groups)} 组重复图片")
        
        if not duplicate_groups:
            self.log("没有发现重复图片")
            return
        
        # 询问用户是否删除重复图片
        response = messagebox.askyesno("确认删除", 
                                      f"发现 {len(duplicate_groups)} 组重复图片,是否删除重复项?\n只保留每组中的一张图片")
        
        if not response:
            self.log("用户取消了删除操作")
            return
        
        # 删除重复图片
        deleted_count = 0
        for group in duplicate_groups:
            # 保留第一张,删除其余的
            keep_file = group[0]
            self.log(f"\n保留图片: {os.path.basename(keep_file)}")
            
            for file_to_delete in group[1:]:
                try:
                    os.remove(file_to_delete)
                    self.log(f"已删除重复图片: {os.path.basename(file_to_delete)}")
                    deleted_count += 1
                except Exception as e:
                    self.log(f"删除图片 {os.path.basename(file_to_delete)} 失败: {str(e)}")
        
        self.log(f"\n操作完成,共删除 {deleted_count} 张重复图片")
        messagebox.showinfo("完成", f"操作完成,共删除 {deleted_count} 张重复图片")

if __name__ == "__main__":
    root = tk.Tk()
    app = ImageDuplicateRemover(root)
    root.mainloop()