1.ImageHash 库:智能图像相似度比较利器

ImageHash 是一个非常实用的 Python 库,它专注于解决一个核心问题:判断两张图片的相似度

与传统的哈希算法(如 MD5、SHA-1)不同,ImageHash 实现的是**感知哈希(Perceptual Hashing)**算法。这两者有本质区别:

  • 传统哈希(Cryptographic Hash):对内容的任何微小改动(哪怕一个像素点)都会导致哈希值天翻地覆。它用于校验文件的完整性,确保文件未被篡改。
  • 感知哈希(Perceptual Hash):对图片内容的“感知”进行哈希。只要图片在视觉上是相似的,即使经过了缩放、裁剪、加水印、轻微的颜色变化等操作,它们的哈希值也会非常接近。它用于“查找相似图片”。

ImageHash 的核心思想是,为每张图片生成一个简短的“指纹”(哈希值),然后通过比较这些指纹的差异(汉明距离)来判断图片的相似度。距离越小,图片越相似。

2.主要哈希算法介绍

ImageHash 库提供了四种主要的感知哈希算法,每种算法都有其特点和适用场景:

  1. average_hash (aHash - 平均哈希)

    • 原理:将图片缩小到一个固定尺寸(如 8x8),转为灰度图,计算所有像素的平均灰度值。然后遍历所有像素,如果像素值大于平均值则记为1,反之记为0,最后得到一个64位的哈希值。
    • 特点:速度最快,但精度相对较低。对伽马校正或颜色直方图调整等操作敏感。
    • 适用场景:对速度要求高、图片差异比较明显的场景。
  2. phash (pHash - 感知哈希)

    • 原理:同样将图片缩小和灰度化,但它进行离散余弦变换(DCT)来提取图片的低频信息。这和 JPEG 压缩的原理类似,能更好地抓住图片的结构信息。
    • 特点:精度比 aHash 高,鲁棒性更好(对缩放、旋转、轻微变形不敏感),但速度稍慢。
    • 适用场景:大多数情况下推荐使用,是准确性和速度的良好平衡。
  3. dhash (dHash - 差异哈希)

    • 原理:将图片缩小后,不计算平均值,而是比较相邻像素之间的灰度差异。例如,如果左边像素比右边亮,则记为1,反之记为0。
    • 特点:速度比 pHash 快,比 aHash 准确。对于图片内容的渐变和边缘信息捕捉得更好。
    • 适用场景:性能和准确性都表现优异,是 aHash 的一个很好的替代品。
  4. whash (wHash - 小波哈希)

    • 原理:基于离散小波变换(DWT),它能从不同尺度上分析图片的特征。
    • 特点:通常具有最好的鲁棒性,尤其是在图片有水印或噪点时。算法更复杂,速度最慢。
    • 适用场景:对准确性要求极高,不惜牺牲计算速度的场景。

分析:

  • 没有银弹:没有哪一种算法是万能的。选择哪种算法取决于你的具体需求:是更看重速度还是准确性?你的图片集主要是内容相似还是包含大量细微变化?
  • pHash 和 dHash 是首选:在大多数应用中,phashdhash 提供了最佳的平衡。如果不确定,可以从 phash 开始尝试。
  • 哈希大小 (hash_size):所有算法都允许你指定 hash_size(默认为8),生成 hash_size * hash_size 位的哈希值。hash_size 越大,哈希值越长,对细节的区分度越高,但计算成本也越高,且对微小变化的容忍度会降低。hash_size=816 是常用选择。

3.核心用法与代码示例

3.1 安装

首先,你需要安装 ImageHash 和图像处理库 Pillow

pip install ImageHash Pillow

3.2 生成图像哈希值

这是最基本的操作。你需要先用 Pillow 打开图片,然后传给 ImageHash 的相应算法函数。

代码逻辑

  1. 导入 Image from PILimagehash
  2. 使用 Image.open() 打开图片文件。
  3. 调用 imagehash 中你选择的哈希函数(如 imagehash.phash())并传入图片对象。
  4. 打印生成的哈希值。
from PIL import Image
import imagehash

# 打开图片文件
try:
    img1 = Image.open('image1.jpg')
    img2 = Image.open('image2_resized.jpg') # 同一张图,但尺寸不同
except FileNotFoundError:
    print("错误:请确保 'image1.jpg' 和 'image2_resized.jpg' 文件存在于脚本目录中。")
    exit()

# 使用 pHash 算法计算哈希值
hash1 = imagehash.phash(img1)
hash2 = imagehash.phash(img2)

print(f"图片1的pHash值: {hash1}")
print(f"图片2的pHash值: {hash2}")

# 也可以尝试其他算法
# hash_ahash = imagehash.average_hash(img1)
# hash_dhash = imagehash.dhash(img1)
# hash_whash = imagehash.whash(img1)
# print(f"图片1的aHash值: {hash_ahash}")
# print(f"图片1的dHash值: {hash_dhash}")
# print(f"图片1的wHash值: {hash_whash}")

3.3 比较两个哈希值

得到哈希值后,它们之间的差异(汉明距离)可以直接通过减法运算得出。汉明距离代表两个等长字符串之间对应位置不同字符的个数。

代码逻辑

  1. 获取两个图像的哈希值对象。
  2. 将两个哈希值对象相减,得到一个整数。
  3. 这个整数就是它们的汉明距离。
# (接上文代码)

# 计算两个哈希值之间的汉明距离
# 结果是一个整数,代表有多少位(bit)不同
distance = hash1 - hash2

print(f"\n两张图片哈希值的汉明距离: {distance}")

