【图像相似度计算】hash哈希算法实现

1.基本算法

图像的局部hash算法包括aHash, pHash, dHash等,它们对图片的缩放、轻微的色彩变化、水印等不影响人眼判断的修改不敏感,非常适合用于查找相似图片。

可以尝试将这些算法分别进行测试,或联合使用,以应对各种复杂场景中的相似度计算。


1.1 aHash (Average Hash - 平均哈希)

  • 核心思想: 比较每个像素与整张图的平均灰度值
  • 简化步骤:
    1. 缩小尺寸: 缩小至8x8
    2. 转为灰度: 变为64个像素的灰度图。
    3. 计算平均值: 计算64个像素的平均灰度值。
    4. 生成哈希: 像素值大于等于平均值记为1,小于则记为0
    5. 得出指纹: 组合成64位哈希值。
  • 优缺点:
    • 优点: 速度极快,算法最简单。
    • 缺点: 对整体亮度、对比度、伽马校正等非常敏感,鲁棒性最差。

1.2 dHash (Difference Hash - 差异哈希)

  • 核心思想: 比较相邻像素之间的灰度差异,关注图片的梯度变化。
  • 简化步骤:
    1. 缩小尺寸: 缩小至9x8
    2. 转为灰度: 变为灰度图。
    3. 比较差异: 比较每一行中相邻的像素,左边比右边亮记为1,否则记为0
    4. 得出指纹: 得到 8x8=64 位的哈希值。
  • 优缺点:
    • 优点: 速度很快,对整体亮度和对比度不敏感,鲁棒性远超aHash。
    • 缺点: 对几何变换(如旋转)等相对敏感。

1.3 pHash (Perceptual Hash - 感知哈希)

pHash 的方法要复杂得多,它不直接在像素上操作,而是工作在频域 (Frequency Domain),这使其能够捕捉到图片最核心的结构信息。

  • 核心思想:
    利用离散余弦变换 (DCT) 来获取图片的低频信息。低频信息代表了图片最大块的结构和轮廓,而高频信息则代表了细节。人类识别图片主要依赖于低频信息。

  • 简化步骤:

    1. 缩小尺寸: 缩小至一个稍大的尺寸,例如 32x32
    2. 转为灰度: 转换为灰度图。
    3. 计算DCT:32x32的像素矩阵进行离散余弦变换,得到一个同样大小的DCT系数矩阵。
    4. 提取低频: 只保留结果矩阵左上角的 8x8 区域,这里包含了最重要的低频信息。
    5. 计算中位数: 计算这64个DCT系数的中位数(或平均值,中位数更鲁棒)。
    6. 生成哈希: 将64个DCT系数与中位数比较,大于等于中位数的记为1,小于则记为0
    7. 得出指纹: 组合成一个极为鲁棒的64位哈希值。
  • 优缺点:

    • 优点: 鲁棒性极高。它对缩放、旋转、轻微变形、水印、亮度、对比度、压缩JPEG伪影等都有非常强的抵抗力,准确率是三者中最高的。
    • 缺点: 计算相对复杂,速度比 aHash 和 dHash ,通常需要依赖专门的数学库(如NumPy/SciPy)来实现DCT。

核心对比

特性aHash (平均哈希)dHash (差异哈希)pHash (感知哈希)
核心思想像素与全局平均值比较相邻像素之间相互比较基于DCT的频域信息
速度最快很快较慢
鲁棒性较好极好
主要弱点对整体亮度和对比度敏感对某些几何变换敏感计算相对复杂,速度较慢
复杂度非常低

结论与如何选择

  • 追求极致速度,且图片源很统一: 选择 aHash。这在一些内部的、简单的查重任务中可能有用,但通常不被推荐。
  • 在速度和鲁棒性之间寻求最佳平衡: 选择 dHash。它对于绝大多数应用场景(如相册去重)都是一个绝佳的“甜点”选择,性价比最高。
  • 追求最高准确率和鲁棒性,不介意稍慢的速度: 选择 pHash。当你要处理来自互联网、经过各种压缩和编辑(甚至添加了水印)的图片时,pHash 是最可靠的选择。它是专业的图像搜索引擎和版权保护系统的首选基础算法。

2.Hash算法实现-借助成熟工具

2.1pHash

借助imageash实现,需要输入两个文件夹的地址,以及判断阈值。
安装相关库:

pip install ImageHash

代码实现如下:

