Python计算图片相似度的几种算法:直方图、哈希与SSIM详解
一、核心算法定位与选型逻辑
评估图像相似度时,不同算法对图像各类畸变的敏感程度存在巨大差异。例如,直接逐像素做差(MSE/PSNR)对即使1像素的平移也会产生巨大误差;而简单对比尺寸或文件大小则无法反映内容差异。因此,合理的技术选型应先明确图像比对的核心诉求。
直方图算法主要量化像素强度的统计分布,适合评估色调、光影分布相近的图像检索;感知哈希(aHash/dHash/pHash)将图像抽象为低频二进制指纹,计算速度极快,是图库秒级查重与海量粗筛的首选;SSIM(Structural Similarity Index)则模拟人类视觉系统对亮度和结构轮廓的感知,适合图像压缩质量检测、超分辨率重建评估以及精密相似度校验。
二、基于颜色直方图的相似度计算
颜色直方图(Color Histogram)通过统计图像中特定颜色或亮度区间出现的频次,将二维图像映射为一维或多维统计向量。在比对时,算法仅计算两组向量之间的数学距离,完全脱离了像素在图像中的具体坐标。
如果直接在 RGB 空间计算直方图,光照强弱会同时改变 R、G、B 三个通道的取值。因此,工业应用中通常先将图片转换至 HSV 空间(色调 H、饱和度 S、明度 V)。在 HSV 空间中,色调(Hue)与饱和度(Saturation)代表了图像的本征色彩,丢弃或降低明度(Value)通道的权重可以大幅减轻光照明暗变化带来的干扰。
1. OpenCV 比对实现代码
在 Python 中使用 cv2.calcHist 与 cv2.compareHist 计算直方图相似度的完整实现如下:
import cv2
def compare_image_histograms(img_path1: str, img_path2: str):
"""
基于 HSV 颜色直方图计算两张图片的相似度
"""
# 1. 加载图像文件
img1 = cv2.imread(img_path1)
img2 = cv2.imread(img_path2)
if img1 is None or img2 is None:
raise FileNotFoundError("无法读取指定图像,请确认路径有效。")
# 2. 转换色彩空间至 HSV
hsv1 = cv2.cvtColor(img1, cv2.COLOR_BGR2HSV)
hsv2 = cv2.cvtColor(img2, cv2.COLOR_BGR2HSV)
# 3. 统计 H-S 二维直方图 (H通道: 50 bins, S通道: 60 bins)
hist1 = cv2.calcHist([hsv1], [0, 1], None, [50, 60], [0, 180, 0, 256])
hist2 = cv2.calcHist([hsv2], [0, 1], None, [50, 60], [0, 180, 0, 256])
# 4. 归一化至 [0, 1] 范围
cv2.normalize(hist1, hist1, alpha=0, beta=1, norm_type=cv2.NORM_MINMAX)
cv2.normalize(hist2, hist2, alpha=0, beta=1, norm_type=cv2.NORM_MINMAX)
# 5. 计算多种常用比对指标
correl = cv2.compareHist(hist1, hist2, cv2.HISTCMP_CORREL)
bhattacharyya = cv2.compareHist(hist1, hist2, cv2.HISTCMP_BHATTACHARYYA)
chisqr = cv2.compareHist(hist1, hist2, cv2.HISTCMP_CHISQR)
intersect = cv2.compareHist(hist1, hist2, cv2.HISTCMP_INTERSECT)
return {
"correlation": correl, # 越大越相似 [ -1 ~ 1 ]
"bhattacharyya": bhattacharyya, # 越小越相似 [ 0 ~ 1 ]
"chi_square": chisqr, # 越小越相似 [ 0 ~ inf )
"intersection": intersect # 越大越相似
}
# 示例调用
# results = compare_image_histograms("photo_a.jpg", "photo_b.jpg")
# print(f"巴氏距离: {results['bhattacharyya']:.4f}, 相关性: {results['correlation']:.4f}")
三、基于图像感知哈希的快速指纹对比
感知哈希(Perceptual Hashing)的核心思想是将图像进行极度降采样与信息过滤,提取具有代表性的低频骨架特征,并将其编码为一个固定长度(常用 64 位,即 8 字节)的二进制哈希指纹。
比对两张图片时,只需计算两个 64 位二进制指纹之间的汉明距离(Hamming Distance)——即两个哈希值在对应比特位上不同数值的个数。现代 CPU 可以在单个周期内通过异或(XOR)与 POPCNT 指令完成 64 位汉明距离计算,每秒可完成上亿次比对,因此是工业级海量图像检索的核心基石。
常见的图像哈希算法主要有以下三种变体:
- 均值哈希(aHash, Average Hash):缩放为 8×8 灰度图,计算像素均值,将大于均值的位设为 1,其余设为 0。运算最简单,但抵抗噪声与微小旋转的能力较弱。
- 差异哈希(dHash, Difference Hash):缩放为 9×8 灰度图,每行比较相邻两像素的强度差值(左像素大于右像素置 1,反之置 0)。保留了边缘梯度方向,计算极快且对轻度拉伸、对比度微调非常稳健。
- 感知哈希(pHash, Perceptual Hash):缩放至 32×32 并执行二维离散余弦变换(DCT),仅保留左上角 8×8 的低频能量分量,再通过低频中位数或均值二值化。抗压缩、抗伽马调整能力最强,但 DCT 变换稍有计算开销。
2. 图像哈希完整 Python 实现
以下代码基于 OpenCV 与 NumPy 原生实现了三种哈希函数及汉明距离比对,避免引入庞大的第三方依赖:
import cv2
import numpy as np
def ahash(img: np.ndarray, hash_size: int = 8) -> np.ndarray:
"""均值哈希 (Average Hash)"""
resized = cv2.resize(img, (hash_size, hash_size), interpolation=cv2.INTER_AREA)
gray = cv2.cvtColor(resized, cv2.COLOR_BGR2GRAY)
avg = gray.mean()
return (gray > avg).flatten()
def dhash(img: np.ndarray, hash_size: int = 8) -> np.ndarray:
"""差异哈希 (Difference Hash)"""
# 宽度额外扩展1像素,以便每行计算8次差值
resized = cv2.resize(img, (hash_size + 1, hash_size), interpolation=cv2.INTER_AREA)
gray = cv2.cvtColor(resized, cv2.COLOR_BGR2GRAY)
# 比较水平相邻像素:左边大于右边则为 True
diff = gray[:, 1:] > gray[:, :-1]
return diff.flatten()
def phash(img: np.ndarray, hash_size: int = 8, highfreq_factor: int = 4) -> np.ndarray:
"""感知哈希 (Perceptual Hash - DCT)"""
img_size = hash_size * highfreq_factor # 通常缩放为 32x32
resized = cv2.resize(img, (img_size, img_size), interpolation=cv2.INTER_AREA)
gray = cv2.cvtColor(resized, cv2.COLOR_BGR2GRAY)
# 浮点型二维离散余弦变换
dct = cv2.dct(np.float32(gray))
# 截取左上角代表主要低频能量的 8x8 区域
dct_lowfreq = dct[:hash_size, :hash_size]
# 排除第一项直流分量 dct[0, 0] 计算平均值
avg = (dct_lowfreq.sum() - dct_lowfreq[0, 0]) / (hash_size * hash_size - 1)
return (dct_lowfreq > avg).flatten()
def hamming_distance(hash1: np.ndarray, hash2: np.ndarray) -> int:
"""计算两个布尔型哈希数组的汉明距离"""
return int(np.count_nonzero(hash1 != hash2))
def hash_similarity_ratio(hash1: np.ndarray, hash2: np.ndarray) -> float:
"""将汉明距离映射为 0.0 ~ 1.0 的相似度比例"""
distance = hamming_distance(hash1, hash2)
total_bits = len(hash1)
return max(0.0, 1.0 - (distance / total_bits))
# 示例执行代码
# img_a = cv2.imread("sample1.jpg")
# img_b = cv2.imread("sample2.jpg")
# dist_dhash = hamming_distance(dhash(img_a), dhash(img_b))
# print(f"dHash 汉明距离: {dist_dhash}, 归一化相似度: {1.0 - dist_dhash/64:.2%}")
四、基于结构相似性(SSIM)的高精度质量与相似度评估
与直方图的无序统计以及哈希的粗粒度压缩不同,SSIM(Structural Similarity Index Measure)严格保留了二维图像的几何拓扑关系。SSIM 的理论基础是:人类视觉系统(HVS)在观察物体时,能够高度自适应地剥离明暗强弱与色彩漂移,重点提取场景中的物体结构信息(边缘、纹理、轮廓交互)。
对于参考图像窗口 $x$ 与失真图像窗口 $y$,SSIM 将视觉对比拆解为三个独立的无量纲乘子:
- 亮度对比项 $l(x, y)$:以局部窗口的均值 $\mu_x$ 和 $\mu_y$ 评估平均亮度差异。
- 对比度对比项 $c(x, y)$:以局部窗口的标准差 $\sigma_x$ 和 $\sigma_y$ 评估纹理反差与信号动态范围差异。
- 结构对比项 $s(x, y)$:以局部窗口的互协方差 $\sigma_{xy}$ 评估去除亮度和对比度后的几何结构相关性。
3. scikit-image 与 OpenCV 实现代码
在实际生产环境中,通常直接调用经过充分优化的 scikit-image 库来计算 SSIM:
import cv2
from skimage.metrics import structural_similarity as ssim
def calculate_ssim(img_path1: str, img_path2: str, multichannal: bool = False):
"""
计算两张图片的结构相似度 SSIM 分数
"""
img1 = cv2.imread(img_path1)
img2 = cv2.imread(img_path2)
if img1 is None or img2 is None:
raise FileNotFoundError("无法加载输入图像。")
# 1. SSIM 要求两张图片具有完全相同的几何尺寸
if img1.shape[:2] != img2.shape[:2]:
# 若尺寸不一致,按参考图尺寸自适应双三次插值缩放
img2 = cv2.resize(img2, (img1.shape[1], img1.shape[0]), interpolation=cv2.INTER_CUBIC)
# 2. 灰度化或多通道处理(灰度化最贴合人眼对结构的感知且计算速度更快)
if not multichannal:
gray1 = cv2.cvtColor(img1, cv2.COLOR_BGR2GRAY)
gray2 = cv2.cvtColor(img2, cv2.COLOR_BGR2GRAY)
# full=True 会额外返回每个像素位置的局部差异矩阵 diff
score, diff = ssim(gray1, gray2, full=True)
else:
# 彩色多通道评估 (channel_axis=2 对应 RGB 维度)
score, diff = ssim(img1, img2, channel_axis=2, full=True)
# diff 矩阵归一化到 0~255 可视化热力图
diff_image = (diff * 255).astype("uint8")
return {
"ssim_score": score,
"diff_map": diff_image
}
# 示例调用
# res = calculate_ssim("original.png", "compressed.jpg")
# print(f"SSIM 结构保真度分数: {res['ssim_score']:.4f}")
# if res['ssim_score'] > 0.95:
# print("画质接近无损或人眼无感知")
五、算法综合对比与生产环境工程落地指南
为了直观展现各算法的技术特质与适用边界,下表从计算复杂度、形变容忍度及典型应用场景等维度进行了横向归纳:
| 比对算法 | 单次比对耗时 | 尺寸缩放鲁棒性 | 轻度旋转/形变 | 光照与色彩变化 | 空间结构感知力 | 核心适用场景 |
|---|---|---|---|---|---|---|
| HSV 直方图 | ~ 1.2 ms | 极高(尺寸无关) | 高(无空间约束) | 中等(依赖 H-S) | 极弱(无法识别拼图) | 色调检索、电商同色系商品推荐 |
| 均值哈希 aHash | < 0.05 ms | 高(缩放到 8×8) | 弱(>5° 迅速失效) | 较好(纯灰度均值) | 较弱(过分平滑) | 简单缩略图快速匹配 |
| 差异哈希 dHash | < 0.05 ms | 高(缩放到 9×8) | 弱(>5° 迅速失效) | 极好(局部相对梯度) | 良好(保留边缘主轴) | 大规模图库秒级去重、粗筛索引 |
| 感知哈希 pHash | ~ 0.4 ms | 高(频域低频保持) | 较好(低频抗轻微形变) | 极好(DCT 特征抗压) | 中等(宏观轮廓特征) | 版权保护、防二创/加水印监测 |
| 结构相似性 SSIM | 15 ~ 50 ms | 弱(需预先尺寸对齐) | 极弱(要求严格像素对齐) | 良好(局部均值解耦) | 极高(像素级纹理/结构) | 图像压缩评级、精密质检、精排 |
六、实践总结与避坑检查项
在实际编写 Python 图像相似度计算服务时,建议遵循以下工程检查要点:
- 尺寸预对齐策略:除颜色直方图对输入尺寸不敏感外,SSIM 严格要求尺寸一致;感知哈希内部会自动缩放,但建议使用
cv2.INTER_AREA进行降采样,以获得抗混叠性能最优的低频信号。 - 色彩空间的一致性:OpenCV 默认以 BGR 格式读取图像,而 Pillow 与 matplotlib 使用 RGB 格式。在比对前必须确保通道顺序一致,否则计算直方图与哈希时会产生严重偏差。
- 动态阈值标定:避免硬编码绝对阈值。建议抽取业务代表性数据集(相同图、微调图、不同图样本各数百张),绘制 ROC 曲线计算最佳阈值拐点,以平衡召回率与准确率。
通过合理组装“直方图的色彩把握能力”、“感知哈希的吞吐能力”与“SSIM 的高保真结构把关能力”,即可在 Python 环境下构建兼顾极速响应与精准度量的高可用图像相似度分析引擎。
免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。









