基于离散余弦变换的感知哈希算法:原理、实现与工程实践

如果需要下载源码请到:纯Qt实现pHash算法源码资源-CSDN下载

摘要

感知哈希(Perceptual Hash, pHash)是一类将多媒体内容映射为紧凑指纹的算法族,其核心特性在于:语义相似的输入产生相近的哈希值,而传统密码学哈希(如SHA-256)则追求雪崩效应。本文系统阐述三种主流图像感知哈希算法------均值哈希(aHash)、差异哈希(dHash)和基于离散余弦变换的感知哈希(DCT-pHash)的数学原理与工程实现,并给出一个零外部依赖、基于Qt框架的C++生产级实现方案。

**关键词**:感知哈希;离散余弦变换;图像相似度;汉明距离;近似重复检测

1 引言

1.1 问题背景

在UI自动化测试、内容审核、版权检测等场景中,需要高效判断两张图片是否"视觉上相似"。传统的逐像素比较方法存在明显缺

  • **尺寸敏感**:同一图片缩放后像素矩阵完全不同

  • **编码敏感**:JPEG压缩引入的量化噪声导致像素级差异

  • **几何敏感**:微小的平移、裁剪即导致比对失败

感知哈希通过提取图像的结构性特征而非像素级细节,将高维图像数据降维为固定长度的二进制指纹,在保持语义不变性的同时实现O(1)时间复杂度的相似度比较。

1.2 符号约定

| 符号 | 含义 |

|------|------|

| I(x,y) | 灰度图像在坐标(x,y)处的像素值 |

| F(u,v) | DCT变换后频域系数 |

| H | 64位哈希值 |

| d_H(H_1, H_2) | 两个哈希值的汉明距离 |

| N | 变换矩阵维度 |

2 算法原理

2.1 均值哈希(Average Hash, aHash)

均值哈希是最简单的感知哈希算法,其核心思想是将图像的全局亮度分布编码为二进制串。

**算法流程:**

  1. **预处理**:将输入图像缩放至 8 \\times 8 像素并转换为灰度图,得到矩阵 I \\in \\mathbb{R}\^{8 \\times 8}

  2. **计算全局均值**:

\\bar{I} = \\frac{1}{64} \\sum_{x=0}\^{7} \\sum_{y=0}\^{7} I(x,y)

  1. **二值化编码**:

H\[k\] = \\begin{cases} 1, \& \\text{if } I(x,y) \\geq \\bar{I} \\\\ 0, \& \\text{otherwise} \\end{cases}, \\quad k = 8x + y

**复杂度分析**:时间复杂度 O(1)(固定64次比较),空间复杂度 O(1)(64位整数)。

**局限性**:aHash仅编码了全局亮度分布,对局部结构变化不敏感。当两张图片的亮度直方图相似但空间结构不同时,可能产生误判

2.2 差异哈希(Difference Hash, dHash)

差异哈希通过编码相邻像素的梯度方向来捕获图像的局部结构信息。

**算法流程:**

  1. **预处理**:将输入图像缩放至 9 \\times 8 像素(宽度多1列用于差分)并转换为灰度图

  2. **水平梯度编码**:

H\[k\] = \\begin{cases} 1, \& \\text{if } I(x, y+1) \> I(x, y) \\\\ 0, \& \\text{otherwise} \\end{cases}, \\quad k = 8x + y, \\quad y \\in \[0,7\]

**优势**:相比aHash,dHash编码了像素间的相对关系而非绝对值,因此对全局亮度变化(如曝光调整)具有更好的鲁棒性。

2.3 DCT感知哈希(DCT-based pHash)

DCT-pHash是三种算法中精度最高的,其核心思想是利用离散余弦变换将图像从空间域转换到频率域,然后仅保留低频分量作为图像的结构性指纹。

2.3.1 离散余弦变换(DCT)

二维DCT-II的定义为:

