update
This commit is contained in:
126
internal/pkg/phash/phash.go
Normal file
126
internal/pkg/phash/phash.go
Normal file
@ -0,0 +1,126 @@
|
||||
// Package phash 计算图片的感知哈希(dHash,Difference Hash)。
|
||||
//
|
||||
// dHash 是 64-bit 轻量指纹,用于判断「同一张图被转码 / 加水印 / 改尺寸」这类近重复。
|
||||
// 去重检索不在这里做——交给 PostgreSQL + pgvector 的 HNSW 索引(见 database 包的
|
||||
// ensureDedupSchema),本包只负责产出指纹,复杂度极低、零 CGO、零 ML 依赖。
|
||||
//
|
||||
// 与之前被移除的全表扫描实现不同:现在指纹只写库、检索交给索引,O(1) 桶内比对即可,
|
||||
// 图片量级再大也扛得住。
|
||||
package phash
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"image"
|
||||
_ "image/gif"
|
||||
_ "image/jpeg"
|
||||
_ "image/png"
|
||||
"math"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Size 是哈希位数(9x8 灰度 → 8 行 × 8 差 = 64 bit)。
|
||||
const Size = 64
|
||||
|
||||
// DefaultThreshold 汉明距离阈值:≤ 该值判为近重复。可按真实样本微调。
|
||||
const DefaultThreshold = 10
|
||||
|
||||
// Of 计算 data(jpeg/png/gif 字节流)的 dHash 指纹。
|
||||
// 解码失败返回 0,调用方据此把 phash 置空(NULL,不参与近邻检索),不阻断入库。
|
||||
func Of(data []byte) uint64 {
|
||||
img, _, err := image.Decode(bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return dhash(img)
|
||||
}
|
||||
|
||||
const (
|
||||
dw = 9 // 灰度矩阵宽度(列数)
|
||||
dh = 8 // 灰度矩阵高度(行数)
|
||||
)
|
||||
|
||||
// dhash 把图像缩到 9x8 灰度,逐行比较相邻像素亮度,得到 64-bit 指纹。
|
||||
func dhash(img image.Image) uint64 {
|
||||
b := img.Bounds()
|
||||
scaleX := float64(b.Dx()) / float64(dw)
|
||||
scaleY := float64(b.Dy()) / float64(dh)
|
||||
|
||||
gray := make([][]float64, dh)
|
||||
for y := 0; y < dh; y++ {
|
||||
gray[y] = make([]float64, dw)
|
||||
y0 := int(float64(y) * scaleY)
|
||||
y1 := int(math.Ceil(float64(y+1) * scaleY))
|
||||
if y1 <= y0 {
|
||||
y1 = y0 + 1
|
||||
}
|
||||
for x := 0; x < dw; x++ {
|
||||
x0 := int(float64(x) * scaleX)
|
||||
x1 := int(math.Ceil(float64(x+1) * scaleX))
|
||||
if x1 <= x0 {
|
||||
x1 = x0 + 1
|
||||
}
|
||||
var sum float64
|
||||
var n int
|
||||
for sy := y0; sy < y1 && sy < b.Dy(); sy++ {
|
||||
for sx := x0; sx < x1 && sx < b.Dx(); sx++ {
|
||||
r, g, bl, _ := img.At(b.Min.X+sx, b.Min.Y+sy).RGBA()
|
||||
// 感知亮度(ITU-R BT.601)
|
||||
lum := 0.299*float64(r>>8) + 0.587*float64(g>>8) + 0.114*float64(bl>>8)
|
||||
sum += lum
|
||||
n++
|
||||
}
|
||||
}
|
||||
if n > 0 {
|
||||
gray[y][x] = sum / float64(n)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var hash uint64
|
||||
i := 0
|
||||
for y := 0; y < dh; y++ {
|
||||
for x := 0; x < dw-1; x++ {
|
||||
if gray[y][x] > gray[y][x+1] {
|
||||
hash |= 1 << uint(i)
|
||||
}
|
||||
i++
|
||||
}
|
||||
}
|
||||
return hash
|
||||
}
|
||||
|
||||
// ToVectorBits 把 64-bit 指纹转成 pgvector 的二进制向量字符串 "[0,1,...,1]"(长度 64),
|
||||
// 用于写入 vector(64) 列,并以 bit_hamming_ops 建 HNSW 索引做汉明近邻检索。
|
||||
// h 为 0(解码失败)时返回空串,调用方据此存 NULL、不参与检索。
|
||||
func ToVectorBits(h uint64) string {
|
||||
if h == 0 {
|
||||
return ""
|
||||
}
|
||||
var sb strings.Builder
|
||||
sb.Grow(Size + 1)
|
||||
sb.WriteByte('[')
|
||||
for i := 0; i < Size; i++ {
|
||||
if i > 0 {
|
||||
sb.WriteByte(',')
|
||||
}
|
||||
if (h>>uint(i))&1 == 1 {
|
||||
sb.WriteByte('1')
|
||||
} else {
|
||||
sb.WriteByte('0')
|
||||
}
|
||||
}
|
||||
sb.WriteByte(']')
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// Hamming 返回两个指纹的汉明距离(不同 bit 数)。仅在 Go 侧需要手工比对时使用;
|
||||
// 检索场景直接交给 pgvector 的 <~> 算子。
|
||||
func Hamming(a, b uint64) int {
|
||||
c := 0
|
||||
x := a ^ b
|
||||
for x != 0 {
|
||||
x &= x - 1
|
||||
c++
|
||||
}
|
||||
return c
|
||||
}
|
||||
Reference in New Issue
Block a user