import os
from PIL import Image
import imagehash
from tqdm import tqdm

def find_similar_images(folder_a: str, folder_b: str, similarity_threshold: float = 95.0, hash_size: int = 8):
    """
    在两个文件夹之间根据感知哈希值查找相似图片。

    Args:
        folder_a (str): 第一个图片文件夹的路径 (a文件夹)。
        folder_b (str): 第二个图片文件夹的路径 (b文件夹)。
        similarity_threshold (float): 相似度阈值 (0-100)。高于此值的图片被认为是相似的。
        hash_size (int): 感知哈希的精度,通常为 8 或 16。值越大越精确,但计算也越慢。
    """
    # 检查文件夹是否存在
    if not os.path.isdir(folder_a):
        print(f"错误:文件夹 '{folder_a}' 不存在。")
        return
    if not os.path.isdir(folder_b):
        print(f"错误:文件夹 '{folder_b}' 不存在。")
        return

    # --- 第1步:计算并存储b文件夹中所有图片的哈希值 ---
    print(f"正在计算文件夹 'b' ({folder_b}) 中图片的哈希值...")
    b_hashes = {}
    b_image_files = [f for f in os.listdir(folder_b) if f.lower().endswith(('png', 'jpg', 'jpeg', 'bmp', 'gif'))]
    
    if not b_image_files:
        print(f"警告:在文件夹 'b' ({folder_b}) 中没有找到支持的图片文件。")
        return

    for filename in tqdm(b_image_files, desc="处理文件夹 b"):
        try:
            path = os.path.join(folder_b, filename)
            with Image.open(path) as img:
                # 使用 pHash (感知哈希) 算法
                h = imagehash.phash(img, hash_size=hash_size)
                b_hashes[path] = h
        except Exception as e:
            print(f"\n无法处理文件 {filename}: {e}")

    # --- 第2步:遍历a文件夹,与b文件夹的哈希值进行比较 ---
    print(f"\n正在将文件夹 'a' ({folder_a}) 中的图片与文件夹 'b' 进行比较...")
    similar_pairs = []
    a_image_files = [f for f in os.listdir(folder_a) if f.lower().endswith(('png', 'jpg', 'jpeg', 'bmp', 'gif'))]

    if not a_image_files:
        print(f"警告:在文件夹 'a' ({folder_a}) 中没有找到支持的图片文件。")
        return
        
    # 计算汉明距离阈值
    # 汉明距离表示两个哈希值有多少位不同。距离越小,图片越相似。
    # 相似度 = (哈希总位数 - 汉明距离) / 哈希总位数
    # 所以,汉明距离 <= 哈希总位数 * (1 - 相似度阈值 / 100)
    max_hamming_distance = int(hash_size * hash_size * (1 - similarity_threshold / 100.0))
    print(f"相似度阈值: {similarity_threshold}% (对应的最大汉明距离为: {max_hamming_distance})")


    for filename_a in tqdm(a_image_files, desc="处理文件夹 a"):
        try:
            path_a = os.path.join(folder_a, filename_a)
            with Image.open(path_a) as img_a:
                hash_a = imagehash.phash(img_a, hash_size=hash_size)

                # 与b文件夹中的每个哈希值进行比较
                for path_b, hash_b in b_hashes.items():
                    # imagehash库重载了减法运算符,直接计算汉明距离
                    hamming_distance = hash_a - hash_b
                    
                    if hamming_distance <= max_hamming_distance:
                        # 计算实际的相似度百分比
                        similarity = (hash_size**2 - hamming_distance) / (hash_size**2) * 100
                        similar_pairs.append({
                            "image_a": path_a,
                            "image_b": path_b,
                            "similarity": f"{similarity:.2f}%",
                            "hamming_distance": hamming_distance
                        })
        except Exception as e:
            print(f"\n无法处理文件 {filename_a}: {e}")

    # --- 第3步:输出结果 ---
    if similar_pairs:
        print(f"\n--- 发现 {len(similar_pairs)} 对相似图片 ---")
        # 按相似度降序排序
        sorted_pairs = sorted(similar_pairs, key=lambda x: float(x['similarity'][:-1]), reverse=True)
        for pair in sorted_pairs:
            print(
                f"图片 A: {os.path.basename(pair['image_a'])}\n"
                f"图片 B: {os.path.basename(pair['image_b'])}\n"
                f"相似度: {pair['similarity']} (汉明距离: {pair['hamming_distance']})\n"
                f"---------------------------------"
            )
    else:
        print("\n--- 未发现相似的图片对 ---")


