Files
deskview/internal/vision/hash.go
T
2026-10-28 17:39:33 +08:00

121 lines
3.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Package vision 提供帧差指纹、图片编码与(可选的)本地检测接口。
package vision
import (
"image"
"golang.org/x/image/draw"
)
// detailSize 是细节网格的边长。相比 8×8 的感知哈希,这份高分辨率灰度
// 网格专门用来捕捉"布局没变、只是文字/局部内容变了"这类细粒度变化——
// 题目换了但背景一样时,粗哈希几乎不动,细节网格的平均差却会明显跳变。
const detailSize = 64
// Fingerprint 是用于判断"画面有没有变化"的感知哈希。
type Fingerprint struct {
AHash uint64 // 均值哈希(8×8,抗噪、粗筛)
DHash uint64 // 差分哈希(8×8,抗噪、粗筛)
Mean uint8 // 平均亮度
Detail []byte // 细节灰度网格(detailSize×detailSize),用于细粒度内容差
}
// grayGrid 把图像缩放到 w×h 的灰度图。
func grayGrid(src image.Image, w, h int) *image.Gray {
dst := image.NewGray(image.Rect(0, 0, w, h))
draw.ApproxBiLinear.Scale(dst, dst.Bounds(), src, src.Bounds(), draw.Src, nil)
return dst
}
// Of 计算图像指纹。
func Of(img image.Image) Fingerprint {
if img == nil {
return Fingerprint{}
}
small := grayGrid(img, 8, 8)
var sum, n int
for _, v := range small.Pix {
sum += int(v)
n++
}
avg := sum / max(n, 1)
mean := uint8(min(avg, 255))
var aHash uint64
for i, v := range small.Pix {
if int(v) > avg {
aHash |= 1 << uint(i)
}
}
wide := grayGrid(img, 9, 8)
var dHash uint64
for y := 0; y < 8; y++ {
for x := 0; x < 8; x++ {
l := wide.GrayAt(x, y).Y
r := wide.GrayAt(x+1, y).Y
if l > r {
dHash |= 1 << uint(y*8+x)
}
}
}
// 细节网格:把画面缩到 detailSize×detailSize 的灰度图,
// 用于和上一帧做逐像素的归一化平均差(MAD),比感知哈希灵敏得多。
detailGrid := grayGrid(img, detailSize, detailSize)
detail := make([]byte, len(detailGrid.Pix))
copy(detail, detailGrid.Pix)
return Fingerprint{AHash: aHash, DHash: dHash, Mean: mean, Detail: detail}
}
// Distance 返回两个指纹的汉明距离(取两种哈希的较大值,偏向灵敏)。
func (f Fingerprint) Distance(o Fingerprint) int {
return max(bitsSet(f.AHash^o.AHash), bitsSet(f.DHash^o.DHash))
}
// DetailDiff 返回两份细节网格的归一化平均绝对差(0~1)。
// 它衡量"逐像素平均变了多少",对文字替换、局部内容更新这类变化远比对
// 比哈希敏感,是提升画面更新识别灵敏度的主力信号。
func (f Fingerprint) DetailDiff(o Fingerprint) float64 {
a, b := f.Detail, o.Detail
if len(a) == 0 || len(b) == 0 || len(a) != len(b) {
return 0
}
var sum int
for i := range a {
d := int(a[i]) - int(b[i])
if d < 0 {
d = -d
}
sum += d
}
return float64(sum) / (float64(len(a)) * 255.0)
}
// Changed 判断画面是否发生了足以触发提问的变化。
// 任一信号超过阈值即判定为变化:感知哈希(抗噪粗筛)命中,或细节网格的
// 归一化平均差超过 sensitivity(细粒度内容变化)。
func (f Fingerprint) Changed(o Fingerprint, threshold int, sensitivity float64) bool {
if threshold <= 0 {
threshold = 5
}
if sensitivity <= 0 {
sensitivity = 0.01
}
if f.Distance(o) > threshold {
return true
}
return f.DetailDiff(o) > sensitivity
}
func bitsSet(v uint64) int {
n := 0
for v != 0 {
v &= v - 1
n++
}
return n
}