F(u,v) = \\frac{2}{N} C(u) C(v) \\sum_{x=0}\^{N-1} \\sum_{y=0}\^{N-1} I(x,y) \\cos\\left\[\\frac{\\pi(2x+1)u}{2N}\\right\] \\cos\\left\[\\frac{\\pi(2y+1)v}{2N}\\right\]

其中归一化系数:

C(k) = \\begin{cases} \\frac{1}{\\sqrt{2}}, \& k = 0 \\\\ 1, \& k \> 0 \\end{cases}

DCT的关键性质在于**能量集中**:自然图像的大部分能量集中在低频系数中。高频系数对应图像的细节和噪声,低频系数对应图像的整体结构。

2.3.2 算法流程

输入图像 → 缩放至32×32 → 灰度化 → 32×32 DCT → 取左上8×8 → 均值二值化 → 64位哈希

详细步骤:

  1. **预处理**:将输入图像缩放至 32 \\times 32 并转换为灰度图。选择32而非8的原因是DCT需要足够的空间分辨率来产生有意义的频率分量。

  2. **二维DCT变换**:对 32 \\times 32 矩阵执行二维DCT。为提升计算效率,采用**行列分离**策略:

  • 先对每一行执行一维DCT:

R(x,u) = C(u) \\sqrt{\\frac{2}{N}} \\sum_{y=0}\^{N-1} I(x,y) \\cos\\left\[\\frac{\\pi(2y+1)u}{2N}\\right\]

  • 再对结果的每一列执行一维DCT:

F(u,v) = C(v) \\sqrt{\\frac{2}{N}} \\sum_{x=0}\^{N-1} R(x,u) \\cos\\left\[\\frac{\\pi(2x+1)v}{2N}\\right\]

分离式DCT将 O(N\^4) 的直接计算降低为 O(N\^3)

  1. **低频提取**:取DCT系数矩阵的左上角 8 \\times 8 子矩阵。根据DCT的能量集中特性,这64个系数包含了图像最显著的结构信息。

  2. **均值二值化**:计算64个低频系数的均值(排除DC分量 F(0,0),因为DC分量仅反映全局亮度),将每个系数与均值比较生成64位哈希。

2.3.3 为什么排除DC分量

DC分量 F(0,0) 是所有像素值的加权和,本质上是图像的平均亮度。排除它使得哈希对全局亮度变化(如不同显示器的gamma校正、环境光影响)具有不变性。

3 相似度度量

3.1 汉明距离

两个等长二进制串的汉明距离定义为对应位不同的个数:

d_H(H_1, H_2) = \\text{popcount}(H_1 \\oplus H_2)$

其中 \\oplus 为按位异或运算,\\text{popcount} 计算二进制中1的个数。

对于64位哈希,d_H \\in \[0, 64\]

  • d_H = 0:完全相同

  • d_H \\leq 5:高度相似(通常认为是同一图像的变体)

  • d_H \\leq 10:相似

  • d_H \> 10:不同图像

3.2 归一化相似度

S(H_1, H_2) = 1 - \\frac{d_H(H_1, H_2)}{64}

S \\in \[0, 1\],其中1表示完全相同。

3.3 汉明距离的高效计算

在现代x86处理器上,汉明距离可通过硬件指令 `POPCNT` 在单个时钟周期内完成:

cpp 复制代码
int hammingDistance(uint64_t hash1, uint64_t hash2)

{

    uint64_t diff = hash1 ^ hash2;

    int distance = 0;

    while (diff) {

        distance += diff & 1;

        diff >>= 1;

    }

    return distance;

}

编译器在开启优化时会自动将上述循环替换为 `__popcnt64` 内联指令。

4 工程实现

4.1 技术选型

| 维度 | 选择 | 理由 |

|------|------|------|

| 语言 | C++11 | 性能要求、与现有项目集成 |

| 图像处理 | Qt QImage | 项目已依赖Qt,避免引入OpenCV等重量级库 |

| 构建产物 | DLL动态库 | 便于多项目复用 |

| 外部依赖 | 无 | DCT手动实现,消除对FFTW/CImg的依赖 |

4.2 关键实现细节

4.2.1 DCT的行列分离实现

