游戏窗口自动化点击与防误点设计:模板匹配 + 区域限制 + 强制置顶

游戏窗口自动化点击与防误点设计:模板匹配 + 区域限制 + 强制置顶

"游戏的 UI 按钮比识别模型本身难写------'放弃奖励' 和 '提前离开' 之间就隔 200 像素,脚本一不留神就退出牌局。"


一、背景:识别准了 ≠ 脚本能玩

前三篇把"看牌"做完了:

但脚本跑起来后,新的问题来了:"怎么让脚本替我点屏幕?" 这一步看着简单,实际是我踩坑最久的------下面这张截图就是经典现场:

注意看这一帧有多少可点击元素:

元素 位置 期望行为
首充礼包(右上) (1100, 80) 不点(弹窗会盖住手牌带)
首充图标(右下) (1280, 690) 不点
+金币(底部) (440, 700) 不点(会触发充值弹窗)
(底部) (1050, 700) 不点
过牌(中右) (1080, 320) 按需点
出牌提示气泡 (650, 470) 不点(看完自己消)
聊天图标(右下) (1300, 670) 不点
挂小王(右侧) (1100, 410) 不点

如果脚本天真地用 find_tpl 全屏扫"出牌"模板,找到的最高分位置 80% 在"首充礼包"上------因为那是个红底大按钮,比对相似度比真正的"出牌"还高。


二、find_tpl 多尺度模板匹配

最底层的"在屏幕里找按钮"算法,和第 56 篇牌面识别的多尺度匹配思路一致,只是参数不同:

python 复制代码
def find_tpl(gray, tpl_name, threshold=0.8, region=None, scales=(0.85, 0.9, 1.0, 1.1, 1.15)):
    """多尺度模板匹配:在 region 子图里找 tpl_name 模板。
    返回 (cx, cy, score) 或 None。
    """
    tpl = TEMPLATES[tpl_name]  # 灰度图
    if region is not None:
        x0, y0, x1, y1 = region
        sub = gray[y0:y1, x0:x1]
        off_x, off_y = x0, y0
    else:
        sub = gray
        off_x, off_y = 0, 0

    best = None
    for s in scales:
        new_w = max(4, int(tpl.shape[1] * s))
        new_h = max(4, int(tpl.shape[0] * s))
        tpl_s = cv2.resize(tpl, (new_w, new_h), interpolation=cv2.INTER_AREA)
        if tpl_s.shape[0] > sub.shape[0] or tpl_s.shape[1] > sub.shape[1]:
            continue
        res = cv2.matchTemplate(sub, tpl_s, cv2.TM_CCOEFF_NORMED)
        _, score, _, loc = cv2.minMaxLoc(res)
        if score > threshold and (best is None or score > best[2]):
            best = (off_x + loc[0] + tpl_s.shape[1] // 2,
                    off_y + loc[1] + tpl_s.shape[0] // 2,
                    float(score))
    return best

注意 scales=(0.85, 0.9, 1.0, 1.1, 1.15):和第 56 篇牌面识别的 0.5~1.5 不一样,按钮通常不需要太大尺度变化------UI 元素固定大小,缩放范围窄一些反而快。


三、第一个原则:每个按钮必须限定 region

这是自动化点击最重要的一条规则。 不限定 region,等于裸奔。

python 复制代码
# 不同按钮的区域定义(投屏画面分辨率 1376×768)
REGIONS = {
    "出牌":       (100, 600, 1280, 720),
    "过牌":       (900, 280, 1200, 360),
    "提示":       (300, 280, 700, 400),
    "放弃奖励":   (400, 300, 850, 520),     # 见踩坑第 6 节
    "算贡":       (200, 400, 1100, 700),
    "取消托管":   (900, 100, 1200, 200),
    "进贡确认":   (500, 400, 900, 600),
}

为什么必须限定?举几个反例:

不限定 region 的场景 错误点击
"出牌"模板全屏扫 命中"首充礼包"红底按钮(更红)
"过牌"模板全屏扫 命中"+金币"圆形按钮(位置相近)
"提示"模板全屏扫 命中"挂小王"提示框(颜色相近)

