You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于Ebitengine实现图像点击检测的优化方案咨询

问题

我正在使用Ebitengine(Golang)开发一个趣味展示项目:点击人体器官图像的对应部位,该部位图像会消失。参考官方教程时,开发者建议使用alphaImage(*image.Alpha)替代ebiten.Image进行光标交互检测,因为(*ebiten.Image).At方法从GPU读取像素速度极慢,应尽量避免。但我的场景中,所有器官图像尺寸、位置完全相同,该方案无法直接复用官方思路。目前我采用的方案是每次点击时遍历所有图像,检测光标位置的Alpha值是否非零来判断点击部位。现咨询是否存在更优方案,或是必须调整各图像的尺寸与位置?

当前实现代码如下:

package main

import (
    "fmt"
    "image/color"
    _ "image/png"
    "log"

    "github.com/hajimehoshi/ebiten/v2"
    "github.com/hajimehoshi/ebiten/v2/ebitenutil"
)

// var example_img *ebiten.Image
var body *ebiten.Image
var digest *ebiten.Image
var head *ebiten.Image
var heart *ebiten.Image
var lung *ebiten.Image

var parts map[string]*ebiten.Image
var hide map[string]bool

func init() {
    var errs []error
    var temp_err error

    // https://ebitengine.org/en/tour/image.html
    body, _, temp_err = ebitenutil.NewImageFromFile("img/body.png")
    errs = append(errs, temp_err)
    digest, _, temp_err = ebitenutil.NewImageFromFile("img/digest.png")
    errs = append(errs, temp_err)
    head, _, temp_err = ebitenutil.NewImageFromFile("img/head.png")
    errs = append(errs, temp_err)
    heart, _, temp_err = ebitenutil.NewImageFromFile("img/heart.png")
    errs = append(errs, temp_err)
    lung, _, temp_err = ebitenutil.NewImageFromFile("img/lung.png")
    errs = append(errs, temp_err)

    parts = map[string]*ebiten.Image{
        "body":   body,
        "digest": digest,
        "head":   head,
        "heart":  heart,
        "lung":   lung,
    }
    hide = map[string]bool{
        "body":   false,
        "digest": false,
        "head":   false,
        "heart":  false,
        "lung":   false,
    }

    is_err := false
    for _, err := range errs {
        if err != nil {
            log.Fatal(err)
            is_err = true
        }
    }

    if is_err {
        log.Fatal("Error loading images")
    }

    /*
        var err error
        example_img, _, err = ebitenutil.NewImageFromFile("img/model.png")
        if err != nil {
            log.Fatal(err)
        }
    */
}

type Game struct{}

// Tick, called 60 times per second (default)
func (g *Game) Update() error {
    if ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) {
        x, y := ebiten.CursorPosition()
        part := CheckPart(x, y)
        fmt.Printf("Mouse position: x=%d, y=%d, part=%s\n", x, y, part)

        if part != "None" {
            hide[part] = true
        }
    }

    return nil
}

// Called every frame (depending on the monitor refresh rate).
// Screen argument is the final destination of rendering.
// A ebitenutil.DebugPrint is a utility function to render a debug message on an image.
func (g *Game) Draw(screen *ebiten.Image) {
    // Print debug message on the screen
    // ebitenutil.DebugPrint(screen, "Hello, World!")

    // Fill the screen with a color
    screen.Fill(color.RGBA{0xff, 0xff, 0xff, 0xff})

    // Draw the image on the screen
    // screen.DrawImage(example_img, nil)
    for name, part := range parts {
        if !hide[name] {
            screen.DrawImage(part, nil)
        }
    }
}

// Layout accepts an outside size, which is a window size on desktop, and returns the game's logical screen size.
// Layout will be more meaningful e.g., when the window is resizable.
func (g *Game) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int) {
    return 640, 480
}

func main() {
    ebiten.SetWindowSize(640, 480)
    ebiten.SetWindowTitle("Demo")

    // Run the main loop of an Ebitengine game
    if err := ebiten.RunGame(&Game{}); err != nil {
        log.Fatal(err)
    }
}

func CheckPart(posX int, posY int) string {
    // Check alpha value of all parts at the given position
    // If alpha value is not 0, return the part name
    // Otherwise, return an empty string

    for name, part := range parts {
        // Get the color of the pixel at the given position
        _, _, _, alpha := part.At(posX, posY).RGBA()

        // Check if the alpha value is not 0
        if alpha != 0 {
            return name
        }
    }

    return "None"
}
优化方案

不需要调整图像尺寸与位置,以下几种方案可以显著提升检测性能:

1. 预构建CPU端Alpha缓存(最直接的优化)

核心思路是提前将每个ebiten.Image转换为CPU端的*image.Alpha对象并缓存,检测时直接读取CPU内存中的Alpha值,避免频繁从GPU拉取像素数据。