直接实现二维DCT需要四重循环(O(N\^4)),通过行列分离降低为两次一维DCT(O(N\^3)):

cpp 复制代码
// 行 DCT

for (int y = 0; y < N; y++) {

    for (int u = 0; u < N; u++) {

        double sum = 0.0;

        for (int x = 0; x < N; x++) {

            sum += matrix[y * N + x]

                 * qCos(M_PI * (2.0 * x + 1.0) * u / (2.0 * N));

        }

        double cu = (u == 0) ? 1.0 / qSqrt(2.0) : 1.0;

        rowDct[y * N + u] = cu * sum * qSqrt(2.0 / N);

    }

}



// 列 DCT

for (int x = 0; x < N; x++) {

    for (int v = 0; v < N; v++) {

        double sum = 0.0;

        for (int y = 0; y < N; y++) {

            sum += rowDct[y * N + x]

                 * qCos(M_PI * (2.0 * y + 1.0) * v / (2.0 * N));

        }

        double cv = (v == 0) ? 1.0 / qSqrt(2.0) : 1.0;

        fullDct[v * N + x] = cv * sum * qSqrt(2.0 / N);

    }

}

4.2.2 灰度转换

Qt 5.15提供了 `QImage::Format_Grayscale8` 格式,内部使用ITU-R BT.601标准加权公式:

Y = 0.299R + 0.587G + 0.114B

通过 `convertToFormat(QImage::Format_Grayscale8)` 一步完成,避免手动逐像素转换。

4.2.3 DLL导出机制

采用Qt标准的导出宏模式:

cpp 复制代码
#if defined(PHASH_LIBRARY)

#  define PHASH_EXPORT Q_DECL_EXPORT  // 编译DLL时导出符号

#else

#  define PHASH_EXPORT Q_DECL_IMPORT  // 使用DLL时导入符号

#endif

编译DLL时定义 `PHASH_LIBRARY` 预处理宏,使用方仅需包含头文件并链接 `.lib` 文件。

4.3 API设计

库提供三个层次的API:

┌─────────────────────────────────────────────┐

│ 高层API: compareImages(), isSimilar() │ ← 一步完成比较

├─────────────────────────────────────────────┤

│ 中层API: pHash(), aHash(), dHash() │ ← 计算哈希值

├─────────────────────────────────────────────┤

│ 底层API: hammingDistance(), similarity() │ ← 哈希值比较

└─────────────────────────────────────────────┘

每个哈希函数提供三种重载,接受 `QImage`、`QPixmap` 或文件路径 `QString`,适配不同使用场景。

5 算法对比与选型建议

5.1 性能对比

| 算法 | 预处理尺寸 | 计算复杂度 | 单次耗时(参考值) |

|------|-----------|-----------|-------------------|

| aHash | 8×8 | O(1) | ~0.1ms |

| dHash | 9×8 | O(1) | ~0.1ms |

| pHash | 32×32 | O(N\^3), N=32 | ~1ms |

5.2 精度对比

| 场景 | aHash | dHash | pHash |

|------|-------|-------|-------|

| JPEG压缩 | ★★☆ | ★★★ | ★★★ |

| 缩放变换 | ★★☆ | ★★★ | ★★★ |

| 亮度调整 | ★☆☆ | ★★☆ | ★★★ |

| 轻微裁剪 | ★☆☆ | ★★☆ | ★★★ |

| 色彩空间转换 | ★★★ | ★★★ | ★★★ |

| 水印添加 | ★☆☆ | ★★☆ | ★★★ |

5.3 选型建议

  • **实时性要求高、精度要求一般**:使用dHash(如视频帧去重)

  • **精度要求高、可接受毫秒级延迟**:使用pHash(如UI自动化截图验证)

  • **需要快速预筛选**:先用aHash/dHash粗筛,再用pHash精确比对

6 应用场景

6.1 UI自动化测试中的截图验证

在自动化测试执行过程中,每一步操作后截取屏幕截图,通过pHash与基准截图比对,判断UI状态是否符合预期:

