first commit
This commit is contained in:
commit
3553790475
27 files changed
+3839
No files matched your search
@@ -0,0 +1,25 @@
|
||||
package vision
|
||||
|
||||
import (
|
||||
"context"
|
||||
"image"
|
||||
)
|
||||
|
||||
// Detection 是一条本地检测结果。
|
||||
type Detection struct {
|
||||
Label string
|
||||
Score float32
|
||||
Box image.Rectangle
|
||||
}
|
||||
|
||||
// Detector 是本地视觉检测器(例如 YOLO)的抽象。
|
||||
//
|
||||
// 第一版不启用:屏幕理解的准确率由视觉大模型直接读取截图来保证,
|
||||
// 帧差指纹负责"要不要问"。二期若需要隐私打码或题目区域定位,
|
||||
// 实现该接口挂到 agent 上即可,主链路不用改。
|
||||
type Detector interface {
|
||||
// Name 返回检测器名称,用于日志。
|
||||
Name() string
|
||||
// Detect 对整帧图像做检测。
|
||||
Detect(ctx context.Context, img image.Image) ([]Detection, error)
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package vision
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/jpeg"
|
||||
|
||||
"golang.org/x/image/draw"
|
||||
)
|
||||
|
||||
// EncodeJPEG 把图像等比缩放到长边不超过 maxEdge 后编码为 JPEG。
|
||||
// 视觉模型对分辨率不敏感,但对 token 很敏感,这一步是成本控制的关键。
|
||||
func EncodeJPEG(img image.Image, maxEdge, quality int) ([]byte, error) {
|
||||
if img == nil {
|
||||
return nil, fmt.Errorf("待编码图像为空")
|
||||
}
|
||||
if maxEdge <= 0 {
|
||||
maxEdge = 1280
|
||||
}
|
||||
if quality <= 0 || quality > 100 {
|
||||
quality = 75
|
||||
}
|
||||
|
||||
b := img.Bounds()
|
||||
w, h := b.Dx(), b.Dy()
|
||||
if w <= 0 || h <= 0 {
|
||||
return nil, fmt.Errorf("图像尺寸无效: %dx%d", w, h)
|
||||
}
|
||||
|
||||
target := img
|
||||
if longest := max(w, h); longest > maxEdge {
|
||||
scale := float64(maxEdge) / float64(longest)
|
||||
nw, nh := max(int(float64(w)*scale+0.5), 1), max(int(float64(h)*scale+0.5), 1)
|
||||
dst := image.NewRGBA(image.Rect(0, 0, nw, nh))
|
||||
draw.CatmullRom.Scale(dst, dst.Bounds(), img, b, draw.Src, nil)
|
||||
target = dst
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := jpeg.Encode(&buf, target, &jpeg.Options{Quality: quality}); err != nil {
|
||||
return nil, fmt.Errorf("JPEG 编码失败: %w", err)
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
// Package vision 提供帧差指纹、图片编码与(可选的)本地检测接口。
|
||||
package vision
|
||||
|
||||
import (
|
||||
"image"
|
||||
|
||||
"golang.org/x/image/draw"
|
||||
)
|
||||
|
||||
// Fingerprint 是用于判断"画面有没有变化"的感知哈希。
|
||||
type Fingerprint struct {
|
||||
AHash uint64 // 均值哈希
|
||||
DHash uint64 // 差分哈希
|
||||
Mean uint8 // 平均亮度
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Fingerprint{AHash: aHash, DHash: dHash, Mean: mean}
|
||||
}
|
||||
|
||||
// Distance 返回两个指纹的汉明距离(取两种哈希的较大值,偏向灵敏)。
|
||||
func (f Fingerprint) Distance(o Fingerprint) int {
|
||||
return max(bitsSet(f.AHash^o.AHash), bitsSet(f.DHash^o.DHash))
|
||||
}
|
||||
|
||||
// Changed 判断画面是否发生了足以触发提问的变化。
|
||||
func (f Fingerprint) Changed(o Fingerprint, threshold int) bool {
|
||||
if threshold <= 0 {
|
||||
threshold = 5
|
||||
}
|
||||
return f.Distance(o) > threshold
|
||||
}
|
||||
|
||||
func bitsSet(v uint64) int {
|
||||
n := 0
|
||||
for v != 0 {
|
||||
v &= v - 1
|
||||
n++
|
||||
}
|
||||
return n
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package vision
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/jpeg"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func solid(w, h int, c color.Gray) *image.Gray {
|
||||
img := image.NewGray(image.Rect(0, 0, w, h))
|
||||
for y := 0; y < h; y++ {
|
||||
for x := 0; x < w; x++ {
|
||||
img.SetGray(x, y, c)
|
||||
}
|
||||
}
|
||||
return img
|
||||
}
|
||||
|
||||
func TestFingerprintIdentical(t *testing.T) {
|
||||
a := solid(120, 80, color.Gray{Y: 40})
|
||||
b := solid(120, 80, color.Gray{Y: 40})
|
||||
if d := Of(a).Distance(Of(b)); d != 0 {
|
||||
t.Fatalf("相同画面应无差异,实际距离 %d", d)
|
||||
}
|
||||
if Of(a).Changed(Of(b), 5) {
|
||||
t.Fatal("相同画面不应判定为变化")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFingerprintDetectsChange(t *testing.T) {
|
||||
base := solid(120, 80, color.Gray{Y: 20})
|
||||
changed := image.NewGray(image.Rect(0, 0, 120, 80))
|
||||
for y := 0; y < 80; y++ {
|
||||
for x := 60; x < 120; x++ {
|
||||
changed.SetGray(x, y, color.Gray{Y: 250})
|
||||
}
|
||||
}
|
||||
if !Of(changed).Changed(Of(base), 5) {
|
||||
t.Fatal("右半边变白应被判定为变化")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncodeJPEGResizesLongEdge(t *testing.T) {
|
||||
src := solid(2000, 400, color.Gray{Y: 128})
|
||||
raw, err := EncodeJPEG(src, 1280, 75)
|
||||
if err != nil {
|
||||
t.Fatalf("编码失败: %v", err)
|
||||
}
|
||||
if len(raw) == 0 {
|
||||
t.Fatal("编码结果为空")
|
||||
}
|
||||
img, err := jpeg.Decode(bytes.NewReader(raw))
|
||||
if err != nil {
|
||||
t.Fatalf("解码失败: %v", err)
|
||||
}
|
||||
b := img.Bounds()
|
||||
if b.Dx() != 1280 {
|
||||
t.Fatalf("长边应缩放到 1280,实际宽 %d", b.Dx())
|
||||
}
|
||||
if b.Dy() != 256 {
|
||||
t.Fatalf("等比缩放后高应为 256,实际 %d", b.Dy())
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncodeJPEGKeepsSmallImage(t *testing.T) {
|
||||
src := solid(64, 32, color.Gray{Y: 200})
|
||||
raw, err := EncodeJPEG(src, 1280, 75)
|
||||
if err != nil {
|
||||
t.Fatalf("编码失败: %v", err)
|
||||
}
|
||||
img, err := jpeg.Decode(bytes.NewReader(raw))
|
||||
if err != nil {
|
||||
t.Fatalf("解码失败: %v", err)
|
||||
}
|
||||
if img.Bounds().Dx() != 64 {
|
||||
t.Fatalf("小图不应放大,实际宽 %d", img.Bounds().Dx())
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user