if __name__ == "__main__":
    # ==================== 用户配置 ====================
    
    # 1. 设置你的文件夹路径
    # 请确保路径正确,Windows用户可以使用 "C:\\Users\\YourUser\\Desktop\\a" 或 r"C:\Users\YourUser\Desktop\a"
    FOLDER_A_PATH = "a" 
    FOLDER_B_PATH = "b"

    # 2. 设置相似度阈值 (x)
    # 100.0 表示完全一样, 95.0 表示非常相似, 90.0 表示比较相似。
    # 建议从 95.0 开始尝试。
    SIMILARITY_X = 95.0

    # ================================================

    # 创建示例文件夹和图片(如果不存在)
    if not os.path.exists(FOLDER_A_PATH):
        print(f"创建示例文件夹 '{FOLDER_A_PATH}'")
        os.makedirs(FOLDER_A_PATH)
    if not os.path.exists(FOLDER_B_PATH):
        print(f"创建示例文件夹 '{FOLDER_B_PATH}'")
        os.makedirs(FOLDER_B_PATH)
        
    print("脚本开始运行。请确保您的 'a' 和 'b' 文件夹中已放入图片。")
    
    find_similar_images(FOLDER_A_PATH, FOLDER_B_PATH, similarity_threshold=SIMILARITY_X)

2.2 dHash与aHash

另外两种hash算法,同样借助相关库实现。

import os
import ImageHash
from PIL import Image

def compare_images_with_library(image_path1: str, image_path2: str):
    """
    使用 ImageHash 库中的 aHash 和 dHash 算法比较两张图片的相似度。
    
    Args:
        image_path1 (str): 第一张图片的路径。
        image_path2 (str): 第二张图片的路径。
    """
    if not os.path.exists(image_path1) or not os.path.exists(image_path2):
        print("错误:一个或两个图片路径不存在。")
        return

    try:
        # ImageHash 需要 Pillow 的 Image 对象作为输入
        img1 = Image.open(image_path1)
        img2 = Image.open(image_path2)
    except Exception as e:
        print(f"打开图片时出错: {e}")
        return

    hash_bits = 64  # ImageHash 默认生成64位哈希

    print(f"正在比较图片:\n1: {os.path.basename(image_path1)}\n2: {os.path.basename(image_path2)}\n")

    # --- aHash (平均哈希) ---
    # 使用 imagehash.average_hash() 计算哈希值
    hash1_a = ImageHash.average_hash(img1)
    hash2_a = ImageHash.average_hash(img2)
    
    # ImageHash 对象可以直接相减,结果就是汉明距离
    dist_a = hash1_a - hash2_a
    sim_a = (hash_bits - dist_a) / hash_bits * 100
    
    print("--- 平均哈希 (aHash) ---")
    # hash 对象可以直接打印,输出其十六进制表示
    print(f"哈希值1: {hash1_a}")
    print(f"哈希值2: {hash2_a}")
    print(f"汉明距离: {dist_a}")
    print(f"相似度: {sim_a:.2f}%\n")

    # --- dHash (差异哈希) ---
    # 使用 imagehash.dhash() 计算哈希值
    hash1_d = ImageHash.dhash(img1)
    hash2_d = ImageHash.dhash(img2)
    
    dist_d = hash1_d - hash2_d
    sim_d = (hash_bits - dist_d) / hash_bits * 100
    
    print("--- 差异哈希 (dHash) ---")
    print(f"哈希值1: {hash1_d}")
    print(f"哈希值2: {hash2_d}")
    print(f"汉明距离: {dist_d}")
    print(f"相似度: {sim_d:.2f}%\n")