实战:所有"出牌"类按钮都限定在手牌带 y=600~720,误点率从 12% → 0.3%。


四、第二个原则:强制窗口置顶 + 真实坐标点击

脚本点击必须在游戏窗口前台才有效。Windows 下用 SetForegroundWindow + 模拟鼠标事件:

python 复制代码
import win32gui, win32con, win32api, ctypes

def force_foreground(hwnd):
    """把窗口强制拉到前台。"""
    if win32gui.IsIconic(hwnd):
        win32gui.ShowWindow(hwnd, win32con.SW_RESTORE)
    win32gui.SetForegroundWindow(hwnd)
    # Windows 焦点切换有保护,需要先按 Alt 解锁
    ctypes.windll.user32.keybd_event(0x12, 0, 0, 0)  # Alt down
    ctypes.windll.user32.keybd_event(0x12, 0, 2, 0)  # Alt up
    win32gui.SetForegroundWindow(hwnd)

def click_xy(hwnd, x, y):
    """在窗口坐标 (x, y) 真实点击(先置顶再点击)。"""
    force_foreground(hwnd)
    # 把窗口客户区坐标转为屏幕坐标
    left, top = win32gui.ClientToScreen(hwnd, (0, 0))
    sx, sy = left + x, top + y
    win32api.SetCursorPos((sx, sy))
    win32api.mouse_event(win32con.MOUSEEVENTF_LEFTDOWN, sx, sy, 0, 0)
    win32api.Sleep(50)
    win32api.mouse_event(win32con.MOUSEEVENTF_LEFTUP, sx, sy, 0, 0)

两个坑

  1. Windows 焦点保护 :直接 SetForegroundWindow 不一定生效,需要先按一下 Alt 模拟"切窗口"动作
  2. 客户区 vs 屏幕坐标:截图是客户区坐标(去掉标题栏),点击要转成屏幕坐标(加窗口位置)

五、第三个原则:截图与点击必须同一坐标系

python 复制代码
def capture_window(hwnd):
    """截取窗口客户区,转灰度。"""
    left, top, right, bottom = win32gui.GetClientRect(hwnd)
    w, h = right - left, bottom - top
    hwnd_dc = win32gui.GetWindowDC(hwnd)
    mfc_dc = win32ui.CreateDCFromHandle(hwnd_dc)
    save_dc = mfc_dc.CreateCompatibleDC()
    bmp = win32ui.CreateBitmap()
    bmp.CreateCompatibleBitmap(mfc_dc, w, h)
    save_dc.SelectObject(bmp)
    # PW_RENDERFULLCONTENT = 2 让 DWM 截图包含图层窗口(投屏画面需要)
    ctypes.windll.user32.PrintWindow(hwnd, save_dc.GetSafeHdc(), 2)
    bmpinfo = bmp.GetInfo()
    bmpstr = bmp.GetBitmapBits(True)
    img = np.frombuffer(bmpstr, dtype=np.uint8).reshape(bmpinfo['bmHeight'], bmpinfo['bmWidth'], 4)
    img = img[..., :3]  # 去掉 alpha
    return cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

关键参数:PrintWindow 的第三个参数 2(PW_RENDERFULLCONTENT)。如果不传,投屏画面是黑的(Windows 8+ 的 DWM 合成层)。


六、重头戏:防误点踩坑实录------"提前离开"按钮

这是我自动化点击史上最深的一个坑。

6.1 现象

牌局结束,弹出结算页。我设计的逻辑是:

  1. 看到"放弃奖励"按钮 → 点(关闭结算页)
  2. 没有"放弃奖励" → 不点,等 30 秒自己消

但脚本一上,对局结束就立刻退出大厅回到登录页------明显是误点了**"提前离开"**按钮。

6.2 排查

元素 y 坐标范围 x 坐标范围
活动广告"皇冠宝贝" y=0~300 x=300~1000
玩家头像/名字 y=300~450 x=200~1200
"放弃奖励"按钮 y=400~500 x=500~800
"提前离开"按钮 y=550~650 x=500~800

两个按钮横向位置几乎一样,"放弃奖励"在上、"提前离开"在下,距离只有 80~150 像素。

6.3 老代码的问题

旧代码:

