基于图像识别的明日方舟自动化助手:模板匹配实战与避坑指南
简介这是一份面向明日方舟玩家的自动化辅助工具源码基于图像识别技术实现日常任务的一键执行适合具备C基础、对计算机视觉与游戏自动化感兴趣的中高级开发者研究学习。资源包共2000个文件约124.87MB以1487个json配置数据、190个h头文件、162个cpp源文件为主辅以hpp、py脚本、go与dart代码及yaml、md文档覆盖任务调度、战斗识别、基建生产、自动招募等模块。已有344人学习下载。读者可从中获取完整的图像识别落地思路包括屏幕元素捕捉、灰度化与降噪预处理、特征提取及分类器训练并借助OpenCV与多线程并行计算提升识别效率同时能参考任务触发条件、执行步骤与结果验证的完整逻辑设计理解光照、角度、分辨率变化下的算法优化与反作弊规避思路适合作为C图像识别项目实战的参考范例。1. 明日方舟日常太肝图像识别助手把重复操作压成一次点击基建换班、刷理智、领任务奖励、公招识别这些事单看每件只要几十秒但一天点下来手指是真的酸。更麻烦的是它们高度重复没有任何策略乐趣纯粹是消耗时间。我最初的想法很简单能不能让程序看着屏幕自己判断当前在哪个界面然后点该点的按钮这就是「明日方舟游戏助手」要解决的问题——基于图像识别技术实现一键完成日常任务。它适合两类人一是每天必清日常但不想手动点的老玩家二是想拿一个真实项目练图像识别落地的新手。核心逻辑不复杂截屏、模板匹配、模拟点击循环执行。但真做起来分辨率适配、界面误判、点击偏移这些坑一个都不会少。下面把我自己跑通的方案拆开讲从环境搭到参数调再到翻车记录尽量让你少走弯路。2. 图像识别驱动点击方案选型与最小可跑框架2.1 为什么选模板匹配而不是深度学习标题里写的是「图像识别技术」热搜词里还挂着「深度学习图像识别」很多人第一反应是上 YOLO 或者 CNN 分类器。我一开始也这么想后来发现方向错了。明日方舟的日常任务界面有几个特点UI 元素固定、按钮样式统一、背景变化小。这种场景下模板匹配的准确率和速度都碾压深度学习方案。具体对比一下。深度学习方案需要标注数据、训练模型、调参一套下来没有几天搞不定而且换分辨率还得重新标注。模板匹配只需要截几张图当模板OpenCV 的matchTemplate直接跑单次匹配在 1080P 下通常 10 到 30 毫秒。对于「找到按钮就点」这种需求模板匹配是性价比最高的选择。那深度学习什么时候用当你要识别的是动态内容比如公招标签的 OCR 识别、关卡掉落的物品图标分类这些用模板匹配就不行了。我的方案是混合的界面导航和按钮点击用模板匹配文字识别用 OCR物品识别才考虑轻量分类模型。这样既保证了速度又覆盖了全部日常任务。提示不要一上来就追求「全深度学习」先把模板匹配跑通你会发现 80% 的操作都能覆盖。2.2 环境搭建与依赖安装我用的技术栈是 Python OpenCV ADB。ADB 负责截屏和模拟点击OpenCV 负责图像匹配。这套组合在 Windows 和 macOS 上都能跑Linux 也行但 ADB 驱动偶尔需要额外配置。先装依赖pip install opencv-python numpy pillowADB 需要单独下载 Android Platform Tools解压后把路径加到系统环境变量里。验证 ADB 是否可用adb version adb devicesadb devices应该列出你的设备或模拟器。如果是模拟器常见端口是 5555 或 62001具体看模拟器设置。真机需要开启 USB 调试。这里有个细节截屏方式有两种。一种是adb exec-out screencap -p直接输出 PNG 到标准输出速度快但部分设备兼容性差。另一种是adb shell screencap存到设备再拉回来稳但慢。我一般先用第一种如果花屏或截出来是黑屏再换第二种。import subprocess import cv2 import numpy as np def screenshot(fastTrue): if fast: result subprocess.run( [adb, exec-out, screencap, -p], capture_outputTrue ) img_array np.frombuffer(result.stdout, dtypenp.uint8) else: subprocess.run([adb, shell, screencap, -p, /sdcard/screen.png]) subprocess.run([adb, pull, /sdcard/screen.png, ./screen.png]) img_array np.fromfile(./screen.png, dtypenp.uint8) img cv2.imdecode(img_array, cv2.IMREAD_COLOR) return img这段代码做了两件事通过 ADB 拿到屏幕截图然后用 OpenCV 解码成 numpy 数组。fastTrue走exec-out通道适合大多数情况如果返回的图是空的或者花屏把fast改成False走文件传输通道。注意np.frombuffer和np.fromfile的区别前者从内存缓冲区读后者从磁盘文件读别搞混。2.3 模板匹配的核心参数与点击映射模板匹配的原理很简单拿一张小图模板在大图截图上滑动计算每个位置的相似度找到最相似的位置。OpenCV 提供了cv2.matchTemplate返回一个相似度矩阵再用cv2.minMaxLoc找到最大值的位置。def find_template(screen, template_path, threshold0.8): template cv2.imread(template_path) result cv2.matchTemplate(screen, template, cv2.TM_CCOEFF_NORMED) min_val, max_val, min_loc, max_loc cv2.minMaxLoc(result) if max_val threshold: h, w template.shape[:2] center_x max_loc[0] w // 2 center_y max_loc[1] h // 2 return (center_x, center_y, max_val) return NoneTM_CCOEFF_NORMED是归一化相关系数匹配返回值在 -1 到 1 之间越接近 1 越相似。threshold是阈值低于这个值就认为没找到。我一般设 0.8 起步如果误判多就往上调到 0.85 或 0.9如果漏判多就降到 0.75。max_loc是匹配区域的左上角坐标加上模板宽高的一半就是中心点这个中心点就是我们要点击的位置。点击用 ADB 的input tapdef tap(x, y): subprocess.run([adb, shell, input, tap, str(x), str(y)])看起来很简单对吧但这里有个血泪经验不同设备的屏幕分辨率不一样模板是在 1080P 下截的换到 720P 设备上匹配就会失败。解决办法有两个一是按分辨率准备多套模板二是把截图和模板都缩放到统一尺寸再匹配。我选的是第二种后面避坑章节会详细讲。3. 日常任务流水线从基建收菜到刷理智的完整实现3.1 任务调度器状态机驱动的界面导航日常任务不是一条直线走到底的。你可能在主页点进基建收完菜回到主页再点进作战刷完理智又回到主页。这种流程用状态机来描述最自然每个界面是一个状态每个操作是状态之间的转移。我定义了一个简单的状态机class TaskRunner: def __init__(self): self.state home self.max_retry 3 def run(self): while True: screen screenshot() if self.state home: self.handle_home(screen) elif self.state base: self.handle_base(screen) elif self.state combat: self.handle_combat(screen) else: break def handle_home(self, screen): # 优先检查基建入口 pos find_template(screen, templates/base_icon.png) if pos: tap(pos[0], pos[1]) self.state base return # 检查作战入口 pos find_template(screen, templates/combat_icon.png) if pos: tap(pos[0], pos[1]) self.state combat return # 都没找到可能弹了公告点关闭 pos find_template(screen, templates/close_btn.png) if pos: tap(pos[0], pos[1])这个调度器的逻辑是每次循环截一张图根据当前状态决定下一步操作。handle_home里按优先级检查各个入口找到就点进去并切换状态。如果都没找到可能是弹了公告或者网络卡了尝试点关闭按钮。这里的关键是「优先级」和「兜底」。优先级决定了先检查哪个入口兜底决定了找不到任何已知元素时怎么办。我一般会加一个「未知界面」的处理截一张图存下来方便事后分析是哪个界面没覆盖到。3.2 基建换班识别干员疲劳与自动替换基建是日常任务里最繁琐的部分。你需要检查每个设施里干员的疲劳度疲劳了就得换人。用图像识别做这件事核心是识别两个东西设施图标和干员头像。设施图标用来定位当前在哪个设施干员头像用来判断是谁、疲劳度如何。疲劳度的识别比较麻烦因为那个进度条是动态的。我的做法是不直接识别疲劳度数值而是识别「疲劳」这个状态图标。当干员疲劳时头像上会出现一个明显的标记用模板匹配找到这个标记就知道该换人了。def check_fatigue(screen, facility_region): # 在设施区域内查找疲劳标记 fatigue_icon cv2.imread(templates/fatigue_mark.png) region screen[facility_region[1]:facility_region[3], facility_region[0]:facility_region[2]] result cv2.matchTemplate(region, fatigue_icon, cv2.TM_CCOEFF_NORMED) _, max_val, _, max_loc cv2.minMaxLoc(result) if max_val 0.75: return True, (max_loc[0] facility_region[0], max_loc[1] facility_region[1]) return False, Nonefacility_region是设施在屏幕上的区域用左上角和右下角坐标表示。先裁剪出这个区域再匹配比全屏匹配快很多也减少了误判。阈值设 0.75 是因为疲劳标记比较小设太高容易漏。换人的逻辑是点击疲劳干员进入干员选择界面选一个未疲劳的干员确认。这里有个坑干员列表可能很长需要滑动。我的做法是只选列表前几个因为通常前排都是低星干员用来换班正好。3.3 刷理智关卡选择与代理指挥的自动化刷理智是日常任务里最耗时的部分但自动化反而最简单因为流程固定选关卡、点开始、等结算、点继续。唯一需要注意的是「代理指挥」是否勾选以及理智是否够。def farm_stage(screen, stage_name): # 找到关卡入口 pos find_template(screen, ftemplates/stage_{stage_name}.png) if not pos: return False tap(pos[0], pos[1]) time.sleep(1) # 检查代理指挥是否勾选 screen screenshot() proxy_pos find_template(screen, templates/proxy_check.png) if not proxy_pos: # 没勾选就点一下 proxy_btn find_template(screen, templates/proxy_btn.png) if proxy_btn: tap(proxy_btn[0], proxy_btn[1]) # 点开始 start_pos find_template(screen, templates/start_btn.png) if start_pos: tap(start_pos[0], start_pos[1]) return True return Falsestage_name是关卡代号比如1-7或CE-5。模板文件名对应关卡按钮的截图。proxy_check是勾选状态的截图proxy_btn是未勾选状态的截图。先检查是否已勾选没勾选才点。刷完一局后会出现结算界面点继续回到关卡选择然后循环。这里要加一个理智检查如果理智不够点开始会弹提示识别到这个提示就停止刷图。注意刷理智的循环一定要加超时和最大次数限制否则理智用完后程序会卡在结算界面无限循环。3.4 公招识别与自动领取奖励公招识别是唯一需要 OCR 的地方。公招标签是文字模板匹配搞不定。我用的是pytesseract配合图像预处理提高识别率。import pytesseract def recognize_recruit_tags(screen, region): # 裁剪标签区域 tags_img screen[region[1]:region[3], region[0]:region[2]] # 转灰度 gray cv2.cvtColor(tags_img, cv2.COLOR_BGR2GRAY) # 二值化 _, binary cv2.threshold(gray, 150, 255, cv2.THRESH_BINARY) # OCR text pytesseract.image_to_string(binary, langchi_sim) tags [t.strip() for t in text.split() if t.strip()] return tagsregion是标签区域的坐标。先转灰度去掉颜色干扰再二值化让文字更清晰最后用pytesseract识别。langchi_sim指定简体中文需要提前安装中文语言包。识别出标签后根据标签组合判断该选什么。比如「高级资深干员」必选「资深干员」加「输出」也是好组合。这部分逻辑可以写死也可以做成配置文件。领取奖励就简单了找到「领取」按钮点一下如果有多个奖励就循环点直到按钮消失。4. 避坑指南分辨率、误判与点击偏移的排查记录4.1 现象换设备后所有模板都匹配失败原因模板是在 1080P 下截的新设备是 720P 或 2K像素尺寸对不上matchTemplate自然找不到。解决统一缩放。在匹配前把截图缩放到模板的基准分辨率或者把模板缩放到截图的分辨率。我选的是前者因为截图缩放一次就行模板不用动。def resize_to_base(screen, base_width1920): h, w screen.shape[:2] if w base_width: return screen scale base_width / w new_h int(h * scale) return cv2.resize(screen, (base_width, new_h))base_width是模板截取时的屏幕宽度我一般用 1920。缩放后所有模板都能正常匹配。注意点击坐标也要按比例换算回去否则点偏。4.2 现象明明按钮在屏幕上但匹配不到原因可能是按钮有动态效果比如呼吸灯、高亮闪烁导致截图和模板不完全一致。也可能是按钮被部分遮挡比如弹了个小窗。解决准备多张模板。同一个按钮截 3 到 5 张不同状态的图匹配时只要有一张命中就算找到。另外匹配前先检查有没有弹窗有弹窗先关弹窗。def find_template_multi(screen, template_paths, threshold0.8): for path in template_paths: pos find_template(screen, path, threshold) if pos: return pos return Nonetemplate_paths是模板路径列表按优先级排列。这个函数会依次尝试每个模板返回第一个匹配成功的结果。4.3 现象点击位置总是偏一点点不到按钮原因ADB 的input tap坐标是相对于屏幕的但截图可能有状态栏或导航栏导致截图坐标和屏幕坐标不一致。另外模板匹配返回的是左上角坐标如果直接点左上角就会点到按钮边缘。解决一是确认截图是否包含状态栏如果包含点击时要减去状态栏高度。二是点击时用中心点不要用左上角。三是加一个偏移量校准手动测几次把偏差值写死。def tap_with_offset(x, y, offset_x0, offset_y0): subprocess.run([adb, shell, input, tap, str(x offset_x), str(y offset_y)])offset_x和offset_y是校准偏移量不同设备可能不一样。我一般先在主页测一个按钮点几次看偏差多少然后把值填进去。4.4 现象程序跑着跑着就卡住了不知道在哪个界面原因状态机没有覆盖所有可能的界面遇到未知界面就卡住了。或者网络延迟导致界面加载慢截图截到了加载中的画面。解决加超时和未知界面处理。每次操作后等待一段时间再截图如果连续几次都识别不到已知元素就截一张图存下来然后尝试点返回或关闭按钮。def handle_unknown(screen, retry_count): if retry_count 3: cv2.imwrite(funknown_{time.time()}.png, screen) # 尝试点返回 subprocess.run([adb, shell, input, keyevent, 4]) return True return Falseretry_count是连续未知次数超过 3 次就存图并点返回键。存图是为了事后分析看看是哪个界面没覆盖到。4.5 现象OCR 识别公招标签总是出错原因游戏字体比较特殊pytesseract默认模型识别率不高。另外标签背景有颜色二值化阈值没调好会导致文字断裂或粘连。解决一是用--psm参数调整识别模式公招标签是单行文字用--psm 7比默认模式准。二是二值化阈值多试几个找到文字最清晰的那个。三是可以自己训练一个简单的 OCR 模型但成本较高先用调参解决。text pytesseract.image_to_string( binary, langchi_sim, config--psm 7 -c tessedit_char_whitelist高级资深干员输出防护治疗辅助术师狙击重装医疗先锋近卫特种 )--psm 7表示把图像当作单行文本处理。tessedit_char_whitelist是白名单限制只识别这些字符能显著提高准确率。5. 进阶技巧用多尺度匹配和日志回放把成功率拉到 95% 以上模板匹配最大的问题是尺度不变性差。同一个按钮在不同分辨率下大小不一样直接匹配就会失败。虽然前面说了统一缩放但缩放本身会引入模糊降低匹配精度。更好的做法是多尺度匹配把模板缩放到多个尺寸分别匹配取最高分。def multi_scale_match(screen, template_path, scalesNone, threshold0.8): if scales is None: scales [0.8, 0.9, 1.0, 1.1, 1.2] template cv2.imread(template_path) best_match None best_score 0 for scale in scales: h, w template.shape[:2] new_size (int(w * scale), int(h * scale)) resized cv2.resize(template, new_size) if resized.shape[0] screen.shape[0] or resized.shape[1] screen.shape[1]: continue result cv2.matchTemplate(screen, resized, cv2.TM_CCOEFF_NORMED) _, max_val, _, max_loc cv2.minMaxLoc(result) if max_val best_score: best_score max_val best_match (max_loc[0] new_size[0] // 2, max_loc[1] new_size[1] // 2, max_val) if best_score threshold: return best_match return Nonescales是缩放比例列表我一般用 0.8 到 1.2步长 0.1。每个比例都匹配一次记录最高分。这样即使模板和实际按钮尺寸有偏差也能找到。代价是匹配时间变成原来的 5 倍但单次匹配也就几十毫秒完全可以接受。另一个技巧是日志回放。每次操作都记录截图、匹配结果、点击坐标存成结构化日志。出问题的时候把日志里的截图拿出来重新跑一遍匹配看看是哪个环节出了问题。这个习惯帮我省了大量调试时间。import json import time def log_action(action, screen, result): log_entry { time: time.time(), action: action, result: result, screen_path: flogs/screen_{int(time.time())}.png } cv2.imwrite(log_entry[screen_path], screen) with open(logs/actions.jsonl, a) as f: f.write(json.dumps(log_entry, ensure_asciiFalse) \n)action是操作名称result是匹配结果screen_path是截图路径。用 JSONL 格式追加写入方便后续分析。截图会占磁盘记得定期清理。最后说一个我踩过的坑不要试图用一套模板适配所有服务器。不同服务器的 UI 可能有细微差别比如按钮颜色、字体渲染。我的做法是按服务器分目录存模板启动时根据当前界面自动判断是哪个服务器然后加载对应的模板集。这套方案跑下来日常任务的成功率能稳定在 95% 以上。剩下的 5% 主要是网络波动和游戏更新导致的 UI 变化前者靠重试解决后者靠更新模板解决。我现在的习惯是每次游戏大版本更新后先跑一遍日志模式把新界面的截图都存下来更新模板库再跑自动模式。这个习惯让我很少翻车。希望帮到你。本文还有配套的精品资源点击获取