简介:一套面向机器学习初学者的完整实战项目资源,围绕TensorFlow实现梦幻西游游戏中的三类弹窗自动化识别:战斗弹窗需判断四个小人中哪个朝向正面,成语弹窗需根据提示点选对应成语,移动弹窗需选择移动的“x”,项目覆盖从CNN图像分类到目标检测与孪生网络对比的完整流程。压缩包共327个文件,约197.66MB,以152张PNG图像数据、81个Python脚本和50个YAML/YML配置文件为主,同时包含模型权重文件、Dockerfile、Jupyter Notebook与Markdown说明文档,目录清晰,可按照模块复现实验。目前已有1268人学习,适合希望结合实际游戏场景理解卷积神经网络、目标检测和孪生网络的读者。资源不仅提供完整代码与数据,还配套Docker环境配置、训练脚本、模型文件和项目文档,可支撑从数据处理、模型训练到推理验证全流程上手;其中成语弹窗方案采用目标检测定位字块加孪生网络判字,移动弹窗的“x”判定也可迁移到其他UI自动化识别任务中。
1. 机器学习实践:把战斗弹窗、成语弹窗、移动弹窗一次性处理掉
做某国风回合制端游多开挂机时,最烦的从来不是打不过怪,而是弹窗。战斗弹窗卡住自动操作,成语弹窗逼你手动答题,移动弹窗挡在寻路路径上,任何一个没处理就是整晚挂机白费。这三类弹窗的行为差异很大,如果靠图像模板匹配硬怼,每次界面换皮就得重新截一堆图,维护成本高得离谱。标题里的“机器学习实践”指的就是把这三种弹窗的识别、分类、点击做成一条自动闭环,用检测模型找弹窗位置,用OCR读成语内容,再让决策模块决定点哪个按钮。这套方案的受众很清楚:玩回合制多开的普通玩家、做游戏脚本想脱离按键精灵的开发者,以及想练手目标检测加OCR落地的人。适合新手的点在于弹窗样本易采集、模型不必很大、逻辑闭环短,一个周末能跑通。
2. 弹窗识别的整体架构:截图、检测、内容识别、决策点击四条链路
2.1 三种弹窗的形态差异和各自的识别难点
战斗弹窗的特征是结构稳定:周围有深色遮罩,中间一个面板,面板上有“战斗胜利”“战斗失败”或确认按钮。它的文本部分不重要,真正的识别目标是“弹窗区域”和“要点的按钮位置”。难点在按钮颜色在不同战斗状态里会变,金色、灰色、蓝色都出现过,只靠颜色阈值找按钮一定会翻车。
成语弹窗是另一种形态:弹窗里有一个成语或半句话,下面四个选项,要求选同义或补全。这种弹窗的难点不在检测框架,而在文字识别。游戏里的成语文字是美术字,带描边、渐变、背景底纹,直接丢给OCR引擎经常把“一诺千金”读成“一诺干金”。所以成语弹窗的完整链路是:先检测到弹窗坐标,然后对文本区域做预处理,再把识别结果拿去做答案匹配。
移动弹窗和前两种都不一样。它通常出现在点击地图、点击NPC或跨场景时,弹窗内容是询问“是否前往该地点”,按钮是“确定”和“取消”。表面上看它和战斗弹窗长得差不多,但坑在弹窗出现时机不定,可能连续弹两次,也可能带一个地图坐标数字。纯模板匹配很难处理坐标数字的变化,而用OCR把坐标读出来以后,可以结合游戏地图做路径判断。
三种弹窗合并处理时,最省事的方案是统一走目标检测:一个检测模型负责找出所有弹窗面板和所有按钮候选框,然后按照弹窗类型走不同的后处理分支。分支里战斗弹窗和移动弹窗只需要点固定按钮,成语弹窗则额外走一次OCR。这样做的好处是训练数据共用一套标注,新增一种弹窗时不需要改主流程。
2.2 一条最小闭环:从窗口截图到鼠标点击
我一般用这个顺序跑通最小闭环:截取当前游戏窗口图像,把整张图交给检测模型,取回弹窗框和按钮框;判断弹窗类别;如果是成语弹窗,对弹窗文本区域做OCR识别;根据识别结果从成语库中找到正确答案对应的选项按钮;最后模拟鼠标点击按钮坐标。这个闭环在普通台式机上,单次耗时控制在 300 毫秒以内是可行的,其中检测模型推理占一半,OCR占三分之一,剩下是点击延时。
第一步是截图。先确认游戏窗口标题,用系统API拿到窗口句柄和位置,再按窗口尺寸截图。要注意窗口置顶和最小化的边界情况:窗口只要被系统判定为最小化,截到的内容就是黑图或旧画面。我在实践中直接把游戏切成窗口模式,固定分辨率,不最小化,只允许移动到屏幕角落。这样比强行处理最小化窗口省很多坑。
第二步是检测。选用的模型是YOLO系轻量版本,输入尺寸640x640,类别数按实际需要设为三类弹窗面板和四类按钮。一套模型同时输出两个面板候选框和四个按钮候选框,比分开跑三个模型节省显存和推理时间。训练数据量其实不需要特别夸张,我第一批只标了400张图,后面又把错误样本补进去,最终模型在常见场景下的召回率就到95%左右了。
第三步是决策点击。拿到按钮坐标后,要把模型输出的坐标从截图分辨率换算回屏幕坐标。这个换算看起来很基础,却是我踩过最多坑的地方,因为Windows系统缩放、游戏内分辨率缩放、窗口边框阴影都会影响换算结果。换算没问题后直接用鼠标事件点击,点击前最好再截一次图做二次确认,避免弹窗已经消失导致误点。
2.3 四步闭环的代码骨架:先跑通再优化
下面的代码是完整闭环的骨架,我不建议一开始就加线程和队列,先把它当作单次循环跑通,后面再拆。
import time from PIL import ImageGrab import pyautogui import cv2 import numpy as np def grab_window(left, top, width, height): # 按窗口坐标截取游戏画面,int值保证截图范围合法 bbox = (int(left), int(top), int(left + width), int(top + height)) img = ImageGrab.grab(bbox=bbox) return cv2.cvtColor(np.array(img), cv2.COLOR_RGB2BGR) def detect_popup(frame, model): # 检测模型返回弹窗框、按钮框、类别 results = model(frame, conf=0.45, imgsz=640) return results def ocr_text(region): # region是弹窗文本区域的BGR图像 gray = cv2.cvtColor(region, cv2.COLOR_BGR2GRAY) gray = cv2.resize(gray, None, fx=2, fy=2, interpolation=cv2.INTER_CUBIC) _, binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) return ocr_engine.recognize(binary) def click_button(btn_box, offset_x, offset_y): # offset_x/offset_y是窗口在屏幕上的左上角坐标 x = int(btn_box[0] + btn_box[2] // 2 + offset_x) y = int(btn_box[1] + btn_box[3] // 2 + offset_y) pyautogui.click(x, y) time.sleep(0.2)代码逻辑说明:grab_window 负责把窗口区域截成OpenCV格式;detect_popup 中 conf 阈值设 0.45 是为了在漏检和误检之间找平衡,前期样本少时先调低到 0.3,后面再提上来;ocr_text 里先放大两倍再做二值化,这是应对美术字体最基本的一招,放大是为了让OCR引擎更容易切分字符,二值化是为了去掉描边带来的灰度干扰。
参数说明:pyautogui.click 默认走系统级鼠标事件,游戏窗口必须保持在前台,如果游戏有后台点击检测机制,这一步会被拦。点击后的 0.2 秒延时是为了等画面刷新,太快会导致下一次截图拿到半开屏动画。整段骨架代码不要直接拿去挂机,它缺少防误判和断线恢复,但它能帮你验证整条链路有没有打通。
3. 采集与标注:弹窗样本的数量和标注质量决定识别上限
3.1 用自动化脚本批量抓弹窗原图
训练样本的来源是游戏运行时截屏。你不需要专门找人替你打怪,可以写一个脚本自动触发弹窗:进入战斗后等待弹窗出现,自动截图保存,点击按钮关闭弹窗,再重复触发。成语弹窗的触发最简单,找到成语NPC后反复对话,每次弹窗内容都不同,连续挂半小时能积累上百张。
我在采集时做了一个小技巧:截图时把弹窗出现的全过程都存下来,包括弹窗出现前的10帧、弹窗完全展开后的静止帧、点击按钮后的消失帧。前10帧用于理解遮罩层出现的过渡状态,静止帧用于训练主模型,消失帧可以作为负样本。很多开发者只截弹窗完全展开的图,结果模型在弹窗刚出现时检测不到,因为训练数据里根本没有半透明遮罩的半成品状态。
负样本同样重要。游戏中普通战斗界面上有技能栏、血条、小地图,这些区域如果不进负样本,检测模型很容易把技能栏误判成弹窗面板。我一般会在非弹窗状态下随机截50张图,专门标注为“无弹窗”,用于抑制误检。负样本数量不需要和正样本一样多,但一定不能没有。
3.2 标注规范和三类训练目标的拆分
标注工具我习惯用通用的矩形框标注工具,输出格式为YOLO的txt,每行是“类别 x_center y_center width height”,坐标是相对图像的归一化值。标注时有三类弹窗面板和四类按钮,弹窗面板类分别是 battle_panel、idiom_panel、move_panel;按钮类是 confirm_btn、cancel_btn、option_btn、close_btn。
战斗弹窗的面板框要包含整个弹窗主体,不要包含外层的半透明遮罩,因为遮罩的边界会跟着分辨率变化,包含进去等于给模型引入噪声。成语弹窗的面板框要稍微向外扩2像素,把文本部分完整包住,否则OCR取区域时会裁掉上下边。移动弹窗的按钮框标注时注意,确定按钮和取消按钮各标各的,不要合成一个大框,因为点击精度要求不一样。
标注还有一个细节:按钮框不要超过面板框的范围。有些标注时会顺手把按钮框标得很大,结果训练时模型学到按钮框和面板框的尺寸比例关系,一旦面板尺寸变化,按钮框也跟着飘。我在代码里加了一个校验函数,训练前检查每个按钮框是否完全落在某个面板框内,不满足的样本直接报警,不进入训练集。
3.3 样本增强:让模型适应不同画质和界面缩放
样本增强用的主流手段包括亮度扰动、对比度扰动、模糊扰动、随机裁剪和HSV色域扰动。回合制游戏的画面变化相对温和,不需要太激进的数据增强。我一般只用亮度扰动和轻微模糊,增强倍数控制在2倍以内。之前试过大量马赛克增强,结果模型把马赛克本身当成特征,反而在正常画面上表现变差。
增强后的样本总量建议控制在1500到2500张之间。检测模型在这个量级下训练20到30个epoch就能收敛。如果某个弹窗类别样本明显偏少,比如移动弹窗只有100张,我会复制该类别样本并叠加轻微偏移,让框的位置变化一下,而不是简单复制原图。简单复制会导致同一个位置的过拟合,叠加2至4像素的偏移后再放进去,模型学到的位置泛化会好很多。
硬负样本也是一个可以长期累积的资源。每次模型误检,把误检的图像保存下来,下一轮训练加进去。我用一个叫 hard_negative 的目录专门存这些图,标签全部标记为背景类。几轮迭代之后,误检最集中的几个区域会被明显压下去。采集、标注、增强这份工作不容易写出惊艳效果,但它直接决定检测上限,模型结构再花哨也补不了训练集的天花板。
4. 训练检测模型与参数调优:让弹窗框稳定落在按钮上
4.1 从预训练权重开始还是从零训练
弹窗识别属于典型的小目标加结构化界面的检测任务。常见做法是从公开预训练权重开始做迁移学习,而不是从零训练。原因很直接:弹窗面板和按钮在视觉上带有明显的边缘、阴影和纹理,这些低层特征和通用物体特征重叠度很高;从零训练需要更多数据才能学到可靠的边缘检测器。
开发阶段我用的训练配置是YOLO系小型模型,输入分辨率640x640,batch size 16,初始学习率0.01。加载预训练权重后冻结前10层,只训练后面的特征融合层和检测头。冻结层数在数据量小时特别有效,因为浅层特征和通用视觉特征相近,冻结它们能防止少量游戏截图把模型带偏。等第一个版本跑通后,再解冻全部层微调10个epoch,此时因为前一轮训练已经稳定,微调风险很低。
训练完成后导出为适合CPU推理的格式。在CPU上单张640x640的推理时间大约在70到110毫秒,完全够用。如果还想压缩,可以把输入分辨率降到416,检测框精度会略有下降,但对点击按钮这个精度要求不高的场景可以接受。
4.2 三个必调参数:置信度阈值、IoU阈值和训练轮数
置信度阈值直接影响漏检和误检的平衡。我最初的教训是设置0.5,结果战斗弹窗有时候半个面板被遮住,置信度降到0.35,模型直接漏检。后来改成动态策略:正常运行用0.45,连续三轮截图都检测不到弹窗时自动降到0.25。降低阈值会带进来几个误检框,但弹窗识别可以接受“先多检再过滤”的思路。
IoU阈值用于控制重复框合并。弹窗面板和按钮的尺寸差异很大,按钮框很小,默认的合并阈值容易把相邻按钮框合并成一个框。我会把合并阈值调到0.5以上,确保“确定”和“取消”两个相邻按钮不会被合并。如果模型输出里同一按钮出现两个框,后处理会按置信度保留一个,另一个直接丢弃。这个逻辑在尝试过的所有版本里都稳。
训练轮数方面,我建议分两个阶段:第一阶段25个epoch,第二阶段解冻全部层再训练10个epoch。加起来35个epoch,总训练时间在普通单卡上可以控制在半小时到一小时。超过这个轮数容易出现验证集mAP不升反降的情况,模型开始记住训练样本里的干扰纹理。观察训练日志时,重点关注新人物的鼠标指针和技能特效是否引起误检,这类现象一旦出现就说明模型已经过拟合了。
4.3 成语弹窗的OCR识别:预处理比模型更关键
成语弹窗OCR不需要一开始就上重型模型。先把文本区域图像做预处理:放大2到3倍,转灰度,用自适应阈值处理底纹,再对每个字符做轮廓分析。很多情况下,预处理做好后一个轻量识别引擎读出来的结果就很接近准确答案了。
预处理完成后,OCR识别出的文字不能直接用。游戏字体是繁体还是简体、有没有异体字、识别置信度如何,都需要判断。我在项目里保留了一个成语库,大约收录常见的数千条成语,并把每一条转成拼音首字母和逐字索引。OCR输出后先做模糊匹配,把相似度最高的三个候选成语返回给决策逻辑,而不是只信第一结果。
如果OCR把成语读错,比如“杯水车薪”读成“杯水新”,模糊匹配也能通过拼音首字母对齐。这个技巧是成语弹窗实践里的关键救命稻草:不追求每个字都识别对,只要声母部分基本正确,匹配库就能找回正确答案。移动弹窗里有坐标数字,OCR的容错空间小一些,需要识别出数字再做范围判断。坐标识别失败时,决策逻辑默认点取消,绝不点确定,因为点错位置比不点的损失大得多。
5. 弹窗识别避坑:五种高频翻车现场与排查顺序
5.1 检测框位置乱跳:同一弹窗被识别成三个不同位置
现象是战斗弹窗明明停在那,模型输出的面板框忽左忽右,按钮框跟着整个面板来回飘,点击经常点在按钮边缘。
原因有两个:第一个是弹窗出现时有位移动画,模型把动画中间帧当成了标准状态,框的中心学歪了;第二个是训练样本中弹窗并非都处在屏幕正中央,有些样本里弹窗靠左,有些靠右,模型没有学到“面板是居中元素”这个先验。
解决办法是数据清洗:把弹窗展开超过0.5秒后的静止帧选为主要训练样本,动画中间帧只保留10%左右作为增强。同时我改变了标注策略,准确性比覆盖更优先,凡是框边缘明显越过面板视觉边界的样本全部重新标,宁可不要这张图也不让框的标注质量拖低模型精度。
5.2 美术字把OCR识别率打成玄学
现象是检测正常,按钮位置也对,但OCR把成语读错,导致答案永远选不对。比如“相见恨晚”被读成“相贝恨晚”,“恨”字左右结构被拆开,二值化后变成一个“艮”加一个竖心旁。
原因在于二值化阈值没处理好。成语文本带描边和渐变,简单全局阈值会把描边也变成前景,字符内部却因为渐变出现空洞。空洞又导致字符轮廓分裂,OCR引擎无法正确切分。
解决方法是改用自适应阈值,并把核心参数调整为适合细笔画字体。对每次OCR都同时输出一个置信度,置信度过低时放弃识别,改用模板匹配历史样本里的同位置成语。这个方法虽然“土”,但在难以识别的字体上,比硬调OCR参数更可靠。
5.3 屏幕缩放导致点击偏移:坐标换算必须带上DPI缩放
现象是检测框在截图里和按钮完美重叠,但点击位置偏上一大截,点到了按钮背景上。
原因是Windows显示缩放不是100%时,截图坐标系和鼠标事件坐标系之间多了一个缩放系数。比如屏幕缩放150%,游戏窗口实际像素如果小于逻辑像素,鼠标点击用的坐标就得按缩放比例换算。
解决方法是先读取系统的缩放参数,把窗口区域和按钮坐标统一到一个坐标系里再点击。代码里不要写死缩放系数,应该每次启动时读取当前缩放值。这个坑最容易在换电脑或者外接显示器后复现,日志里要打印当前使用的缩放系数,方便排查。
5.4 弹窗外层遮罩干扰检测结果
现象是模型在弹窗出现早期能检测到,弹窗完全展开后反而检测不到了;或者面板框把整个遮罩区域框进去,导致后续OCR取到一片黑区域,识别结果为空。
原因是训练样本里混入了遮罩层的标注。遮罩是一个半透明层,视觉上就像面板周围有一圈暗色区域。模型把遮罩当作面板背景,等遮罩颜色变化或透明度改变时,它就不知道往哪里框了。
解决方法是把遮罩视为“弹窗出现的辅助信号”,不作为标注目标。标注面板框时严格限定在不透明面板上,遮罩部分即使在视觉上连续,也不准纳入标注框。另外我加了一道后处理:当面板框面积超过屏幕三分之一时,把它判定为遮罩误检,忽略这次结果,等待下一帧。
5.5 战斗弹窗按钮颜色变化导致漏检
现象是大多数时候能点对按钮,某些特殊战斗状态下按钮变成灰色,模型直接检测不到按钮框。
原因是训练样本中按钮颜色单一,模型学到了“亮色才是按钮”的假规律。灰色按钮特征和背景太接近,模型把它当背景处理了。
解决方法是采集不同按钮状态的样本:可点击状态、冷却状态、灰色禁用状态、高亮状态各收集一批。标注时所有状态的按钮都标成同一类别,让模型学到按钮的轮廓和内部结构,而不是颜色。如果某个状态确实难采到,可以在增强时对按钮区域做灰度变换,手动制造灰按钮样本。
6. 让识别系统更稳:动态阈值、热键暂停和动作日志回放
最后一层优化不需要再动模型,而是把工程细节补齐。第一个建议是给置信度阈值加一条动态逻辑:正常时保持0.45,检测不到弹窗时降到0.25,连续两帧都检测不到就把画面保存下来作为待排查样本。这逼着系统把失败场景变成下一轮训练数据。
第二个建议是启动系统前注册一组热键。我用F8暂停、F9继续、F10立即截图保存。暂停功能看着简单,但挂机场景里一旦误点或者网络掉线,人不可能秒切窗口去救。有热键暂停,风险瞬间可控。
第三个建议是动作日志回放。每轮循环把截图缩略图、检测结果、OCR文本、点击坐标、最终置信度写进一条日志。出问题时,看日志能快速定位是检测漏了还是OCR读错还是坐标换算出问题。这是整条链路里让我少操很多心的部分,强烈建议一开始就写。
我现在的习惯是每完成一轮工具更新,先挂着观察两小时,把日志里所有没法解释的点击拉出来过一遍,找共同特征再补样本。这个过程看着琐碎,却是系统稳定性的真正来源。希望这套弹窗识别的思路和踩坑记录,能帮你在做同类自动化识别时少走几段弯路。
本文还有配套的精品资源,点击获取