# --- 主程序入口 ---
if __name__ == "__main__":
    # ==================== 用户配置 ====================
    # 请将下面的路径替换为您要比较的两张图片的实际路径
    # 在Windows上,路径可能看起来像 "C:\\Users\\YourUser\\Pictures\\image1.jpg"
    IMAGE_PATH_1 = "path/to/your/first_image.jpg"
    IMAGE_PATH_2 = "path/to/your/second_image.jpg"
    # ================================================
    
    # 示例: 创建两个相似的图片用于测试
    if not os.path.exists(IMAGE_PATH_1) or not os.path.exists(IMAGE_PATH_2):
        print("警告: 未找到指定的图片路径,将创建示例图片进行测试。")
        try:
            # 创建一个基础图片
            base_img = Image.new('RGB', (256, 256), color = 'blue')
            base_img.save("test_image_A.jpg")
            
            # 创建一个略有不同的图片(加了点文字)
            modified_img = base_img.copy()
            from PIL import ImageDraw
            draw = ImageDraw.Draw(modified_img)
            draw.text((10, 10), "Test with ImageHash", fill='white')
            modified_img.save("test_image_B.jpg")
            
            IMAGE_PATH_1 = "test_image_A.jpg"
            IMAGE_PATH_2 = "test_image_B.jpg"
            print("已创建 test_image_A.jpg 和 test_image_B.jpg 用于演示。\n")
        except Exception as e:
            print(f"创建示例图片失败,请手动设置图片路径。错误: {e}")

    compare_images_with_library(IMAGE_PATH_1, IMAGE_PATH_2)

3.Hash算法实现-原理展示

以下是ai生成的三种代码的具体实现,有可能会报错,仅供学习参考相关原理。

import os
from PIL import Image
import numpy as np
from scipy.fftpack import dct

def calculate_hamming_distance(hash1: int, hash2: int) -> int:
    """计算两个整数哈希值之间的汉明距离。"""
    # 使用异或(XOR)运算,不同位得1,相同位得0
    # 然后计算结果中1的个数即为汉明距离
    return bin(hash1 ^ hash2).count('1')

def calculate_ahash(image: Image.Image, hash_size: int = 8) -> int:
    """计算图像的平均哈希 (aHash) 值。"""
    # 1. 缩小尺寸并转为灰度
    img = image.resize((hash_size, hash_size), Image.LANCZOS).convert('L')
    pixels = list(img.getdata())
    # 2. 计算平均值
    avg = sum(pixels) / len(pixels)
    # 3. 比较像素灰度值,生成哈希
    bits = [1 if (px >= avg) else 0 for px in pixels]
    # 4. 将二进制位转换为一个整数
    hash_value = sum([2**i for i, bit in enumerate(bits) if bit])
    return hash_value

def calculate_dhash(image: Image.Image, hash_size: int = 8) -> int:
    """计算图像的差异哈希 (dHash) 值。"""
    # 1. 缩小尺寸并转为灰度
    img = image.resize((hash_size + 1, hash_size), Image.LANCZOS).convert('L')
    pixels = list(img.getdata())
    # 2. 比较相邻像素,生成哈希
    bits = []
    for row in range(hash_size):
        for col in range(hash_size):
            pixel_left = pixels[row * (hash_size + 1) + col]
            pixel_right = pixels[row * (hash_size + 1) + col + 1]
            bits.append(1 if pixel_left > pixel_right else 0)
    # 3. 将二进制位转换为一个整数
    hash_value = sum([2**i for i, bit in enumerate(bits) if bit])
    return hash_value

def calculate_phash(image: Image.Image, hash_size: int = 8, highfreq_factor: int = 4) -> int:
    """计算图像的感知哈希 (pHash) 值。"""
    # 1. 缩小尺寸并转为灰度
    img_size = hash_size * highfreq_factor
    img = image.resize((img_size, img_size), Image.LANCZOS).convert('L')
    pixels = np.array(img.getdata(), dtype=np.float64).reshape((img_size, img_size))
    # 2. 计算DCT
    dct_matrix = dct(dct(pixels, axis=0), axis=1)
    # 3. 保留左上角的低频部分
    dct_subset = dct_matrix[:hash_size, :hash_size]
    # 4. 计算DCT系数的中位数
    median = np.median(dct_subset)
    # 5. 生成哈希
    bits = (dct_subset >= median).flatten()
    # 6. 将二进制位转换为一个整数
    hash_value = sum([2**i for i, bit in enumerate(bits) if bit])
    return hash_value

