121 lines
3.3 KiB
Go
121 lines
3.3 KiB
Go
// 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
|
||
}
|