1.基本算法
图像的局部hash算法包括aHash, pHash, dHash等,它们对图片的缩放、轻微的色彩变化、水印等不影响人眼判断的修改不敏感,非常适合用于查找相似图片。
可以尝试将这些算法分别进行测试,或联合使用,以应对各种复杂场景中的相似度计算。
1.1 aHash (Average Hash - 平均哈希)
- 核心思想: 比较每个像素与整张图的平均灰度值。
- 简化步骤:
- 缩小尺寸: 缩小至
8x8
。 - 转为灰度: 变为64个像素的灰度图。
- 计算平均值: 计算64个像素的平均灰度值。
- 生成哈希: 像素值大于等于平均值记为
1
,小于则记为0
。 - 得出指纹: 组合成64位哈希值。
- 缩小尺寸: 缩小至
- 优缺点:
- 优点: 速度极快,算法最简单。
- 缺点: 对整体亮度、对比度、伽马校正等非常敏感,鲁棒性最差。
1.2 dHash (Difference Hash - 差异哈希)
- 核心思想: 比较相邻像素之间的灰度差异,关注图片的梯度变化。
- 简化步骤:
- 缩小尺寸: 缩小至
9x8
。 - 转为灰度: 变为灰度图。
- 比较差异: 比较每一行中相邻的像素,左边比右边亮记为
1
,否则记为0
。 - 得出指纹: 得到
8x8=64
位的哈希值。
- 缩小尺寸: 缩小至
- 优缺点:
- 优点: 速度很快,对整体亮度和对比度不敏感,鲁棒性远超aHash。
- 缺点: 对几何变换(如旋转)等相对敏感。
1.3 pHash (Perceptual Hash - 感知哈希)
pHash 的方法要复杂得多,它不直接在像素上操作,而是工作在频域 (Frequency Domain),这使其能够捕捉到图片最核心的结构信息。
-
核心思想:
利用离散余弦变换 (DCT) 来获取图片的低频信息。低频信息代表了图片最大块的结构和轮廓,而高频信息则代表了细节。人类识别图片主要依赖于低频信息。 -
简化步骤:
- 缩小尺寸: 缩小至一个稍大的尺寸,例如
32x32
。 - 转为灰度: 转换为灰度图。
- 计算DCT: 对
32x32
的像素矩阵进行离散余弦变换,得到一个同样大小的DCT系数矩阵。 - 提取低频: 只保留结果矩阵左上角的
8x8
区域,这里包含了最重要的低频信息。 - 计算中位数: 计算这64个DCT系数的中位数(或平均值,中位数更鲁棒)。
- 生成哈希: 将64个DCT系数与中位数比较,大于等于中位数的记为
1
,小于则记为0
。 - 得出指纹: 组合成一个极为鲁棒的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 如何使用
- 保存代码:将上面的代码保存为
image_similarity.py
。 - 修改路径:在文件底部的
if __name__ == "__main__":
部分,找到IMAGE_PATH_1
和IMAGE_PATH_2
这两行。 - 填入你的图片地址:将
"path/to/your/first_image.jpg"
和"path/to/your/second_image.jpg"
替换成您电脑上两张图片的真实路径。 - 运行脚本:打开终端或命令提示符,导航到脚本所在的目录,然后运行:
python image_similarity.py
- 查看结果:程序将输出这两张图片在 aHash, dHash, 和 pHash 三种算法下的哈希值、汉明距离以及最终的相似度百分比。
注意: 如果您不修改路径,脚本会尝试在当前目录下自动创建两张相似的测试图片 (test_image_1.jpg
和 test_image_2.jpg
) 并进行比较,以便您能直接看到运行效果。