# 用Python开发本地图片相似性检测器:感知哈希+汉明距离识别重复照片


背景介绍

随着手机摄影和图像编辑的普及,照片库中常常积累大量重复或相似图片(如原图、裁剪版、滤镜版、旋转版)。手动筛选耗时费力,因此开发一个自动化工具来识别相似图片十分必要。

本文将介绍如何用Python实现一个基于感知哈希(Perceptual Hash)汉明距离(Hamming Distance)的图片相似性检测器,帮助你快速整理照片库。

技术原理

感知哈希(Perceptual Hash)

感知哈希通过缩小尺寸、转灰度图、计算像素均值等步骤,生成能代表图片“视觉特征”的哈希串。相似图片的哈希串差异极小,对缩放、旋转、滤镜等编辑操作具有鲁棒性。

汉明距离

衡量两个二进制串的差异程度(不同位的数量)。距离越小,图片相似度越高。例如:哈希串10101011的汉明距离为1,表示高度相似。

实现步骤

  1. 文件系统遍历:递归扫描目标文件夹,筛选图片文件。
  2. 感知哈希生成:对每张图片 resize、转灰度、计算均值、生成二进制哈希。
  3. 相似度比较:计算两两哈希的汉明距离,判定是否相似。
  4. 相似分组:通过BFS找到所有连通的相似图片(解决“间接相似”的情况)。
  5. 结果输出:终端实时显示+生成报告文件。

代码实现(完整可运行)

import os
import argparse
from PIL import Image
import datetime

def scan_images(folder_path, exclude_folders=None):
    """递归扫描文件夹中的图片文件,支持常见格式,可排除指定子文件夹"""
    image_extensions = ('.jpg', '.jpeg', '.png', '.bmp', '.gif')
    image_paths = []
    exclude = [os.path.abspath(p) for p in exclude_folders] if exclude_folders else []
    for root, dirs, files in os.walk(folder_path):
        current_root = os.path.abspath(root)
        if current_root in exclude:
            continue  # 跳过排除的文件夹
        for file in files:
            if file.lower().endswith(image_extensions):
                image_paths.append(os.path.join(root, file))
    return image_paths

def generate_perceptual_hash(image_path, size=8):
    """生成图片的感知哈希(8×8灰度图版本)"""
    try:
        img = Image.open(image_path)
        # 转为灰度图并缩小尺寸(降低复杂度,保留视觉特征)
        img = img.convert('L').resize((size, size), Image.LANCZOS)
        pixels = list(img.getdata())
        avg = sum(pixels) / len(pixels)
        # 生成二进制哈希:像素>均值记为1,否则为0
        phash = ''.join(['1' if p > avg else '0' for p in pixels])
        return phash
    except Exception as e:
        print(f"警告:处理图片 {image_path} 时出错,跳过。错误:{e}")
        return None

def get_image_hashes(image_paths):
    """为所有图片生成感知哈希"""
    hashes = []
    for path in image_paths:
        phash = generate_perceptual_hash(path)
        hashes.append(phash)
    return hashes

def hamming_distance(hash1, hash2):
    """计算两个感知哈希的汉明距离(不同位的数量)"""
    if len(hash1) != len(hash2):
        return float('inf')  # 长度不同视为不相似
    # 异或运算后统计1的个数
    xor = int(hash1, 2) ^ int(hash2, 2)
    return bin(xor).count('1')

def group_similar_images(image_paths, hashes, threshold):
    """通过BFS分组相似图片(处理间接相似的连通性)"""
    groups = []
    visited = set()
    n = len(image_paths)
    for i in range(n):
        if i in visited:
            continue  # 已处理过的图片跳过
        current_group_paths = []
        current_group_hashes = []
        queue = [i]
        visited.add(i)
        while queue:
            current_idx = queue.pop(0)
            current_group_paths.append(image_paths[current_idx])
            current_group_hashes.append(hashes[current_idx])
            # 寻找所有与当前图片相似且未处理的图片
            for j in range(n):
                if j == current_idx or j in visited:
                    continue
                dist = hamming_distance(hashes[current_idx], hashes[j])
                if dist <= threshold:
                    visited.add(j)
                    queue.append(j)
        if len(current_group_paths) > 1:  # 只保留有相似图片的组
            groups.append((current_group_paths, current_group_hashes))
    return groups