修改后的关键代码:

import (
    // ... 原有导入
    "image/draw"
)

var partsAlpha map[string]*image.Alpha // 新增CPU端Alpha缓存

func init() {
    // ... 原有图像加载逻辑
    
    // 预构建每个器官的Alpha缓存
    partsAlpha = make(map[string]*image.Alpha)
    for name, img := range parts {
        bounds := img.Bounds()
        alphaImg := image.NewAlpha(bounds)
        draw.Draw(alphaImg, bounds, img, bounds.Min, draw.Src)
        partsAlpha[name] = alphaImg
    }
    
    // ... 原有错误处理逻辑
}

func CheckPart(posX int, posY int) string {
    // 先过滤超出图像范围的点击
    for name, alphaImg := range partsAlpha {
        imgBounds := alphaImg.Bounds()
        if posX < imgBounds.Min.X || posX >= imgBounds.Max.X ||
           posY < imgBounds.Min.Y || posY >= imgBounds.Max.Y {
            continue
        }
        // 直接读取CPU端的Alpha值
        alpha := alphaImg.AlphaAt(posX, posY).A
        if alpha != 0 && !hide[name] { // 跳过已隐藏的器官
            return name
        }
    }
    return "None"
}

2. 颜色编码碰撞图(最高效的检测方案)

制作一张与屏幕尺寸完全相同的碰撞掩码图,给每个器官区域分配唯一的纯色(无透明度)。点击时只需读取该图对应坐标的颜色,即可直接映射到目标器官,无需遍历所有图像。

实现步骤:

  1. 制作碰撞图:用不同纯色填充每个器官区域(例如head用#FF0000、heart用#00FF00),背景用#000000或其他无关颜色。
  2. 加载并缓存碰撞图到CPU端的*image.RGBA对象。
  3. 点击时通过颜色匹配快速定位器官。

关键代码示例:

var collisionMap *image.RGBA

func init() {
    // ... 原有图像加载逻辑
    
    // 加载碰撞图并转换为CPU端格式
    collisionImg, _, err := ebitenutil.NewImageFromFile("img/collision.png")
    if err != nil {
        log.Fatal(err)
    }
    bounds := collisionImg.Bounds()
    collisionMap = image.NewRGBA(bounds)
    draw.Draw(collisionMap, bounds, collisionImg, bounds.Min, draw.Src)
}

func CheckPart(posX int, posY int) string {
    imgBounds := collisionMap.Bounds()
    if posX < imgBounds.Min.X || posX >= imgBounds.Max.X ||
       posY < imgBounds.Min.Y || posY >= imgBounds.Max.Y {
        return "None"
    }
    
    // 读取碰撞图对应坐标的颜色
    c := collisionMap.At(posX, posY).(color.RGBA)
    // 根据颜色匹配对应器官
    switch {
    case c.R == 255 && c.G == 0 && c.B == 0:
        return "head"
    case c.R == 0 && c.G == 255 && c.B == 0:
        return "heart"
    case c.R == 0 && c.G == 0 && c.B == 255:
        return "lung"
    case c.R == 255 && c.G == 255 && c.B == 0:
        return "digest"
    case c.R == 255 && c.G == 0 && c.B == 255:
        return "body"
    default:
        return "None"
    }
}

3. 优化遍历顺序(低成本小优化)

由于器官存在层级覆盖关系(例如head在body上方),可以将上层器官放在遍历顺序的前面,一旦检测到命中就立即返回,减少不必要的检测次数。同时跳过已隐藏的器官,进一步减少计算量。

修改后的关键代码:

// 用有序切片替代map,按器官层级从上到下排列
var parts []struct {
    Name string
    Img  *ebiten.Image
}

func init() {
    // ... 原有图像加载逻辑
    
    // 按层级排序,上层器官在前
    parts = []struct {
        Name string
        Img  *ebiten.Image
    }{
        {"head", head},
        {"heart", heart},
        {"lung", lung},
        {"digest", digest},
        {"body", body},
    }
    
    // 同步初始化hide map
    hide = make(map[string]bool)
    for _, p := range parts {
        hide[p.Name] = false
    }
    
    // ... 预构建Alpha缓存(同方案1)
}

func CheckPart(posX int, posY int) string {
    for _, p := range parts {
        if hide[p.Name] {
            continue // 跳过已隐藏的器官
        }
        alphaImg := partsAlpha[p.Name]
        imgBounds := alphaImg.Bounds()
        if posX < imgBounds.Min.X || posX >= imgBounds.Max.X ||
           posY < imgBounds.Min.Y || posY >= imgBounds.Max.Y {
            continue
        }
        if alphaImg.AlphaAt(posX, posY).A != 0 {
            return p.Name
        }
    }
    return "None"
}

内容的提问来源于stack exchange,提问作者TaihouKai

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.19 02:18:11