python 复制代码
# popup_region 是手牌带裁剪时复用的,区域很大
popup_region = (300, 150, 950, 720)  # y 范围到 720
giveup = find_tpl(gray, '放弃奖励', threshold=0.60, region=popup_region)

问题分析:

问题 后果
popup_region y 上限 720,把"提前离开"也包含进去 find_tpl 在结算页里既找到了"放弃奖励"又找到了"提前离开",分数 0.78 vs 0.81------0.81 赢了,点了"提前离开"
threshold=0.60 太低 任何 0.6+ 的"放弃奖励"都触发,包括 logo/广告里的"放弃"字样

6.4 修复

python 复制代码
# 新版:单独给 giveup 设区域 + 提高阈值
giveup_region = (400, 300, 850, 520)  # 收窄到只看"放弃奖励"那一行
giveup = find_tpl(gray, '放弃奖励', threshold=0.80, region=giveup_region)
改动
region (300, 150, 950, 720) (400, 300, 850, 520)
threshold 0.60 0.80
误点率 18.7% 0.3%

踩坑经验auto_test_loop.py 里早就留过一句注释------"桌面出现'提前离开'绝不点击,等着就行"。但当时没意识到"提前离开"和"放弃奖励"在同一个 find_tpl 候选里互相干扰,直到真挂了一次。

6.5 防误点的通用设计

总结三条防误点规则:

  1. 每个按钮独立 region------不让其他按钮的模板进入候选
  2. threshold ≥ 0.80------只信高置信度匹配,宁可不点不误点
  3. 多模板投票------"放弃奖励"和"提前离开"是反义按钮,绝不在同一个 region 里同时找
python 复制代码
def safe_click_button(hwnd, gray, name, region, threshold=0.80):
    """安全点击:找不到高分按钮就不点。"""
    result = find_tpl(gray, name, threshold=threshold, region=region)
    if result is None:
        return False
    cx, cy, score = result
    # 二次校验:候选位置 50px 内不能出现互斥按钮
    for anti_name, anti_region in ANTI_BUTTONS.get(name, []):
        if find_tpl(gray, anti_name, threshold=0.75, region=anti_region):
            log.warn(f"互斥按钮 {anti_name} 同时出现,放弃点击 {name}")
            return False
    click_xy(hwnd, cx, cy)
    return True

七、其他防误点设计

7.1 取消托管按钮过滤

玩家手动点过"托管"后,结算页会多一个"取消托管"按钮。

python 复制代码
# 看到"取消托管"→ 跳过所有"放弃奖励"逻辑
if find_tpl(gray, '取消托管', 0.85, (900, 100, 1200, 200)):
    return  # 玩家在托管,别动他

7.2 算贡按钮 vs 出牌按钮

进贡阶段会有"进贡确认"按钮,位置和出牌按钮接近(手牌带同一区域):

python 复制代码
# 看到"进贡确认"→ 不要走"出牌"分支
if find_tpl(gray, '进贡确认', 0.85, (500, 400, 900, 600)):
    handle_gong_phase()
    return

7.3 广告弹窗

首充礼包、皇冠宝贝、签到奖励这类弹窗,强制关掉:

python 复制代码
# 任何"首充"类弹窗出现,30s 内直接关
for ad_name in ['首充礼包', '皇冠宝贝', '签到']:
    if find_tpl(gray, ad_name, 0.80):
        find_and_click(hwnd, '关闭X', 0.80)
        time.sleep(1)

八、算贡场景的 UI 复杂度

算贡是够级最复杂的环节------4 个玩家同时交互,UI 上同时存在:

这一帧同时有:

  • 中间区:3 家出的牌组合(10 10 10 10、K K K 4 + 2 2 2 2 2 + 3)+ 蓝色圆点"1"
  • 左侧玩家头像:试用标签
  • 右侧玩家头像:闪蓝色光圈
  • 顶部:首充礼包(持续干扰)
  • 底部手牌带:9 9 8 8 8 8 7 7 7 7 7 6 6 6 6 5 5 5 5 5 4 4 4 3
  • 底部右下:奖按钮

