图像重复检测:从感知哈希(pHash)开始构建

图像重复检测:从感知哈希(pHash)开始构建

本文旨在为希望在缺乏现有库支持的情况下,构建图片重复检测功能的开发者提供一个起点。我们将深入探讨感知哈希(pHash)这一核心技术,详细阐述其工作原理、实现步骤,并提供概念性的代码示例,以帮助读者理解如何生成图像指纹并进行相似度比较,从而有效识别近似重复的图片。

1. 感知哈希(pHash)概述

在构建图片库或相册网站时,检测并管理重复图片是一个常见需求。传统的哈希算法(如md5、sha-256)对数据的任何微小改动都极其敏感,即使是像素级的变化也会导致哈希值完全不同,因此不适用于图像的相似度检测。而感知哈希(perceptual hash, phash)则是一种能够根据图像的视觉内容生成“指纹”的算法。即使图像经过缩放、压缩、颜色调整等轻微修改,其phash值也能保持高度相似,从而实现对近似重复图像的识别。

pHash的核心思想在于:通过一系列降维和特征提取步骤,将图像的视觉特征编码成一个紧凑的二进制字符串(哈希值)。当需要比较两张图片时,只需计算它们pHash值之间的汉明距离(Hamming Distance),距离越小,图片相似度越高。

2. pHash工作流程详解

感知哈希的实现通常遵循以下几个核心步骤:

2.1 步骤一:尺寸缩减与灰度转换

为了简化图像数据并去除高频细节(这些细节通常对图像识别的干扰较大),首先将原始图像缩放到一个非常小的尺寸,例如8×8像素或32×32像素。同时,将图像转换为灰度图,进一步减少数据维度,只关注亮度信息。这个小尺寸的灰度图像包含了原始图像的低频信息,即其主要视觉特征。

2.2 步骤二:计算平均值

对缩减并灰度化后的图像中的所有像素值(亮度值)求平均。这个平均值将作为后续步骤中区分像素亮度的基准。

2.3 步骤三:生成哈希指纹

遍历缩减后的灰度图像中的每一个像素。将每个像素的亮度值与步骤二中计算出的平均值进行比较:

  • 如果像素值大于或等于平均值,则对应的哈希位设为1。
  • 如果像素值小于平均值,则对应的哈希位设为0。

将所有这些二进制位按顺序拼接起来,就得到了该图像的感知哈希指纹(例如,对于8×8的图像,会生成一个64位的二进制字符串)。

2.4 步骤四:相似度比较(汉明距离)

要判断两张图片是否相似,只需计算它们各自的pHash值之间的汉明距离。汉明距离是指两个等长二进制字符串中对应位置上不同位的数量。

例如: Hash A: 10110100 Hash B: 10100101 汉明距离为2(第4位和第8位不同)。

汉明距离越小,表示两个哈希值越相似,进而说明对应的两张图片在视觉上越接近。

图像重复检测:从感知哈希(pHash)开始构建

Vmake AI

全能电商创意工作室:生成AI服装虚拟模特

图像重复检测:从感知哈希(pHash)开始构建105

查看详情 图像重复检测:从感知哈希(pHash)开始构建

3. 概念性代码示例

以下是使用go语言风格的概念性代码骨架,展示了如何实现上述pHash步骤。由于Go语言标准库中没有直接的图像处理函数来完成所有步骤,这里主要展示其逻辑结构。

