前言
本文所述相似图片,即为在视觉上一致的图片,因图片格式、形状变化、图片亮度等等变化,无法使用图片md5的方法来判断图片是否相似。在当前业务场景中,判断两张图片是否视觉一致,以及快速在海量图片中找到视觉相似的图片,有着迫切的需求,如下2个场景急需图片相似判定及快速查找的方案:
- 接口重构或者底层图片处理库的升级,需要使用线上请求,重放到测试环境中,使用图片的相似判定方法,对线上和测试环境下载的图片进行对比,看改动是否影响了下发的图片。
- 审核到一张涉及黄反的图片,快速查找当前图片服务里是否还有相似的图片进行屏蔽。 本文所述方案包含:
- 图片的相似判定方法,可用于接口变更时进行对比以保证变更不影响图片的下发。
- 快速在海量图片中找到相似图片的方法,可用于相似黄反图片的快速查找。
相似图片判定需要符合的要求
- 视觉物体完全一致的图片,如下面2张图


- 图片格式不一样,也可以判定
- 图片的亮度、对比度较小调整的情况下,仍然可以判定
- 图片等比缩放后,仍可以判定
- 图片压缩质量变化后,仍可以判定
- 图片有变形拉伸等,仍可以判定到有一定相似度
图片的相似判定方法
目前常见的图片相似判定方法如下:
- 图像颜色直方图是一种经典、简单但实用的图像相似性判定方法,它基于图像中颜色或亮度的统计分布来衡量两张图像之间的相似度。
- 感知哈希是一种用于图像相似性判断的轻量级算法,试图捕捉图像的"感知特征"——也就是肉眼看到的视觉结构。其先压缩图片,然后对压缩后的图片灰度化,再对灰度化后的图片提取特征(如 DCT),最后生成生成hash值,然后用汉明距离对hash值进行距离判定,低于10的说明有相似性,低于5的说明图片一致。常见的实现方式有均值哈希(aHash)、差值哈希(dHash)、感知哈希(pHash)和wHash(小波哈希)。
- 结构相似度(SSIM, Structural Similarity Index)模拟人眼对亮度、对比度、结构等的感知,SSIM ∈ [0, 1],越接近1表示越相似,专注于图像整体结构信息,对像素级别变化较敏感。
- 特征点匹配(局部特征)提取图像的关键点(角点、边缘),并进行匹配,常见方法:SIFT,SURF,ORB,通过匹配点的数量或匹配得分评估,鲁棒性强,抗旋转、缩放、部分遮挡,算法相对复杂。
- 深度学习嵌入(CNN Embedding)用卷积神经网络(如 ResNet、Inception、EfficientNet)提取语义特征向量(一般为512或2048维),用余弦相似度、欧氏距离等度量,强语义理解能力,能区分图片内容而不仅仅是像素,较为复杂。
本文主要叙述感知哈希用于图片相似度判定,图像颜色直方图方法判断准确率不高,而其余三种方法则实现复杂或者运行缓慢,不太适用于我们当前的业务场景需求。
感知哈希的四种实现方案,优缺点如下:
| 方法名 | 原理 | 特点 |
|---|---|---|
| aHash(平均哈希) | 灰度图均值对比 | 快速但抗变形弱 |
| dHash(差值哈希) | 相邻像素比较差值 | 对细节敏感 |
| pHash(感知哈希) | DCT频域特征 | 鲁棒性更强,推荐使用 |
| wHash(小波哈希) | 小波变换 | 抗干扰性强,但较慢 |
综合准确率和运行性能来看,选用pHash进行图片相似度判定是比较合适的。
基于DCT频域特征的pHash的核心原理
- 图像缩放,将图像缩小为 32×32 像素(或其他固定尺寸),降低复杂度但保留大致结构
- 转为灰度图 ,将 RGB 图像转为灰度,只保留亮度信息
- 离散余弦变换(DCT),使用 DCT 将图像转换为频域数据。DCT 会把图像转换为一堆频率系数,其中前几个系数代表整体结构,后面代表细节。
- 提取低频特征,截取 DCT 左上角的 8×8 共 64 个低频系数(去掉最左上角的 DC 分量)
- 生成哈希,计算这 64 个值的平均值,然后将每个值与均值比较:
≥ 均值:置 1
< 均值:置 0
得到一个 64 位的二进制字符串,即图像的 pHash - 示例输出(十六进制): 3c8f9ad3739e4aa5
使用汉明距离比较两个pHash
汉明距离(Hamming Distance)是一种度量两个等长字符串之间差异的指标,表示两个字符串在相同位置上不同字符的个数。它最早由理查德·汉明(Richard Hamming)提出,用于纠错编码领域,如海明码。
在图像相似性检测、感知哈希(aHash、dHash、pHash)中,它被广泛用于衡量两张图像的"相似度"。
汉明距离 = A 和 B 在对应位上不相同的位数:
A = 1011101
B = 1001001
↑ ↑ (两处不同)
→ 汉明距离 = 2
当我们使用感知哈希算法(如 pHash)将一张图像转为一个定长的二进制哈希值(通常是 64 位),我们可以通过汉明距离来判断图像是否相似:
| 汉明距离 | 图像相似性解读 |
|---|---|
| 0 | 几乎完全一致(可能是重复) |
| 1–5 | 高度相似(可能是轻度编辑) |
| 6–10 | 有一定相似性(结构、构图相似,但非同图) |
| 11–20 | 差异明显,不同图片 |
| ≥21 | 明显不同 |
为什么通常认为 5~10 是「可接受的相似范围」?因为:
- 感知哈希是模糊的特征摘要,轻度裁剪、缩放、亮度变化或压缩会导致部分位发生翻转;
- 实践中发现,轻微变动通常造成 1–5 位哈希不同;
- 6–10 表示局部较多的变化或是同类图片(如同一物体不同角度);
- 超过 15 通常是完全不同的图像。
实际使用建议:
- 如果你要「查重」:汉明距离 ≤ 5
- 如果你要「推荐相似图」:可以接受 ≤ 10
- 如果你要「反盗图检测」:可以考虑 ≤ 12~15,但需人工审核辅助
使用pHash及汉明距离判定图片相似性示例代码(Go)
package main
import (
"fmt"
"log"
// 图像格式支持
_ "image/jpeg"
_ "image/png"
"github.com/corona10/goimagehash"
"github.com/disintegration/imaging"
_ "golang.org/x/image/webp"
)
func main() {
// 加载图片
img1Path := "img1.jpg"
img2Path := "img2.webp"
img1, err := imaging.Open(img1Path)
if err != nil {
log.Fatalf("Failed to open image 1: %v", err)
}
img2, err := imaging.Open(img2Path)
if err != nil {
log.Fatalf("Failed to open image 2: %v", err)
}
// 生成 pHash
hash1, err := goimagehash.PerceptionHash(img1)
if err != nil {
log.Fatalf("Failed to compute pHash for img1: %v", err)
}
hash2, err := goimagehash.PerceptionHash(img2)
if err != nil {
log.Fatalf("Failed to compute pHash for img2: %v", err)
}
// 计算汉明距离
distance, err := hash1.Distance(hash2)
if err != nil {
log.Fatalf("Failed to calculate Hamming distance: %v", err)
}
fmt.Printf("pHash 1: %s\n", hash1.ToString())
fmt.Printf("pHash 2: %s\n", hash2.ToString())
fmt.Printf("汉明距离: %d\n", distance)
// 判断是否相似(阈值可以根据需求调整)
if distance <= 10 {
fmt.Println("这两张图片可能是相似的 ✅")
} else {
fmt.Println("这两张图片差异较大 ❌")
}
}
img1.jpg和img2.webp是完全一样的图片,只是格式不一样,输出结果如下:
pHash 1: p:f4b883c6338bc3cc
pHash 2: p:f4b883c6338bc3cc
汉明距离: 0
这两张图片可能是相似的
将img1.jpg从400x400,缩放成100x100,进行汉明距离计算,结果仍然为0。 将img1.jpg缩放拉伸成600x100之后,汉明距离依然为0。 说明pHash可以判定不同格式的图片,也可以判定等比缩放的图片,对拉伸的图片也具备一定的判定能力。
海量图片快速查找相似图片
黄反内容筛查这个业务场景,查出一张黄色/反动图片,需要可以快速的查找图片上传服务中是否还有相似的图片,以防止遗漏造成政策风险。如果直接把海量图片重新过一次图片审核,则成本过高,利用图片相似度判定的pHash,可以较低成本解决此问题,但把海量照片一张一张来对比pHash,则效率过于低效。
基于 pHash(感知哈希) 和 LSH(局部敏感哈希)+ Redis 实现相似图片的快速查找,是一个实用且高效的解决方案。LSH(Locality Sensitive Hashing)将高维的pHash向量映射到多个桶中,使得相似的数据被映射到同一个桶,在查找时,只需比较同一个桶里的元素,降低计算量。
pHash + LSH + Redis 存储图片的过程大致如下(示例说明,具体使用过程存储结构会有所调整): ![[phash与redis的计算过程.png]]
![[相似图片海量查找.png]]
用python代码简单的实现来说明使用流程: 步骤一、计算图片的pHash
from PIL import Image
import imagehash
def compute_phash(image_path):
img = Image.open(image_path)
return imagehash.phash(img) # 返回的是ImageHash对象,可以转成int或str
步骤二、将pHash值输入LSH并构建倒排索引(存入Redis),简单实现(以分段LSH为例)
import redis
# 连接Redis
r = redis.StrictRedis(host='localhost', port=6379, db=0)
def lsh_buckets(phash_bin, band_size=8):
"""将64位phash切成8段,返回8个bucket key"""
return [phash_bin[i:i+band_size] for i in range(0, 64, band_size)]
def index_image(image_id, phash_bin):
buckets = lsh_buckets(phash_bin)
for i, bucket in enumerate(buckets):
key = f"lsh_bucket:{i}:{bucket}"
r.sadd(key, image_id) # 把图片ID加入桶中
步骤三、查找相似图片
def query_similar_images(query_phash_bin, threshold=10):
candidate_ids = set()
buckets = lsh_buckets(query_phash_bin)
for i, bucket in enumerate(buckets):
key = f"lsh_bucket:{i}:{bucket}"
ids = r.smembers(key)
candidate_ids.update(ids)
# 过滤阶段:计算海明距离
similar_images = []
query_int = int(query_phash_bin, 2)
for cid in candidate_ids:
cid = cid.decode() # Redis返回bytes
stored_phash = r.get(f"phash:{cid}")
if stored_phash:
stored_int = int(stored_phash.decode(), 2)
dist = bin(query_int ^ stored_int).count("1")
if dist <= threshold:
similar_images.append((cid, dist))
# 按距离排序
return sorted(similar_images, key=lambda x: x[1])
步骤4:存储图片pHash(辅助步骤),在图片首次索引时,同时存储其pHash
def store_phash(image_id, phash_bin):
r.set(f"phash:{image_id}", phash_bin)
我们从一张图像中提取出 pHash 值(通常是 64 位二进制字符串,例如):
pHash = "1101010110010010010110100100101010100110010110101010101010101010"
将 pHash 切分为多个 bands(即 LSH 的子哈希段):
bands = [
'11010101', # band 0
'10010010', # band 1
'01011010', # ...
'01001010',
'10100110',
'01011010',
'10101010',
'10101010'
]
为每个 band 生成 Redis 中的桶 key(中间的数据即为哈希段的序号,分为8段):
lsh_bucket:0:11010101
lsh_bucket:1:10010010
...
lsh_bucket:7:10101010
以上述的 lsh_bucket:x:xxxxxxxx 为Redis桶的key,桶的存储类型为Set,值是图片id。pHash分成8段,每段按序生成桶key,取出桶key对应的图片id的集合,然后找到每个图片的pHash,进行汉明距离的计算,找到相似图片。
分成8个段,通过8个桶的联合查找,提高查全率。
基于LSH查找相似图片的查全率
- s 是两个元素的相似度(1 - 距离/总位数),汉明距离=6,总位数为64,则s=1-6/64=0.906
- r 是每 band 的位数
- b 是 band 数量 举例:如果两张图片海明距离是 6(相似度约 90.6%) 假设 r = 8, b = 8
- 假设
r = 8,b = 8 - 则被 LSH 检出的概率 ≈ 1 - (1 - 0.906⁸)⁸ ≈ 99.21%
如何选择 r 和 b?一般经验法则(以 64 位 pHash 为例):
| 目标 | 建议设置 |
|---|---|
| 相似图像查全率高(Recall 优先) | r = 8, b = 8(即 64 位分成 8 个 band,每 band 8 位) |
| 精度更高(减少误报) | r = 16, b = 4(更严格 band 规则) |
| 模糊匹配更宽松(召回多) | r = 4, b = 16(宽容 band) |
实例对比(假设 s = 0.9,汉明距离=6.4):
| (r, b) | P_hit (s=0.9) | 候选图数量趋势 | 精度 |
|---|---|---|---|
| (4, 16) | ≈ 99.8% | 多(宽松) | 低 |
| (8, 8) | ≈ 98.8% | 适中 | 中 |
| (16, 4) | ≈ 89.0% | 少 | 高 |
| (32, 2) | ≈ 66.0% | 很少 | 很高 |
越小的 r,每个 band 越容易碰撞 → 查全率高但误报多
越大的 r,要求越严格 → 误报少但漏掉一些相似图像
推荐组合总结(64位 pHash 场景):
| 场景 | 推荐参数 (r, b) |
|---|---|
| 默认使用(平衡 recall/precision) | (8, 8) |
| 想召回更多相似图(对 recall 要求高) | (4, 16) |
| 精度要求极高(减少误判) | (16, 4) |
假设你有 1,000,000 张图,使用 64 位 pHash,分 8 个 band。 如果 r = 4:每个 band 有 2⁴ = 16 种可能 → 分 bucket 粒度很粗,一个 bucket 可能有几万个图,查询时要对成千上万张图做 Hamming 比较(慢) 如果 r = 8:每个 band 有 2⁸ = 256 种可能 → 分得细,每个 bucket 只有几百张图,查询候选少,速度更快
综上,使用每band的位数r=8,band的数量b=8,在64位pHash的使用场景中,查全率和查找速度都是相对均衡的。
总结
利用图片的pHash进行相似图片的查找,计算速度较快,准确率高,在配合图片宽高数据等,可以用于图片下发服务流量对比测试,确保接口重构、优化过程中没有下发错误的照片。而使用LSH + Redis,则可以实现海量图片中相似图片的快速查找,在图片的审核上有较好的应用场景,可以快速找到图片上传服务里相似的黄反图片,且成本低廉。
Last modified on 2025-10-09