
在构建图片库或相册网站时,检测并管理重复图片是一个常见需求。传统的哈希算法(如md5、sha-256)对数据的任何微小改动都极其敏感,即使是像素级的变化也会导致哈希值完全不同,因此不适用于图像的相似度检测。而感知哈希(perceptual hash, phash)则是一种能够根据图像的视觉内容生成“指纹”的算法。即使图像经过缩放、压缩、颜色调整等轻微修改,其phash值也能保持高度相似,从而实现对近似重复图像的识别。
pHash的核心思想在于:通过一系列降维和特征提取步骤,将图像的视觉特征编码成一个紧凑的二进制字符串(哈希值)。当需要比较两张图片时,只需计算它们pHash值之间的汉明距离(Hamming Distance),距离越小,图片相似度越高。
感知哈希的实现通常遵循以下几个核心步骤:
为了简化图像数据并去除高频细节(这些细节通常对图像识别的干扰较大),首先将原始图像缩放到一个非常小的尺寸,例如8x8像素或32x32像素。同时,将图像转换为灰度图,进一步减少数据维度,只关注亮度信息。这个小尺寸的灰度图像包含了原始图像的低频信息,即其主要视觉特征。
对缩减并灰度化后的图像中的所有像素值(亮度值)求平均。这个平均值将作为后续步骤中区分像素亮度的基准。
遍历缩减后的灰度图像中的每一个像素。将每个像素的亮度值与步骤二中计算出的平均值进行比较:
将所有这些二进制位按顺序拼接起来,就得到了该图像的感知哈希指纹(例如,对于8x8的图像,会生成一个64位的二进制字符串)。
要判断两张图片是否相似,只需计算它们各自的pHash值之间的汉明距离。汉明距离是指两个等长二进制字符串中对应位置上不同位的数量。
例如: Hash A: 10110100 Hash B: 10100101 汉明距离为2(第4位和第8位不同)。
汉明距离越小,表示两个哈希值越相似,进而说明对应的两张图片在视觉上越接近。
以下是使用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.")
}
}
注意事项:
汉明距离的阈值是判断图片是否重复的关键。没有一个固定的“最佳”阈值,它取决于你的应用场景对“重复”的定义。
pHash虽然有效,但并非万能。它对以下情况的鲁棒性较差:
对于包含大量图片的图库,逐一计算汉明距离进行比较的效率会很低。为了提高检索效率,可以考虑以下策略:
除了pHash,还有其他常用的感知哈希算法,例如:
感知哈希(pHash)为图像重复检测提供了一个简单而有效的起点,尤其适用于资源有限或需要从头构建解决方案的场景。通过理解其尺寸缩减、灰度转换、平均值计算、哈希生成和汉明距离比较的工作原理,开发者可以构建出能够识别近似重复图片的系统。在实际应用中,应根据需求调整汉明距离阈值,并考虑在大规模数据集下采用更高效的索引和搜索策略。对于更复杂的图像相似度检测需求,可以进一步探索其他感知哈希算法或更高级的图像特征提取技术。
以上就是图像重复检测:从感知哈希(pHash)开始构建的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号