update backend
This commit is contained in:
@ -198,8 +198,8 @@ func DecodeWithType(s string, typ byte) (uint32, error) {
|
||||
//
|
||||
// 前端按类型化路由(/runway-looks、/street-snaps)或存储 type 分流,后端按路由 typ 调 DecodeWithType。
|
||||
const (
|
||||
TypeRunway = 'r' // 走秀 / 文章 lookbook(brand_runway 表)
|
||||
TypeSnap = 's' // 街拍(street_snap 表)
|
||||
TypeRunway = 'r' // 走秀 / 文章 lookbook(brand_runways 表)
|
||||
TypeSnap = 's' // 街拍(street_snaps 表)
|
||||
|
||||
// 图片级前缀:单张图片也要有对外标识(图片收藏用)。
|
||||
// 与图集前缀(r/s)互不冲突,因此同一数字主键在图集与图片下得到不同编码串,
|
||||
|
||||
@ -19,7 +19,7 @@ type Claims struct {
|
||||
UserID uint32
|
||||
Username string
|
||||
Email string
|
||||
// Tier 用户等级 free/vip;详情页据其拼装图片 URL(普通/高清)。
|
||||
// Tier 用户等级 free/vip;仅作账号标记(图片质量模型已统一,不再据此拼装 URL)。
|
||||
Tier string
|
||||
}
|
||||
|
||||
|
||||
@ -90,7 +90,7 @@ func dhash(img image.Image) uint64 {
|
||||
}
|
||||
|
||||
// ToVectorBits 把 64-bit 指纹转成 pgvector 的二进制向量字符串 "[0,1,...,1]"(长度 64),
|
||||
// 用于写入 vector(64) 列,并以 bit_hamming_ops 建 HNSW 索引做汉明近邻检索。
|
||||
// 用于写入 vector(64) 列,并以 vector_l2_ops 建 HNSW 索引做汉明近邻检索(L2²==汉明)。
|
||||
// h 为 0(解码失败)时返回空串,调用方据此存 NULL、不参与检索。
|
||||
func ToVectorBits(h uint64) string {
|
||||
if h == 0 {
|
||||
|
||||
122
internal/pkg/phash/phash_test.go
Normal file
122
internal/pkg/phash/phash_test.go
Normal file
@ -0,0 +1,122 @@
|
||||
package phash
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/png"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// genGradient 生成一张水平灰阶渐变的 PNG 字节流(w 宽、h 高)。
|
||||
// 用于验证 dHash 对「同一图案」的稳定性,以及对「缩放 / 转码」的鲁棒性。
|
||||
func genGradient(w, h int) []byte {
|
||||
img := image.NewRGBA(image.Rect(0, 0, w, h))
|
||||
for y := 0; y < h; y++ {
|
||||
for x := 0; x < w; x++ {
|
||||
v := uint8(x * 255 / w)
|
||||
img.Set(x, y, color.Gray{Y: v})
|
||||
}
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
if err := png.Encode(&buf, img); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
// genGradientReverse 生成一张「左亮右暗」的反向渐变 PNG。
|
||||
// 与 genGradient(左暗右亮)方向相反,dHash 的相邻比较结果完全相反,
|
||||
// 汉明距离应为 64(最大),用于验证「差异显著」的图能被正确区分。
|
||||
func genGradientReverse(w, h int) []byte {
|
||||
img := image.NewRGBA(image.Rect(0, 0, w, h))
|
||||
for y := 0; y < h; y++ {
|
||||
for x := 0; x < w; x++ {
|
||||
v := uint8(255 - x*255/w)
|
||||
img.Set(x, y, color.Gray{Y: v})
|
||||
}
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
if err := png.Encode(&buf, img); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
// genSolid 生成一张纯色 PNG。
|
||||
func genSolid(w, h int, v uint8) []byte {
|
||||
img := image.NewRGBA(image.Rect(0, 0, w, h))
|
||||
c := color.Gray{Y: v}
|
||||
for y := 0; y < h; y++ {
|
||||
for x := 0; x < w; x++ {
|
||||
img.Set(x, y, c)
|
||||
}
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
if err := png.Encode(&buf, img); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
// TestOfIdentical 同一字节流 → 指纹完全一致。
|
||||
func TestOfIdentical(t *testing.T) {
|
||||
a := genGradient(64, 64)
|
||||
b := genGradient(64, 64)
|
||||
if Of(a) != Of(b) {
|
||||
t.Fatalf("相同图片应得到相同 dHash,got %016x vs %016x", Of(a), Of(b))
|
||||
}
|
||||
}
|
||||
|
||||
// TestOfResizeSimilar 同一图案缩放到不同尺寸 → 汉明距离很小(近重复可容忍范围内)。
|
||||
func TestOfResizeSimilar(t *testing.T) {
|
||||
a := genGradient(64, 64)
|
||||
b := genGradient(128, 128)
|
||||
d := Hamming(Of(a), Of(b))
|
||||
if d > 20 {
|
||||
t.Fatalf("同一图案缩放后汉明距离应很小,got %d", d)
|
||||
}
|
||||
}
|
||||
|
||||
// TestOfDifferent 方向相反的图案(左暗右亮 vs 左亮右暗)→ 汉明距离应显著大于近重复阈值。
|
||||
func TestOfDifferent(t *testing.T) {
|
||||
a := genGradient(64, 64)
|
||||
b := genGradientReverse(64, 64)
|
||||
d := Hamming(Of(a), Of(b))
|
||||
if d <= DefaultThreshold {
|
||||
t.Fatalf("不同图片汉明距离应大于阈值 %d,got %d", DefaultThreshold, d)
|
||||
}
|
||||
}
|
||||
|
||||
// TestToVectorBits 验证向量串格式与维度(64 位、方括号包裹、0/1 组成)。
|
||||
func TestToVectorBits(t *testing.T) {
|
||||
s := ToVectorBits(0b1) // 仅最低位为 1
|
||||
if len(s) != 2*Size+1 { // 64 个 0/1 + 63 个逗号 + 2 个方括号 = 129
|
||||
t.Fatalf("向量串长度应为 %d,got %d(%s)", 2*Size+1, len(s), s)
|
||||
}
|
||||
if s[0] != '[' || s[len(s)-1] != ']' {
|
||||
t.Fatalf("向量串应以方括号包裹,got %s", s)
|
||||
}
|
||||
if s[1] != '1' { // bit0 = 1
|
||||
t.Fatalf("第 0 维应为 1,got %c", s[1])
|
||||
}
|
||||
if s[3] != '0' { // bit1 = 0([1,0,...] 中第二个元素)
|
||||
t.Fatalf("第 1 维应为 0,got %c", s[3])
|
||||
}
|
||||
if ToVectorBits(0) != "" {
|
||||
t.Fatalf("解码失败(0)应返回空串,got %q", ToVectorBits(0))
|
||||
}
|
||||
}
|
||||
|
||||
// TestHamming 验证汉明距离的对称性与已知值。
|
||||
func TestHamming(t *testing.T) {
|
||||
if Hamming(0, 0) != 0 {
|
||||
t.Fatalf("相同指纹汉明距离应为 0")
|
||||
}
|
||||
if Hamming(0b1111, 0b0000) != 4 {
|
||||
t.Fatalf("1111 vs 0000 汉明距离应为 4")
|
||||
}
|
||||
if Hamming(0b1111, 0b0000) != Hamming(0b0000, 0b1111) {
|
||||
t.Fatalf("汉明距离应对称")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user