背景介绍
随着手机摄影和图像编辑的普及,照片库中常常积累大量重复或相似图片(如原图、裁剪版、滤镜版、旋转版)。手动筛选耗时费力,因此开发一个自动化工具来识别相似图片十分必要。
本文将介绍如何用Python实现一个基于感知哈希(Perceptual Hash)和汉明距离(Hamming Distance)的图片相似性检测器,帮助你快速整理照片库。
技术原理
感知哈希(Perceptual Hash)
感知哈希通过缩小尺寸、转灰度图、计算像素均值等步骤,生成能代表图片“视觉特征”的哈希串。相似图片的哈希串差异极小,对缩放、旋转、滤镜等编辑操作具有鲁棒性。
汉明距离
衡量两个二进制串的差异程度(不同位的数量)。距离越小,图片相似度越高。例如:哈希串1010和1011的汉明距离为1,表示高度相似。
实现步骤
- 文件系统遍历:递归扫描目标文件夹,筛选图片文件。
- 感知哈希生成:对每张图片 resize、转灰度、计算均值、生成二进制哈希。
- 相似度比较:计算两两哈希的汉明距离,判定是否相似。
- 相似分组:通过BFS找到所有连通的相似图片(解决“间接相似”的情况)。
- 结果输出:终端实时显示+生成报告文件。
代码实现(完整可运行)
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索引)或引入分布式计算。