def output_results(groups, target_folder, threshold, output_file=None):
    """输出相似图片分组结果到终端和报告文件"""
    timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
    report_filename = output_file or f"similar_images_report_{timestamp}.txt"
    with open(report_filename, 'w', encoding='utf-8') as f:
        f.write(f"# 图片相似性检测报告\n")
        f.write(f"生成时间:{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
        f.write(f"目标文件夹:{target_folder}\n")
        f.write(f"相似度阈值(汉明距离):{threshold}\n\n")
        f.write(f"共发现 {len(groups)} 组相似图片:\n\n")
        for idx, (group_paths, group_hashes) in enumerate(groups, 1):
            f.write(f"## 组 {idx}\n")
            first_hash = group_hashes[0]
            f.write(f"- 基准图片:{group_paths[0]}\n")
            for img_path, img_hash in zip(group_paths[1:], group_hashes[1:]):
                dist = hamming_distance(first_hash, img_hash)
                f.write(f"- 相似图片:{img_path} (与基准的汉明距离:{dist})\n")
            f.write("\n")
    # 终端输出
    print(f"\n# 图片相似性检测结果\n")
    print(f"目标文件夹:{target_folder}")
    print(f"相似度阈值:汉明距离 ≤ {threshold}")
    print(f"共发现 {len(groups)} 组相似图片:\n")
    for idx, (group_paths, group_hashes) in enumerate(groups, 1):
        print(f"## 组 {idx}")
        first_hash = group_hashes[0]
        print(f"- 基准图片:{group_paths[0]}")
        for img_path, img_hash in zip(group_paths[1:], group_hashes[1:]):
            dist = hamming_distance(first_hash, img_hash)
            print(f"- 相似图片:{img_path} (与基准的汉明距离:{dist})")
        print()
    print(f"\n详细报告已保存至:{report_filename}")

def main():
    parser = argparse.ArgumentParser(
        description='本地图片相似性检测器,通过感知哈希和汉明距离识别重复/相似图片'
    )
    parser.add_argument(
        '--path', 
        required=True, 
        help='目标文件夹路径(如:"D:\\MyPhotos" 或 "/Users/me/Photos")'
    )
    parser.add_argument(
        '--threshold', 
        type=int, 
        default=3, 
        help='相似度阈值(汉明距离≤此值视为相似,默认3)'
    )
    parser.add_argument(
        '--exclude', 
        nargs='*', 
        help='需要排除的子文件夹路径(多个路径用空格分隔,如:"--exclude D:\\MyPhotos\\screenshots")'
    )
    parser.add_argument(
        '--output', 
        help='报告输出文件名(可选,默认自动生成带时间戳的文件名)'
    )
    args = parser.parse_args()

    # 1. 扫描图片
    print(f"[进度] 正在扫描文件夹 {args.path}...")
    image_paths = scan_images(args.path, args.exclude)
    total = len(image_paths)
    print(f"[进度] 共找到 {total} 张图片(支持格式:.jpg, .jpeg, .png, .bmp, .gif)")

    # 2. 生成感知哈希
    print(f"[进度] 正在计算图片哈希(可能需要一段时间)...")
    hashes = get_image_hashes(image_paths)
    # 过滤无效哈希(处理失败的图片)
    valid_pairs = [(p, h) for p, h in zip(image_paths, hashes) if h is not None]
    valid_paths = [p for p, h in valid_pairs]
    valid_hashes = [h for p, h in valid_pairs]
    valid_count = len(valid_paths)
    print(f"[进度] 成功处理 {valid_count} 张图片的哈希({total - valid_count} 张处理失败)")

    # 3. 分组相似图片
    print(f"[进度] 正在分析相似性(阈值:{args.threshold})...")
    groups = group_similar_images(valid_paths, valid_hashes, args.threshold)
    print(f"[进度] 分析完成,共发现 {len(groups)} 组相似图片")

    # 4. 输出结果
    output_results(groups, args.path, args.threshold, args.output)

if __name__ == "__main__":
    main()

功能解析

1. 文件扫描(scan_images

  • 递归遍历目标文件夹,支持JPG、PNG等常见格式。
  • 通过--exclude参数排除指定子文件夹(需提供绝对路径)。

2. 感知哈希生成(generate_perceptual_hash

  • 步骤:resize(8×8) → 转灰度图 → 计算像素均值 → 生成二进制哈希。
  • 8×8尺寸确保哈希对缩放、裁剪、滤镜等编辑操作鲁棒。

3. 相似分组(group_similar_images

  • 采用BFS(广度优先搜索)处理“间接相似”:若A与B相似、B与C相似,则A、B、C自动归为一组。
  • 时间复杂度:O(n²)(n为图片数量),适合中小规模照片库(≤1000张)。

4. 结果输出(output_results

  • 终端实时显示每组相似图片及汉明距离。
  • 生成带时间戳的报告文件(如similar_images_report_20240901_1430.txt),方便存档。

功能扩展

1. 自定义阈值

通过--threshold调整相似判定的严格程度。例如:

# 放宽相似判定(汉明距离≤5视为相似)
python image_detector.py --path "D:\MyPhotos" --threshold 5

2. 缩略图预览(可选)

结合matplotlib显示相似图片的缩略图:

import matplotlib.pyplot as plt

def show_thumbnails(group_paths, max_cols=3):
    """显示相似图片的缩略图"""
    n = len(group_paths)
    rows = (n + max_cols - 1) // max_cols
    fig, axes = plt.subplots(rows, max_cols, figsize=(15, 5*rows))
    axes = axes.flatten() if rows > 1 else [axes]
    for i, path in enumerate(group_paths):
        img = Image.open(path)
        axes[i].imshow(img)
        axes[i].set_title(os.path.basename(path))
        axes[i].axis('off')
    for j in range(i+1, len(axes)):
        axes[j].axis('off')
    plt.tight_layout()
    plt.show()

# 在output_results中调用(示例)
# show_thumbnails(group_paths)

3. 性能优化

  • 对大规模图片库(>1000张),可引入局部敏感哈希(LSH)哈希索引,将比较复杂度从O(n²)降至O(n)。

使用示例

命令行执行:

# 扫描D:\MyPhotos,排除截图文件夹,阈值3
python image_detector.py --path "D:\MyPhotos" --threshold 3 --exclude "D:\MyPhotos\screenshots"

输出示例:

# 图片相似性检测结果

目标文件夹:D:\MyPhotos
相似度阈值:汉明距离 ≤ 3
共发现 3 组相似图片:

## 组 1
- 基准图片:D:\MyPhotos\vacation\beach.jpg
- 相似图片:D:\MyPhotos\vacation\beach_edited.png (与基准的汉明距离:2)
- 相似图片:D:\MyPhotos\vacation\beach_cropped.jpg (与基准的汉明距离:3)

## 组 2
- 基准图片:D:\MyPhotos\family\mom.jpg
- 相似图片:D:\MyPhotos\family\mom_smiling.jpg (与基准的汉明距离:1)

## 组 3
- 基准图片:D:\MyPhotos\food\pizza.jpg
- 相似图片:D:\MyPhotos\food\pizza_rotated.jpg (与基准的汉明距离:3)

详细报告已保存至:similar_images_report_20240901_1430.txt

总结

本文实现的图片相似性检测器,利用感知哈希+汉明距离高效识别重复/相似图片,代码结构清晰、易于扩展。适合中级Python开发者学习图像处理哈希算法文件操作等技能,同时解决照片整理的实际痛点。

若需处理超大规模照片库,可进一步优化哈希比较的性能(如LSH索引)或引入分布式计算。