# 根据距离判断相似度
# 阈值需要根据你的具体场景和所选算法来调整
# 对于 64 位的哈希 (hash_size=8),通常:
# 0: 完全相同
# 1-10: 非常相似
# >10: 可能不相似或差异较大
if distance <= 5:
    print("结论:这两张图片非常相似!")
elif distance <= 10:
    print("结论:这两张图片有些相似。")
else:
    print("结论:这两张图片不相似。")

分析:

  • 阈值(Threshold)是关键:如何定义“相似”完全取决于你设置的阈值。这个阈值没有固定标准,需要你根据自己的图片集和业务需求进行测试和调整。可以先拿一批已知相似和不相似的图片对,计算它们的距离分布,然后确定一个合理的阈值。
  • 对于默认 hash_size=8(64位哈希),pHash 的一个经验阈值可以是 56。如果距离小于这个值,就认为它们是同一张图片的不同版本。

3.4 实践案例:查找目录中的重复/相似图片

这是一个非常常见的应用场景。我们可以遍历一个文件夹中的所有图片,计算它们的哈希值,然后找出那些哈希值距离小于特定阈值的图片对。

代码逻辑

  1. 定义一个函数,用于遍历指定目录下的所有图片文件。
  2. 创建一个字典,用于存储每个图片的哈希值 ({filepath: hash_object})。
  3. 遍历所有图片,计算哈希并存入字典。
  4. 使用嵌套循环(或 itertools.combinations)比较字典中所有哈希值对的距离。
  5. 如果距离小于设定的阈值,则将这对图片记录为相似图片。
import os
from PIL import Image
import imagehash
from itertools import combinations

def find_similar_images(directory: str, hash_func=imagehash.phash, threshold: int = 5):
    """
    在指定目录中查找相似的图片。

    :param directory: 要扫描的文件夹路径。
    :param hash_func: 使用的哈希算法函数。
    :param threshold: 判断相似度的汉明距离阈值。
    """
    image_hashes = {}
    similar_images = []
    
    # 1. 计算目录下所有图片的哈希值
    print(f"正在扫描目录 '{directory}'...")
    for filename in os.listdir(directory):
        # 支持常见的图片格式
        if filename.lower().endswith(('.png', '.jpg', '.jpeg', '.bmp', '.gif')):
            filepath = os.path.join(directory, filename)
            try:
                img = Image.open(filepath)
                # RGBA 格式可能影响哈希计算,转换为 RGB
                if img.mode == 'RGBA':
                    img = img.convert('RGB')
                image_hashes[filepath] = hash_func(img)
            except Exception as e:
                print(f"处理文件 {filepath} 时出错: {e}")

    print(f"完成扫描,共找到 {len(image_hashes)} 张图片。正在比较...")

    # 2. 比较所有图片对的哈希值
    # 使用 combinations 避免重复比较 (A,B) 和 (B,A)
    image_filepaths = list(image_hashes.keys())
    for img1_path, img2_path in combinations(image_filepaths, 2):
        hash1 = image_hashes[img1_path]
        hash2 = image_hashes[img2_path]
        
        distance = hash1 - hash2
        
        if distance <= threshold:
            similar_images.append((img1_path, img2_path, distance))
            
    return similar_images

# --- 使用示例 ---
if __name__ == "__main__":
    # 创建一个名为 'my_images' 的测试目录,并放入一些相似和不同的图片
    # 例如: pic1.jpg, pic1_copy.jpg, pic1_small.jpg, different_pic.jpg
    
    # 假设图片都放在 'my_images' 文件夹下
    target_directory = 'my_images' 
    if not os.path.exists(target_directory):
        print(f"错误:请先创建 '{target_directory}' 目录并放入一些图片进行测试。")
    else:
        # 使用 pHash 算法,阈值为 5
        found_pairs = find_similar_images(directory=target_directory, hash_func=imagehash.phash, threshold=5)

        if found_pairs:
            print(f"\n找到了 {len(found_pairs)} 对相似图片:")
            for path1, path2, dist in found_pairs:
                print(f" - 图片 '{os.path.basename(path1)}' 和 '{os.path.basename(path2)}' 很相似 (距离: {dist})")
        else:
            print("\n没有找到相似的图片对。")

分析:

  • 性能考量:当图片数量巨大时(例如上万张),两两比较的计算量会非常大(O(n²))。对于这种情况,需要更高效的查找结构,例如 BK-TreeVP-Tree。这些数据结构可以快速检索出与给定哈希值汉明距离在一定范围内的所有其他哈希值,从而避免暴力比较。ImageHash 本身不提供这些,但可以结合其他库实现。
  • 预处理:代码中加入了 img.convert('RGB') 的步骤。这是一个好习惯,因为PNG等格式可能包含Alpha(透明度)通道,这会干扰哈希算法的计算,导致不准确的结果。统一转换为RGB可以提高一致性。

4.总结

ImageHash 是一个轻量级但功能强大的库,是处理图像去重、相似图片发现、建立图像索引等任务的绝佳工具。

  • 核心优势:简单易用、对视觉变化鲁棒、多种算法可选。
  • 使用要点
    1. 根据需求选择合适的哈希算法(通常从 phashdhash 开始)。
    2. 通过减法计算哈希距离来量化相似度。
    3. 根据实际数据测试并设定一个合理的距离阈值
  • 局限性
    • 对于大幅度的旋转(>10-15度)、翻转或非常复杂的非线性形变,感知哈希的鲁棒性会下降。
    • 它不理解图片内容。一张蓝天白云和一张蓝色大海的图片,可能因为颜色和纹理分布相似而导致哈希值接近。它解决的是“看起来像”,而不是“是什么”的问题。
Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