def compare_two_images(image_path1: str, image_path2: str):
    """
    使用三种哈希算法比较两张图片的相似度。
    
    Args:
        image_path1 (str):第一张图片的路径。
        image_path2 (str): 第二张图片的路径。
    """
    if not os.path.exists(image_path1) or not os.path.exists(image_path2):
        print("错误:一个或两个图片路径不存在。")
        return

    try:
        # 打开图片
        img1 = Image.open(image_path1)
        img2 = Image.open(image_path2)
    except Exception as e:
        print(f"打开图片时出错: {e}")
        return

    hash_bits = 64  # 我们所有的哈希都是64位的

    print(f"正在比较图片:\n1: {os.path.basename(image_path1)}\n2: {os.path.basename(image_path2)}\n")

    # --- aHash ---
    hash1_a = calculate_ahash(img1)
    hash2_a = calculate_ahash(img2)
    dist_a = calculate_hamming_distance(hash1_a, hash2_a)
    sim_a = (hash_bits - dist_a) / hash_bits * 100
    print("--- 平均哈希 (aHash) ---")
    print(f"哈希值1: {hex(hash1_a)}")
    print(f"哈希值2: {hex(hash2_a)}")
    print(f"汉明距离: {dist_a}")
    print(f"相似度: {sim_a:.2f}%\n")

    # --- dHash ---
    hash1_d = calculate_dhash(img1)
    hash2_d = calculate_dhash(img2)
    dist_d = calculate_hamming_distance(hash1_d, hash2_d)
    sim_d = (hash_bits - dist_d) / hash_bits * 100
    print("--- 差异哈希 (dHash) ---")
    print(f"哈希值1: {hex(hash1_d)}")
    print(f"哈希值2: {hex(hash2_d)}")
    print(f"汉明距离: {dist_d}")
    print(f"相似度: {sim_d:.2f}%\n")

    # --- pHash ---
    hash1_p = calculate_phash(img1)
    hash2_p = calculate_phash(img2)
    dist_p = calculate_hamming_distance(hash1_p, hash2_p)
    sim_p = (hash_bits - dist_p) / hash_bits * 100
    print("--- 感知哈希 (pHash) ---")
    print(f"哈希值1: {hex(hash1_p)}")
    print(f"哈希值2: {hex(hash2_p)}")
    print(f"汉明距离: {dist_p}")
    print(f"相似度: {sim_p:.2f}%\n")

# --- 主程序入口 ---
if __name__ == "__main__":
    # ==================== 用户配置 ====================
    # 请将下面的路径替换为您要比较的两张图片的实际路径
    # 在Windows上,路径可能看起来像 "C:\\Users\\YourUser\\Pictures\\image1.jpg"
    IMAGE_PATH_1 = "path/to/your/first_image.jpg"
    IMAGE_PATH_2 = "path/to/your/second_image.jpg"
    # ================================================
    
    # 示例: 创建两个相似的图片用于测试
    if not os.path.exists(IMAGE_PATH_1) or not os.path.exists(IMAGE_PATH_2):
        print("警告: 未找到指定的图片路径,将创建示例图片进行测试。")
        try:
            # 创建一个基础图片
            base_img = Image.new('RGB', (256, 256), color = 'red')
            base_img.save("test_image_1.jpg")
            
            # 创建一个略有不同的图片(加了水印)
            modified_img = base_img.copy()
            from PIL import ImageDraw
            draw = ImageDraw.Draw(modified_img)
            draw.text((10, 10), "Slightly Modified", fill='white')
            modified_img.save("test_image_2.jpg")
            
            IMAGE_PATH_1 = "test_image_1.jpg"
            IMAGE_PATH_2 = "test_image_2.jpg"
            print("已创建 test_image_1.jpg 和 test_image_2.jpg 用于演示。\n")
        except Exception as e:
            print(f"创建示例图片失败,请手动设置图片路径。错误: {e}")

    compare_two_images(IMAGE_PATH_1, IMAGE_PATH_2)

3.3 如何使用

  1. 保存代码:将上面的代码保存为 image_similarity.py
  2. 修改路径:在文件底部的 if __name__ == "__main__": 部分,找到 IMAGE_PATH_1IMAGE_PATH_2 这两行。
  3. 填入你的图片地址:将 "path/to/your/first_image.jpg""path/to/your/second_image.jpg" 替换成您电脑上两张图片的真实路径。
  4. 运行脚本:打开终端或命令提示符,导航到脚本所在的目录,然后运行:
    python image_similarity.py
    
  5. 查看结果:程序将输出这两张图片在 aHash, dHash, 和 pHash 三种算法下的哈希值、汉明距离以及最终的相似度百分比。

注意: 如果您不修改路径,脚本会尝试在当前目录下自动创建两张相似的测试图片 (test_image_1.jpgtest_image_2.jpg) 并进行比较,以便您能直接看到运行效果。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值