package main  import (     "image"     "image/color"     "image/draw"     "math" )  // LoadImageFromFile 模拟从文件加载图片 func LoadImageFromFile(filePath string) (image.Image, error) {     // 实际实现需要使用 image/jpeg, image/png 等库解码图片     // 这里仅为示例,假设已加载图片     return image.NewRGBA(image.Rect(0, 0, 100, 100)), nil // 示例图片 }  // ResizeAndGrayscale 将图片缩放并转换为灰度图 // 目标尺寸通常为8x8或32x32 func ResizeAndGrayscale(img image.Image, targetSize int) *image.Gray {     // 创建一个新的灰度图像画布     smallGray := image.NewGray(image.Rect(0, 0, targetSize, targetSize))      // 实际缩放和灰度转换需要更复杂的图像处理库     // 例如:github.com/nfnt/resize 或自定义像素插值     // 这里仅为概念性演示,直接将原始图像的平均亮度映射到小图     bounds := img.Bounds()     for y := 0; y < targetSize; y++ {         for x := 0; x < targetSize; x++ {             // 简化处理:从原图对应区域取样并转换为灰度             // 实际应进行插值缩放             srcX := int(float64(x) / float64(targetSize) * float64(bounds.Dx()))             srcY := int(float64(y) / float64(targetSize) * float64(bounds.Dy()))             r, g, b, _ := img.At(srcX, srcY).RGBA()             grayVal := uint8((0.299*float64(r) + 0.587*float64(g) + 0.114*float64(b)) / 256)             smallGray.SetGray(x, y, color.Gray{Y: grayVal})         }     }     return smallGray }  // CalculateAverage 计算灰度图像的平均亮度 func CalculateAverage(grayImg *image.Gray) float64 {     sum := 0.0     bounds := grayImg.Bounds()     for y := bounds.Min.Y; y < bounds.Max.Y; y++ {         for x := bounds.Min.X; x < bounds.Max.X; x++ {             sum += float64(grayImg.GrayAt(x, y).Y)         }     }     return sum / float64(bounds.Dx()*bounds.Dy()) }  // GeneratePerceptualHash 生成感知哈希指纹 func GeneratePerceptualHash(grayImg *image.Gray) string {     avg := CalculateAverage(grayImg)     hash := ""     bounds := grayImg.Bounds()     for y := bounds.Min.Y; y < bounds.Max.Y; y++ {         for x := bounds.Min.X; x < bounds.Max.X; x++ {             if float64(grayImg.GrayAt(x, y).Y) >= avg {                 hash += "1"             } else {                 hash += "0"             }         }     }     return hash }  // HammingDistance 计算两个哈希值之间的汉明距离 func HammingDistance(hash1, hash2 string) int {     if len(hash1) != len(hash2) {         panic("Hashes must be of the same length")     }     distance := 0     for i := 0; i < len(hash1); i++ {         if hash1[i] != hash2[i] {             distance++         }     }     return distance }  func main() {     // 示例流程     img1, _ := LoadImageFromFile("image1.jpg")     img2, _ := LoadImageFromFile("image2.jpg")      // 1. 缩放并灰度化 (例如,8x8)     targetSize := 8     grayImg1 := ResizeAndGrayscale(img1, targetSize)     grayImg2 := ResizeAndGrayscale(img2, targetSize)      // 2. 生成哈希     hash1 := GeneratePerceptualHash(grayImg1)     hash2 := GeneratePerceptualHash(grayImg2)      // 3. 计算汉明距离     dist := HammingDistance(hash1, hash2)      println("Hash 1:", hash1)     println("Hash 2:", hash2)     println("Hamming Distance:", dist)      // 根据距离判断是否为重复图片     if dist < 10 { // 阈值需要根据实际情况调整         println("Images are likely duplicates or very similar.")     } else {         println("Images are likely different.")     } } 

注意事项:

  • 上述ResizeAndGrayscale函数是高度简化的,实际应用中需要使用更专业的图像处理库(如github.com/nfnt/resize)进行高质量的缩放和灰度转换。
  • LoadImageFromFile也仅是占位符,需要根据实际图片格式(JPEG, PNG等)使用image/jpeg或image/png库进行解码。

4. 实施考量与注意事项

4.1 阈值设定

汉明距离的阈值是判断图片是否重复的关键。没有一个固定的“最佳”阈值,它取决于你的应用场景对“重复”的定义。

  • 如果阈值设置得太低(例如,0-3),你可能只会检测到完全相同的图片或几乎没有变化的图片。
  • 如果阈值设置得太高(例如,15-20),你可能会将许多不相关的图片误判为重复。 通常,对于8×8的pHash,汉明距离在0-5之间被认为是高度相似,5-10之间是中度相似,超过10则可能不相似。你需要通过实验和测试来找到最适合你需求的阈值。

4.2 局限性

pHash虽然有效,但并非万能。它对以下情况的鲁棒性较差:

  • 大幅度裁剪: 如果图片被大幅裁剪,pHash值可能会显著改变。
  • 旋转: 图片旋转后,即使内容相同,其pHash值也会完全不同。
  • 复杂编辑: 艺术滤镜、显著的颜色反转等复杂编辑会改变图像的整体视觉特征,导致pHash失效。 对于这些更复杂的场景,可能需要结合其他图像特征提取方法(如SIFT、SURF)或深度学习模型。

4.3 性能与扩展

对于包含大量图片的图库,逐一计算汉明距离进行比较的效率会很低。为了提高检索效率,可以考虑以下策略:

  • 索引结构: 将生成的哈希值存储在数据库中。
  • 近似最近邻搜索(ANN): 对于大规模数据集,可以利用Locality Sensitive Hashing (LSH) 或其他基于树的索引结构(如k-d树、Annoy、Faiss)来加速相似哈希值的查找。这些技术可以在牺牲一定准确性的前提下,大幅提高搜索速度。

4.4 其他感知哈希算法

除了pHash,还有其他常用的感知哈希算法,例如:

  • AHash(Average Hash): 最简单的感知哈希,直接计算8×8灰度图的平均值,然后比较每个像素与平均值。
  • DHash(Difference Hash): 比较相邻像素的亮度差异来生成哈希,对图像内容的变化更为敏感,对亮度或对比度调整有较好的鲁棒性。 这些算法在原理上与pHash相似,但在细节处理上有所不同,可能在特定场景下表现更优。

5. 总结

感知哈希(pHash)为图像重复检测提供了一个简单而有效的起点,尤其适用于资源有限或需要从头构建解决方案的场景。通过理解其尺寸缩减、灰度转换、平均值计算、哈希生成和汉明距离比较的工作原理,开发者可以构建出能够识别近似重复图片的系统。在实际应用中,应根据需求调整汉明距离阈值,并考虑在大规模数据集下采用更高效的索引和搜索策略。对于更复杂的图像相似度检测需求,可以进一步探索其他感知哈希算法或更高级的图像特征提取技术。

git go github go语言 编码 ai 深度学习 标准库 字符串 Go语言 github 算法 数据库 faiss

上一篇
下一篇