相似图片的判定及海量图片快速查找相似图片的实现方案
前言

本文所述相似图片,即为在视觉上一致的图片,因图片格式、形状变化、图片亮度等等变化,无法使用图片md5的方法来判断图片是否相似。在当前业务场景中,判断两张图片是否视觉一致,以及快速在海量图片中找到视觉相似的图片,有着迫切的需求,如下2个场景急需图片相似判定及快速查找的方案:

  1. 接口重构或者底层图片处理库的升级,需要使用线上请求,重放到测试环境中,使用图片的相似判定方法,对线上和测试环境下载的图片进行对比,看改动是否影响了下发的图片。
  2. 审核到一张涉及黄反的图片,快速查找当前图片服务里是否还有相似的图片进行屏蔽。 本文所述方案包含:
  3. 图片的相似判定方法,可用于接口变更时进行对比以保证变更不影响图片的下发。
  4. 快速在海量图片中找到相似图片的方法,可用于相似黄反图片的快速查找。
相似图片判定需要符合的要求
  1. 视觉物体完全一致的图片,如下面2张图

  1. 图片格式不一样,也可以判定
  2. 图片的亮度、对比度较小调整的情况下,仍然可以判定
  3. 图片等比缩放后,仍可以判定
  4. 图片压缩质量变化后,仍可以判定
  5. 图片有变形拉伸等,仍可以判定到有一定相似度
图片的相似判定方法

目前常见的图片相似判定方法如下:

  1. 图像颜色直方图是一种经典、简单但实用的图像相似性判定方法,它基于图像中颜色或亮度的统计分布来衡量两张图像之间的相似度。
  2. 感知哈希是一种用于图像相似性判断的轻量级算法,试图捕捉图像的"感知特征"——也就是肉眼看到的视觉结构。其先压缩图片,然后对压缩后的图片灰度化,再对灰度化后的图片提取特征(如 DCT),最后生成生成hash值,然后用汉明距离对hash值进行距离判定,低于10的说明有相似性,低于5的说明图片一致。常见的实现方式有均值哈希(aHash)、差值哈希(dHash)、感知哈希(pHash)和wHash(小波哈希)。
  3. 结构相似度(SSIM, Structural Similarity Index)模拟人眼对亮度、对比度、结构等的感知,SSIM ∈ [0, 1],越接近1表示越相似,专注于图像整体结构信息,对像素级别变化较敏感。
  4. 特征点匹配(局部特征)提取图像的关键点(角点、边缘),并进行匹配,常见方法:SIFT,SURF,ORB,通过匹配点的数量或匹配得分评估,鲁棒性强,抗旋转、缩放、部分遮挡,算法相对复杂。
  5. 深度学习嵌入(CNN Embedding)用卷积神经网络(如 ResNet、Inception、EfficientNet)提取语义特征向量(一般为512或2048维),用余弦相似度、欧氏距离等度量,强语义理解能力,能区分图片内容而不仅仅是像素,较为复杂。

本文主要叙述感知哈希用于图片相似度判定,图像颜色直方图方法判断准确率不高,而其余三种方法则实现复杂或者运行缓慢,不太适用于我们当前的业务场景需求。

感知哈希的四种实现方案,优缺点如下:

方法名原理特点
aHash(平均哈希)灰度图均值对比快速但抗变形弱
dHash(差值哈希)相邻像素比较差值对细节敏感
pHash(感知哈希)DCT频域特征鲁棒性更强,推荐使用
wHash(小波哈希)小波变换抗干扰性强,但较慢

综合准确率和运行性能来看,选用pHash进行图片相似度判定是比较合适的。

基于DCT频域特征的pHash的核心原理
  1. 图像缩放,将图像缩小为 32×32 像素(或其他固定尺寸),降低复杂度但保留大致结构
  2. 转为灰度图 ,将 RGB 图像转为灰度,只保留亮度信息
  3. 离散余弦变换(DCT),使用 DCT 将图像转换为频域数据。DCT 会把图像转换为一堆频率系数,其中前几个系数代表整体结构,后面代表细节。
  4. 提取低频特征,截取 DCT 左上角的 8×8 共 64 个低频系数(去掉最左上角的 DC 分量)
  5. 生成哈希,计算这 64 个值的平均值,然后将每个值与均值比较: ≥ 均值:置 1 < 均值:置 0
    得到一个 64 位的二进制字符串,即图像的 pHash
  6. 示例输出(十六进制): 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查找相似图片的查全率

$$ P(s) = 1 - (1 - s^r)^b $$
上述公式是在描述基于分段哈希 (banding technique) 的 LSH 方法中,估计具有相似度 s 的两个元素在至少一个 band 中发生碰撞的概率的常见且被广泛使用的公式。其中:

  • 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