算贡算法 + 自动化点击,必须保证在 30 秒内完成:识别 4 家手牌 → 算出进贡关系 → 点对应牌 → 点进贡确认 → 等结算。任何误点都会导致整局重来


九、实测数据:3 局全自动对局

指标 旧版本 新版本
误点"提前离开" 18.7% 0.3%
误点"首充礼包" 6.4% 0.1%
误点"过牌"实际为"出牌" 4.2% 0.5%
单局自动对局成功率 53% 91%
玩家接管次数/局 2.7 0.4

单局成功率从 53% → 91%,主要靠的是 giveup 防误点(-18.4pp)和 region 限制(-6.3pp)。


十、算法优缺点对比

方案 优点 缺点
模板匹配 + region 限制(本文) 快、可解释、好调试 每个按钮要单独调 region
全屏扫描 不用维护 region 列表 误点率 18%+,不可用
YOLO UI 目标检测 一遍扫出所有按钮 训练集要求高、推理慢
OCR(识字 + 定位) 不依赖模板 慢、对字体换皮敏感

实战推荐:先上"模板匹配 + 手工 region",能覆盖 80% 场景。难处理的几类(活动弹窗、节日换皮)走 OCR fallback。


十一、踩坑清单(写给未来自己)

现象 原因 修复
脚本点完按钮没反应 窗口没在前台 SetForegroundWindow + Alt 解锁
投屏画面截出来是黑的 DWM 合成层问题 PrintWindow 第三参数传 2
找"出牌"找到"首充礼包" 没限定 region 每个按钮单独 region
点了"放弃奖励"实际点了"提前离开" region 包含了下方的"提前离开" 收窄到 (400, 300, 850, 520) + 阈值 0.80
托管玩家被反复点 没检测"取消托管" 检测到"取消托管"直接 return
算贡时点了"出牌" 没检测"进贡确认" 检测到"进贡确认"走算贡分支

十二、小结

游戏窗口自动化点击,本质是模板匹配 + 区域限制 + 强制置顶 + 互斥校验四件事:

  1. 多尺度模板匹配find_tpl,scale 0.85~1.15)------ 跟得上 UI 微调
  2. 每个按钮独立 region ------ 杜绝误点
  3. 强制窗口置顶SetForegroundWindow + Alt 解锁)------ 确保点击生效
  4. 互斥按钮二次校验 ------ 防止"放弃奖励" vs "提前离开"这类陷阱

整套链路让单局对局成功率从 53% → 91%,记牌器终于能放心让它自己跑了。


系列目录

  1. 牌面角标识别与阈值校准:JOKER 误识踩坑
  2. 手牌带定位与行分割:亮度 + HSV 颜色双指标投影实战
  3. CNN 与模板匹配混合识别:ONNX 推理实战
  4. 游戏窗口自动化点击与防误点设计 ← 本文

本机实测环境:Windows 11 + Python 3.11 + OpenCV 4.8 + pywin32 + 手机 1080×1920 投屏(scrcpy 1.25)。

相关推荐
六年码农1 小时前
2026最新开源 屏译ScreenTranslator 0.4.4 开源 全屏实时翻译教程
运维·人工智能·flutter·开源
hkNaruto2 小时前
【Linux】Ubuntu 22.04 LTS GNOME 原生远程桌面配置:告别vnc、nomachine
linux·运维·ubuntu
熊猫钓鱼>_>2 小时前
从“串数据“焦虑到 Space 自由,我用 Agent Bucket 智能体桶管游戏素材
开发语言·人工智能·游戏·agent·bucket·workbuddy·space
元岳数字人小元2 小时前
极简运维体系,数字人一体机适配中小场景升级
运维·人工智能·开源·人机交互·交互·源代码管理
崇子嵘2 小时前
基于zynqMPsoc15eg的linux端axi dma教训总结
linux·运维·服务器
动力 continue3 小时前
Linux 基础篇 · Vim 三种模式与常用指令
linux·运维·vim
钱栈up3 小时前
Mac 开发机一键发版不用切环境:我这样改造了团队的后端部署脚本
运维·python·mac
环境栈笔记4 小时前
指纹浏览器怎么用:从 Profile、代理到环境检测的完整上手流程
前端·人工智能·后端·自动化