1
This commit is contained in:
@@ -32,6 +32,10 @@ type LocalAi struct {
|
||||
OverlapThreshold float64
|
||||
// 提交前整图等比缩放到的最长边(RF-DETR 对小图更稳)
|
||||
InputSize int
|
||||
// 四级漏斗切片级(技术设计.md「预标注四级漏斗」):切片检测置信度下限 / 块长边(原图像素)/ 相邻块重叠比
|
||||
TileThreshold float64
|
||||
TileSize int
|
||||
TileOverlap float64
|
||||
}
|
||||
|
||||
// Detection RF-DETR 单目标检测结果;Detect 返回前已映射为原图像素左上角(X/Y)+ 宽高
|
||||
@@ -57,6 +61,9 @@ func LocalAiClient(ctx context.Context) *LocalAi {
|
||||
ConfConfirmed: g.Cfg().MustGet(ctx, "localAi.confConfirmed", 0.5).Float64(),
|
||||
OverlapThreshold: g.Cfg().MustGet(ctx, "localAi.overlapThreshold", 0.3).Float64(),
|
||||
InputSize: g.Cfg().MustGet(ctx, "localAi.inputSize", 700).Int(),
|
||||
TileThreshold: g.Cfg().MustGet(ctx, "localAi.tileThreshold", 0.12).Float64(),
|
||||
TileSize: g.Cfg().MustGet(ctx, "localAi.tileSize", 1024).Int(),
|
||||
TileOverlap: g.Cfg().MustGet(ctx, "localAi.tileOverlap", 0.25).Float64(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,10 +129,52 @@ func QwenVL(ctx context.Context, imgData []byte, mime, prompt string) (string, e
|
||||
// imgW/imgH 为原图尺寸;返回坐标均为原图像素尺度。
|
||||
func (c *LocalAi) Detect(ctx context.Context, data []byte, mime string, imgW, imgH int) ([]*Detection, error) {
|
||||
sub, scale := c.prepare(data, mime, imgW, imgH)
|
||||
return c.submitDetect(ctx, sub, mime, scale, 0, 0, c.Threshold)
|
||||
}
|
||||
|
||||
// DetectRegion 对原图中指定像素矩形区域做检测(四级漏斗切片级/VLM 候选区精修):
|
||||
// 裁剪 → 等比缩放至最长边 InputSize 提交 → 坐标映射回原图像素。
|
||||
// threshold 由调用方给定(切片级用低于全图的阈值,小目标置信度天然偏低);
|
||||
// region 越界内部钳制到图片边界。返回坐标均为原图像素尺度。
|
||||
func (c *LocalAi) DetectRegion(ctx context.Context, data []byte, mime string, imgW, imgH int, rx, ry, rw, rh int, threshold float64) ([]*Detection, error) {
|
||||
rx = maxInt(0, minInt(rx, imgW-1))
|
||||
ry = maxInt(0, minInt(ry, imgH-1))
|
||||
rw = minInt(rw, imgW-rx)
|
||||
rh = minInt(rh, imgH-ry)
|
||||
if rw <= 0 || rh <= 0 {
|
||||
return nil, nil
|
||||
}
|
||||
src, _, err := image.Decode(bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("解码图片失败: %w", err)
|
||||
}
|
||||
cropper, ok := src.(interface {
|
||||
SubImage(image.Rectangle) image.Image
|
||||
})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("图片格式不支持区域裁剪")
|
||||
}
|
||||
sub := cropper.SubImage(image.Rect(rx, ry, rx+rw, ry+rh))
|
||||
var buf bytes.Buffer
|
||||
if strings.Contains(mime, "png") {
|
||||
_ = png.Encode(&buf, sub)
|
||||
} else {
|
||||
_ = jpeg.Encode(&buf, sub, &jpeg.Options{Quality: 92})
|
||||
}
|
||||
region, scale := c.prepare(buf.Bytes(), mime, rw, rh)
|
||||
return c.submitDetect(ctx, region, mime, scale, float64(rx), float64(ry), threshold)
|
||||
}
|
||||
|
||||
// submitDetect 提交检测请求并解析:sub 为已缩放的提交图字节,scale 为提交图→原图(或区域)
|
||||
// 的缩放比,offX/offY 为提交图坐标系到原图坐标系的偏移(全图 0,0,区域检测为区域左上角),
|
||||
// threshold 为本次候选置信度下限。响应 x/y 是框中心:先转左上角,再按比例尺映射回原图像素。
|
||||
// 不做中心转左上会把中心当角点,下游再加 w/2 时整框偏移半宽半高
|
||||
// (实测 RNPHE_133: 响应(403,664) 即目标中心,旧逻辑落库 cx 偏 +w/2)。
|
||||
func (c *LocalAi) submitDetect(ctx context.Context, sub []byte, mime string, scale, offX, offY, threshold float64) ([]*Detection, error) {
|
||||
body, err := json.Marshal(map[string]any{
|
||||
"model": c.Model,
|
||||
"image": fmt.Sprintf("data:%s;base64,%s", mime, base64.StdEncoding.EncodeToString(sub)),
|
||||
"threshold": c.Threshold,
|
||||
"threshold": threshold,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -154,20 +203,60 @@ func (c *LocalAi) Detect(ctx context.Context, data []byte, mime string, imgW, im
|
||||
if err := json.Unmarshal(raw, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 响应 x/y 是框中心:先转左上角,再从提交图比例尺映射回原图像素。
|
||||
// 不做此转换会把中心当左上角,下游再加 w/2 时整框偏移半宽半高
|
||||
// (实测 RNPHE_133: 响应(403,664) 即目标中心,旧逻辑落库 cx 偏 +w/2)。
|
||||
for _, d := range out.Detections {
|
||||
d.X -= d.Width / 2
|
||||
d.Y -= d.Height / 2
|
||||
d.X /= scale
|
||||
d.Y /= scale
|
||||
d.X = d.X/scale + offX
|
||||
d.Y = d.Y/scale + offY
|
||||
d.Width /= scale
|
||||
d.Height /= scale
|
||||
}
|
||||
return out.Detections, nil
|
||||
}
|
||||
|
||||
// TileRegion 滑窗切片块(原图像素)
|
||||
type TileRegion struct {
|
||||
X, Y, W, H int
|
||||
}
|
||||
|
||||
// TileRegions 滑窗切片网格(四级漏斗 L2):tileSize 为块长边(原图像素),tileOverlap 为
|
||||
// 相邻块重叠比(步长 = tileSize*(1-tileOverlap)),从左上到右下枚举,边缘块贴边收口保证全覆盖。
|
||||
func TileRegions(imgW, imgH, tileSize int, tileOverlap float64) []TileRegion {
|
||||
if tileSize <= 0 {
|
||||
tileSize = imgW
|
||||
}
|
||||
if tileSize > imgW {
|
||||
tileSize = imgW
|
||||
}
|
||||
if tileSize > imgH {
|
||||
tileSize = imgH
|
||||
}
|
||||
step := maxInt(1, int(float64(tileSize)*(1-tileOverlap)))
|
||||
tiles := make([]TileRegion, 0, (imgW/step+1)*(imgH/step+1))
|
||||
for y := 0; y < imgH; y += step {
|
||||
ty := y
|
||||
if ty+tileSize > imgH {
|
||||
ty = imgH - tileSize
|
||||
}
|
||||
th := minInt(tileSize, imgH-ty)
|
||||
for x := 0; x < imgW; x += step {
|
||||
tx := x
|
||||
if tx+tileSize > imgW {
|
||||
tx = imgW - tileSize
|
||||
}
|
||||
tw := minInt(tileSize, imgW-tx)
|
||||
tiles = append(tiles, TileRegion{X: tx, Y: ty, W: tw, H: th})
|
||||
if tx+tw >= imgW {
|
||||
break
|
||||
}
|
||||
}
|
||||
if ty+th >= imgH {
|
||||
break
|
||||
}
|
||||
}
|
||||
return tiles
|
||||
}
|
||||
|
||||
// prepare 整图等比缩放至最长边 InputSize(等比,不裁剪),返回提交字节与缩放比(原图/提交图)。
|
||||
func (c *LocalAi) prepare(data []byte, mime string, imgW, imgH int) ([]byte, float64) {
|
||||
if imgW <= 0 || imgH <= 0 || imgW <= c.InputSize && imgH <= c.InputSize {
|
||||
|
||||
Reference in New Issue
Block a user