图像指纹技术 - 使用 pHash 和 dHash 检测相似图像
什么是图像指纹 - 感知哈希基础
图像指纹(感知哈希)是将图像转换为紧凑的固定长度哈希值的技术。与加密哈希(MD5、SHA)不同,感知哈希对视觉上相似的图像产生相似的哈希值,即使图像经过缩放、压缩或轻微修改。
与加密哈希的区别:
- 加密哈希:1 位变化导致完全不同的哈希。用于验证文件完整性
- 感知哈希:视觉上相似的图像产生相近的哈希。用于相似性检测
应用场景:
- 重复图像检测:在大型图库中找出重复或近似重复的图像
- 版权保护:检测未授权使用的图像(即使经过裁剪、缩放、滤镜处理)
- 内容审核:检测已知违规图像的变体
- 反向图像搜索:根据图像内容查找来源
- 缓存去重:避免存储视觉上相同的图像的多个副本
术语与原理:图像指纹 (Image Fingerprinting) 是把图像的视觉特征转换为短的固定长度哈希值的技术。普通的密码学哈希 (如 SHA-256) 只要输入变化 1 比特就会输出完全不同的值,而感知哈希 (Perceptual Hash) 会为看起来相似的图像生成相似的哈希值。
典型用途:反向图像搜索是像 Google Images 那样「查找与这张图像相似的图像」功能的基础技术;内容审核则是与已知违规图像的哈希库比对,阻止其再次上传。感知哈希的核心在于「让人类觉得相同的图像得到相同的哈希,让人类觉得不同的图像得到不同的哈希」。常见的哈希长度为 64 比特,2 个哈希之间的汉明距离 (不同的比特数) 就是相似度指标。
aHash(平均哈希)- 最简单的图像哈希
aHash 是最简单的感知哈希算法,通过比较每个像素与平均亮度的关系生成哈希值。
算法步骤:
- 将图像缩小到 8x8(64 像素),消除尺寸差异
- 转为灰度图
- 计算 64 个像素的平均亮度值
- 每个像素与平均值比较:大于平均值为 1,否则为 0
- 生成 64 位哈希值
优点:
- 实现极其简单,计算速度最快
- 对缩放和轻微亮度变化有一定鲁棒性
缺点:
- 对伽马校正和直方图调整敏感
- 区分度较低,容易产生误匹配
- 无法处理局部修改(如添加水印)
适用场景:快速预筛选,作为更精确方法的第一步过滤。对精度要求不高的简单去重。
算法步骤:aHash (Average Hash) 是最简单的感知哈希算法,以图像的平均亮度为基准生成位串。Step 1 缩放:把图像缩小到 8x8 像素 (共 64 个像素),这一步会去除高频成分 (细节),只保留图像的大致结构。Step 2 灰度化:把彩色图像转为灰度,去掉颜色信息、只用亮度比较。Step 3 求平均:计算 64 个像素亮度值的平均值。Step 4 生成位:每个像素的亮度大于等于平均值记为 1,小于则记为 0,得到 64 比特哈希。
实现示例 (Python):from PIL import Image; img = Image.open('photo.jpg').resize((8,8)).convert('L'); pixels = list(img.getdata()); avg = sum(pixels)/len(pixels); hash_bits = ''.join('1' if p >= avg else '0' for p in pixels)
速度与误检:aHash 的优势是计算速度,单张的计算成本很低,因此对较大规模的库做全量扫描也是现实可行的选择。另一方面,缩小到 8x8 会丢失空间结构信息,因此容易把只是构图相似的无关图像误判为相同 (false positive)。
dHash(差异哈希)- 基于梯度的快速哈希
dHash 通过比较相邻像素的亮度差异(梯度)生成哈希,比 aHash 更能捕获图像的结构信息。
算法步骤:
- 将图像缩小到 9x8(72 像素,宽度多 1 列用于计算差异)
- 转为灰度图
- 对每行,比较相邻像素:右侧像素比左侧亮为 1,否则为 0
- 8 行 x 8 列差异 = 64 位哈希值
为什么比 aHash 好:
- 捕获的是相对亮度变化(梯度)而非绝对亮度,对整体亮度/对比度调整更鲁棒
- 保留了图像的结构信息(边缘方向)
- 计算速度与 aHash 相当
实现(Python):
from PIL import Imageimg = Image.open(path).resize((9, 8)).convert("L")- 比较相邻像素生成位串
适用场景:需要快速且比 aHash 更准确的场景。大规模图像去重的首选方法。
算法步骤:dHash (Difference Hash) 是基于相邻像素之间亮度差 (梯度) 生成哈希的算法。Step 1 缩放:把图像缩小到 9x8 像素 (宽度多 1 像素是为了取水平方向的差分)。Step 2 灰度化:从彩色转为灰度。Step 3 计算差分:在每一行比较左右相邻的像素,右侧更大记为 1,否则记为 0。
性能对比 (与 aHash 的一般倾向):计算速度与 aHash 基本相当;精度方面通常优于 aHash,尤其对经过亮度校正的图像检出率更高;误检也比 aHash 更少。具体数值会随图像种类、数据集与阈值设置而大幅变化。不过它无法应对图像的左右翻转和 90 度旋转。
pHash(感知哈希)- 基于 DCT 的高精度哈希
pHash 使用离散余弦变换(DCT)提取图像的频率特征,精度最高但计算量也最大。
算法步骤:
- 将图像缩小到 32x32,转为灰度
- 对 32x32 图像执行 DCT 变换
- 取左上角 8x8 的低频系数(代表图像的整体结构,忽略高频细节)
- 计算 64 个系数的中位数
- 每个系数与中位数比较:大于中位数为 1,否则为 0
- 生成 64 位哈希值
为什么精度最高:
- DCT 将图像从空间域转换到频率域,低频系数代表图像的本质结构
- 对高频噪声(压缩伪影、轻微模糊)天然不敏感
- 使用中位数而非平均值,对异常值更鲁棒
与 dHash 的对比:
- pHash 对 JPEG 压缩、轻微旋转、颜色调整的鲁棒性更好
- dHash 速度更快(无需 DCT),适合大规模预筛选
- 实践中常组合使用:dHash 快速过滤 + pHash 精确验证
算法步骤:pHash (Perceptual Hash) 是利用离散余弦变换 (DCT) 从图像的频率特性生成哈希的高精度算法。Step 1 缩放:把图像缩小到 32x32 像素 (比 aHash/dHash 的 8x8 更大,保留更多结构信息)。Step 2 灰度化:只使用亮度通道。Step 3 应用 DCT:对 32x32 的图像数据做二维 DCT,得到频率系数矩阵。Step 4 提取低频:只取 DCT 系数矩阵左上角的 8x8 (最低的频率成分),忽略高频成分 (细节),以确保对噪声和微小改动的鲁棒性。Step 5 求中位数:计算提取出的 64 个 DCT 系数的中位数 (也有实现用去掉 DC 成分后的 63 个来计算)。Step 6 生成位:每个系数大于等于中位数记为 1,小于则记为 0,得到 64 比特哈希。
鲁棒性与代价:JPEG 再压缩耐受性方面,再压缩后汉明距离的增加通常较小;缩放耐受性方面,缩小后汉明距离的变化也较小;轻微裁剪耐受性方面,周边的少量裁剪通常仍会被判定为相似。另一方面,由于需要做 DCT,计算成本明显高于 aHash/dHash。在大规模库中,先用 dHash 缩小候选范围、再用 pHash 精确判定的 2 阶段方案很有效。
汉明距离相似度评分与阈值设计
两个哈希值之间的汉明距离(不同位的数量)衡量图像的相似程度。阈值的选择直接影响检测的精确率和召回率。
汉明距离计算:
- 对两个 64 位哈希进行 XOR 运算,统计结果中 1 的数量
- 距离 0:完全相同的哈希(极可能是同一图像)
- 距离 1-10:非常相似(可能是同一图像的不同版本)
- 距离 > 20:很可能是不同图像
阈值设计:
- 严格匹配(距离 ≤ 5):仅检测近乎相同的图像。高精确率,低召回率
- 宽松匹配(距离 ≤ 10):检测经过修改的相似图像。平衡精确率和召回率
- 模糊匹配(距离 ≤ 15):检测视觉上相关的图像。高召回率,可能有误报
多哈希组合策略:
- 同时计算 dHash 和 pHash,两者都满足阈值才判定为相似
- 或使用加权组合:总分 = 0.4 * dHash距离 + 0.6 * pHash距离
- 不同应用场景调整权重和阈值
汉明距离的计算:图像指纹的比较,是通过 2 个哈希值之间的汉明距离 (Hamming Distance) 进行的。计算可以用 XOR 运算加上 popcount (数出值为 1 的比特数) 高速完成:distance = bin(hash1 ^ hash2).count('1')
阈值设计指引 (64 比特哈希):0-2 为几乎相同的图像,差异仅相当于 JPEG 再压缩或轻微缩放;3-5 为高相似度,包含轻微色调校正、滤镜、添加文字等;6-10 为中等相似度,包含裁剪、局部编辑、添加水印;11-15 为低相似度。若要把 false positive 降到最低、只确实检出同一图像,就要收紧阈值;版权侵权检测取阈值 10 以下,为了也能检出被编辑过的图像而设置得稍宽;相似图像推荐取阈值 12-15,广泛呈现视觉上相关的图像。
大规模检索:在 100 万张以上的大规模库中做高速检索时,使用 BK-Tree (Burkhard-Keller Tree) 或 VP-Tree (Vantage-Point Tree) 等度量空间索引。BK-Tree 是针对汉明距离的树结构,检索阈值 d 以内的哈希时需要比较的候选远少于全量扫描。此外,把哈希切成多个块并构建倒排索引的 Multi-Index Hashing,也是在大规模库上加速近邻检索的常用手法。
实现模式与实际系统架构
在生产环境中部署图像指纹系统需要考虑存储、索引和查询效率。
存储与索引:
- 数据库存储:将 64 位哈希存为 BIGINT 类型,支持位运算查询
- 分桶索引:将哈希分为多个子段(如 4 个 16 位段),对每段建立倒排索引。查询时在各段分别搜索,取交集
- VP-Tree:Vantage-Point Tree,专为汉明距离设计的空间索引结构。支持高效的范围查询
大规模系统架构:
- 入库流程:图像上传 → 计算多种哈希(dHash + pHash)→ 存入数据库 → 建立索引
- 查询流程:查询图像 → 计算哈希 → 索引搜索候选集 → 精确汉明距离计算 → 返回相似图像
- 增量更新:新图像入库时与现有库对比,检测重复
性能方面的考虑:
- 大规模图库:dHash 索引查询通常较快,但耗时取决于硬件与索引结构
- 超大规模图库:宜采用分布式索引(如 Elasticsearch 的位向量插件)
- 哈希计算:dHash 的开销小于 pHash,pHash 还需承担 DCT 的计算成本
推荐架构 (重复检测流水线):接入层在图像上传时用 Lambda/Cloud Functions 计算哈希,与元数据一起保存到 DynamoDB/Redis,并同时算好 dHash 和 pHash 这 2 种。检索层把新图像的哈希与既有库比对:先用 dHash 通过 BK-Tree 高速检索汉明距离 8 以内的候选,再对候选用 pHash 做精确判定 (阈值 5)。判定层对通过 pHash 阈值的图像对,按需追加像素级的 SSIM 比较,做出最终判定。
主要库与工具:imagehash (Python) 是实现了 aHash、dHash、pHash、wHash 的经典库,用 pip install imagehash 即可引入;blockhash-js (JavaScript) 是可在浏览器/Node.js 上运行的分块哈希实现;phash.org (C++) 是高性能的 pHash 实现,也支持视频指纹。
实际服务中的应用与局限:Google Images 在反向图像搜索中把感知哈希与深度学习特征量结合使用;Facebook/Instagram 用 PhotoDNA 和自研哈希技术检测并阻止 CSAM (儿童性虐待素材) 的扩散;Pinterest 则用于相似图像的聚类与推荐。需要注意的是,感知哈希并非万能:对大幅裁剪 (50% 以上)、旋转、宽高比变更、大量叠加文字等较弱,这些情况下需要 SIFT/ORB 等基于局部特征量的方法,或用 CNN (卷积神经网络) 提取特征。