cpp 复制代码
PHash phash;

uint64_t baselineHash = PHash::pHash("baseline_step1.png");

uint64_t currentHash  = PHash::pHash(currentScreenshot);



double sim = PHash::similarity(baselineHash, currentHash);

if (sim < 0.85) {

    // UI状态异常,标记为失败

    record.status = "Failed";

    record.failureReason = QString("UI不匹配,相似度: %1%").arg(sim * 100, 0, 'f', 1);

}

6.2 近似重复图片检测

利用哈希值的汉明距离进行大规模图片去重。64位哈希值可直接作为数据库索引键,支持O(1)查找:

cpp 复制代码
QMap<uint64_t, QString> hashIndex;

for (const QString &file : imageFiles) {

    uint64_t h = PHash::pHash(file);

    if (hashIndex.contains(h)) {

        qDebug() << "Duplicate:" << file << "==" << hashIndex[h];

    }

    hashIndex[h] = file;

}

// 建立哈希索引

6.3 哈希值的序列化与持久化

库提供 `hashToString()` / `stringToHash()` 方法,将64位哈希值与16字符十六进制字符串互转,便于存储到数据库或配置文件:

cpp 复制代码
uint64_t hash = PHash::pHash(image);

QString hexStr = PHash::hashToString(hash);  // "a3b4c5d6e7f80912"

uint64_t restored = PHash::stringToHash(hexStr);

assert(hash == restored);

7 局限性与改进方向

7.1 当前局限

  1. **旋转不变性**:当前实现对大角度旋转(>5°)敏感。DCT本身不具备旋转不变性。

  2. **局部修改**:大面积局部修改(如替换图片中的某个区域)可能不被检测到,因为低频分量可能未受影响。

  3. **DCT计算效率**:当前采用朴素的 O(N\^3) 实现,对于批量处理场景可进一步优化。

7.2 改进方向

  1. **引入FFT加速**:将DCT通过FFT实现,复杂度降至 O(N\^2 \\log N)

  2. **预计算余弦表**:将 \\cos 值预计算为查找表,消除运行时三角函数调用。

  3. **多尺度哈希**:在多个分辨率下计算哈希并组合,提升对局部修改的检测能力。

  4. **径向哈希**:采用Radon变换替代DCT,获得旋转不变性。

8 结论

本文实现的感知哈希库以极低的工程复杂度(单个源文件、零外部依赖)提供了生产级的图像相似度比较能力。DCT-pHash算法通过频域分析提取图像的结构性指纹,在JPEG压缩、缩放、亮度调整等常见变换下保持稳定,汉明距离阈值5以内即可可靠判定图像相似性。该实现已集成至UI自动化测试框架中,用于执行报告中的截图一致性验证。

如果需要下载源码请到:纯Qt实现pHash算法源码资源-CSDN下载

相关推荐
2301_807367192 小时前
C++中的模板方法模式
开发语言·c++·算法
PhotonixBay2 小时前
共聚焦显微镜的结构组成与应用
人工智能·算法·机器学习
逆境不可逃2 小时前
LeetCode 热题 100 之 33. 搜索旋转排序数组 153. 寻找旋转排序数组中的最小值 4. 寻找两个正序数组的中位数
java·开发语言·数据结构·算法·leetcode·职场和发展
tankeven2 小时前
HJ137 乘之
c++·算法
add45a3 小时前
C++中的观察者模式
开发语言·c++·算法
进击的小头3 小时前
第13篇:基于伯德图的超前_滞后校正器深度设计
python·算法
leaves falling3 小时前
二分查找:迭代与递归实现全解析
数据结构·算法·leetcode
做怪小疯子3 小时前
Leetcode刷题——深度优先搜索(DFS)
算法·leetcode·深度优先
大数据AI人工智能培训专家培训讲师叶梓3 小时前
120B 数学语料 + GRPO 算法,DeepSeekMath 刷新开源大模型推理天花板
人工智能·算法·大模型·推理·deepseek·openclaw·openclaw 讲师