// Package phash 计算图片的感知哈希(dHash,Difference Hash)。 // // dHash 是 64-bit 轻量指纹,用于判断「同一张图被转码 / 加水印 / 改尺寸」这类近重复。 // 去重检索不在这里做——交给 PostgreSQL + pgvector 的 HNSW 索引(索引随全库结构一起, // 由 cmd/dbtool 导出的 SQL 维护),本包只负责产出指纹,复杂度极低、零 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) 列,并以 vector_l2_ops 建 HNSW 索引做汉明近邻检索(L2²==汉明)。 // 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 }