
简介面向想用 Python 实践计算机视觉与自动化控制的初学者和进阶者这套“跳一跳”辅助项目围绕屏幕截图识别、方块距离计算与模拟点击展开帮助理解 Pillow、OpenCV、pyautogui 在一个小游戏场景中的协作方式。压缩包共 279 个文件约 26.76MB包含 217 个 png 截图与素材、36 个 json 配置、7 个 md 说明、6 个 py 脚本并附带 adb、fastboot 等 exe/dll 工具文件便于按模块查看源码、配置与运行依赖。已有 319 人学习适合对照项目结构梳理截图区域设定、目标识别、跳跃力度估算和点击节奏控制等实现思路也能从中了解多机型配置、容错处理与调试排错方法。资源侧重技术学习与代码研读不建议直接用于违反平台规则的实际游戏操作。1. 为什么我会去写一个跳一跳的脚本第一次看到有人用 Python 玩跳一跳我以为它靠录屏回放拆开才发现完全不是——每一跳它都在现场计算抓一帧截图找出棋子在哪、目标方块中心在哪用两点距离乘一个系数得到手指按屏的毫秒数。整套东西不到三百行却把移动端自动化的几件基本事截屏、图像识别、事件注入串了一遍拿它当入门项目比写玩具 demo 划算得多。它适合两类人想学 Python 图像处理又不想啃理论书的以及想给自己的 App 做自动化测试、不知道从哪下手的。下面按我实际拆解的路径走一遍代码都能直接抄。2. 整体链路从 adb 抓一帧到手指离开屏幕这个项目最容易被误解的地方是它根本不管游戏的状态。它不清楚你现在在第几关、下一个方块是什么形状它只做一件事看一帧、算一次、按一下然后重复。正因为整体流程足够简单才适合拿来拆解移动端自动化的底座。整条链路拆成四个环节之后每个环节都能单独替换、单独调参出问题时也能很快判断是哪一段出了问题。2.1 四个环节每个都能单独换掉把流程摊开是这样的环节输入输出典型耗时截屏无一张 PNG 或 ndarray150~400ms定位截图棋子底部坐标、目标中心坐标20~60ms换算两个坐标按压毫秒数1ms注入毫秒数一次 swipe 事件按压时长 50ms四个环节里截屏和注入依赖安卓调试桥adb但两者是解耦的定位和换算完全在本地进程里跑。这个边界划分很重要因为它意味着你可以把截屏换成投屏协议、把注入换成sendevent识别逻辑一行都不用动。后面第 6 章讲二次开发就是从这个边界下手的。耗时上还有一点要知道一整轮下来大约 300~600ms再加上等待落地动画的 1 秒多实际每秒最多跳一次。想提速只能压缩等待时间不能压缩截屏——截屏拿到旧帧会让后面的所有计算都白做这一点在第 5 章还会提到。2.2 环境准备三样东西少一样都跑不起来我把依赖压到最少Python 侧只要 OpenCV 和 NumPy手机侧只要 adb 能连上。# 1. 装依赖opencv 只用基础模块不需要 contrib python -m pip install opencv-python numpy # 2. 确认设备连上了能看到一串序列号才算成功 adb devices # 3. 记下分辨率后面算系数要用 adb shell wm size这三条命令里第二条最容易失败。失败时一般会看到unauthorized原因是手机上没点“允许 USB 调试”。如果看到列表是空的多半是数据线只能充电、不能传数据或者驱动没有装好——这种换线就能解决的故障我见过不止一次排查时先怀疑它。第三条的返回值长这样Physical size: 1080x1920。这个数字请记牢因为第 4 章的系数标定需要用它做参考。不同分辨率下系数不一样1080p 上能跑通的系数放到 2K 屏上可能就会跳过头。手机端还要做两个设置打开开发者选项里的“USB 调试”以及把屏幕常亮、关掉锁屏。锁屏一旦触发截图会拿到锁屏界面识别模块会直接返回空坐标然后主循环就会一直空转——这个问题不报错但很耗时间。2.3 一个能跑起来的最小骨架主循环不处理任何异常逻辑就是“抓、算、按、等”四步import time import cv2 from capture import adb_screencap from vision import find_piece, find_target from action import press def main(coef1.35, rounds50): last_dist None for i in range(rounds): png adb_screencap(/tmp/s.png) # 抓一帧 img cv2.imread(png) if img is None: continue piece find_piece(img) # 棋子底部中心 target find_target(img, piece) # 目标方块中心 if piece is None or target is None: time.sleep(0.4) # 识别失败过一会重试 continue dx target[0] - piece[0] dy target[1] - piece[1] dist (dx * dx dy * dy) ** 0.5 # 欧氏距离单位是像素 last_dist dist ms max(200, min(900, int(dist * coef))) # 卡在安全区间内 press(ms) # 注入按压 time.sleep(1.2) # 等落地动画播完 if __name__ __main__: main()这段代码里有两个参数值得单独说。coef是距离到时间的换算系数单位是“毫秒每像素”。它的物理含义是屏幕上每多 1 个像素的水平距离手指要多按多少毫秒。这个值跟屏幕密度、游戏内部的速度倍率都有关所以不能从别人那里抄。第 4 章会给标定方法。rounds是最大跳数设成 50 是为了防止死循环。真实场景下你会希望它更大但在调参阶段建议压到 10 以内否则一旦系数错了会连续跳很多次失败既浪费时间又容易让人烦躁。press里的max(200, min(900, ...))这层钳制是有原因的第 5 章会专门讲。这里先记住超过 900ms 的按压在不少机型上会被系统截断落点就很不可控。2.4 为什么不用 uiautomator 或 Appium这是选型上最先要回答的问题。跳一跳的画面是游戏引擎渲染的控件树里根本找不到“棋子”和“方块”这两个节点用 uiautomator dump 出来的层级只有一层 SurfaceView。Appium 那套基于无障碍服务的定位方式在这里完全失效唯一能用的就是截图加视觉识别。所以自动化框架的选择不是“哪个更先进”而是“哪个能拿到你要的信息”。控件树能拿到的场景用 Appium 更省事拿不到的就老老实实写视觉。我在项目里保留 adb 而不用更重的方案也是因为它只需要一条 USB 连接没有额外的服务进程要维护挂掉之后重启一条命令就行。3. 图像识别把棋子和目标从像素里抠出来识别模块是整个项目里唯一需要调参的地方也是新手最容易卡住的地方。网上的代码直接抄下来十有八九识别不到棋子——不是代码错了而是颜色区间对不上原作者用的分辨率、系统主题、游戏版本和你不一样同一个阈值在你这儿就是失效的。所以这一段我不只给代码还给出“怎么把阈值调出来”的过程。3.1 先裁剪再识别别一上来就全图跑一张 1080x1920 的图直接在全图上做连通域分析会有两个问题慢而且误检多。游戏画面上方的分数、下方的 UI 元素都可能被当成目标。所以第一步是裁出搜索区域。棋子的有效活动范围大概在屏幕高度的 15%~65%目标方块则在 20%~55% 之间。这个范围不需要绝对精确留点余量没关系多余的区域会由后面的面积筛选过滤掉。def crop(img, top_ratio, bottom_ratio): 按比例裁剪不写死像素值换分辨率不用改代码 h, w img.shape[:2] return img[int(h * top_ratio):int(h * bottom_ratio), :]这里的参数都用比例而不是像素是因为同一份代码要在不同手机上跑。写死img[200:1200, :]在 1080p 上没问题换到 720p 的手机上就会把目标裁掉一半。这是很典型的踩坑点我第一次写的时候就栽在这儿换手机后识别率直接掉到零。裁剪完之后记得把裁剪偏移量存下来因为识别出来的坐标是相对于裁剪区域的最后要加回去才是全图坐标。这个偏移量错了跳的时候就总是差那么一点点看起来像系数问题其实是坐标没换算。3.2 棋子定位颜色阈值加连通域棋子是画面里唯一一个颜色固定、形状固定的物体。它的本体是深蓝紫色头部偏白所以最稳的做法是在 HSV 空间里圈出这段颜色然后取面积最大的连通域。import cv2 import numpy as np def find_piece(img): 返回棋子底部中心的 (x, y)找不到返回 None h, w img.shape[:2] roi img[0:int(h * 0.65), :] # 只在上半屏找 hsv cv2.cvtColor(roi, cv2.COLOR_BGR2HSV) # 深蓝紫色段H 在 100~170S 和 V 都要够高才不是灰色背景 lower np.array([100, 60, 40]) upper np.array([170, 255, 200]) mask cv2.inRange(hsv, lower, upper) # 闭运算把棋子身上的高光断开处连起来 kernel np.ones((5, 5), np.uint8) mask cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel) contours, _ cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if not contours: return None c max(contours, keycv2.contourArea) if cv2.contourArea(c) 500: # 太小的是噪点 return None x, y, cw, ch cv2.boundingRect(c) return x cw // 2, y ch # 底部中心代码里几个参数需要解释清楚因为它们决定了你能不能在自己的手机上跑通。lower和upper是 HSV 区间。H 的取值范围是 0~180不是 0~360这是 OpenCV 的约定第一次用很容易搞错。S 和 V 的下限 60 和 40 是为了把浅灰背景排除掉——背景的饱和度接近 0inRange会直接把它过滤。cv2.morphologyEx那一步的核大小 5x5 是个经验值。核太大棋子会和旁边的方块粘连成一整块导致边界框变得很大核太小棋子身上的高光会把轮廓切成两段max(contours)可能只取到下半身。我试过 3x3 和 7x7前者断得多后者容易粘5x5 比较稳。cv2.contourArea(c) 500是面积下限。这个数字跟分辨率相关1080p 下棋子大概有 3000~6000 像素500 是个安全的下限。如果你换了低分辨率手机要相应调小。最后返回的是(x cw // 2, y ch)即底边中点而不是几何中心。因为跳跃的起算点是棋子脚底和地面接触的位置用中心点算距离会系统性偏大一点跳得越远偏差越明显。阈值怎么调出来。如果跑起来发现返回 None先别改代码把中间结果存下来看cv2.imwrite(/tmp/mask.png, mask) # 看掩码图有没有白块 print(hsv[500, 500]) # 打印棋子所在点的 HSV 值掩码图全黑说明阈值没圈住掩码图有白块但面积太小说明闭运算的核不够大。直接在棋子附近采几个点打印 HSV取最大最小值往外扩 10~20基本就能圈住。这个办法比猜快得多我一般先这么干再动代码。3.3 目标定位逐行扫描找方块顶面目标方块的颜色是随机的没法用固定阈值所以思路要换背景色是已知的画面大面积是浅色凡是和背景色差别足够大的像素就认为是物体。然后从上往下逐行扫第一个满足“连续像素足够多”的行就是方块顶面。def find_target(img, piece_xyNone): 逐行扫描找目标方块顶面的中心找不到返回 None h, w img.shape[:2] top, bottom int(h * 0.20), int(h * 0.55) roi img[top:bottom, :] # 取左上角一小块的中位数当背景色中位数比均值抗噪 bg np.median(roi[:15, :15].reshape(-1, 3), axis0) diff np.abs(roi.astype(np.int16) - bg.astype(np.int16)).sum(axis2) mask (diff 45).astype(np.uint8) for y in range(mask.shape[0]): row mask[y] if row.sum() 8: # 这一行有效像素太少跳过 continue xs np.where(row 0)[0] left, right int(xs.min()), int(xs.max()) cx (left right) // 2 radius (right - left) // 2 return cx, top y radius # 从顶面往下补一个半径 return Nonebg用中位数而不是均值是因为截图左上角偶尔会压到分数文字均值会被拉偏中位数不受影响。取 15x15 的块也是权衡太小了不稳定太大了可能把方块边缘框进去。diff 45这个阈值是三个通道差值的绝对值之和取值范围 0~765。取 45 大致相当于“平均每个通道差 15”能过滤掉压缩噪声又不会漏掉浅色方块。方块如果是接近背景色的淡色这个值要往下调反之如果画面噪点多要往上调。row.sum() 8是为了排除细长的干扰物比如分数文字的笔画。连续 8 个像素才算“一段”这个数跟分辨率有关1080p 下大概是半个棋子宽度。top y radius这一步是整个函数里最不精确的地方。y定位到的是方块顶面那条边而跳跃要落到方块中心所以要往下补。补多少严格算的话3D 透视下顶面是个椭圆中心点比顶边低一个短半轴粗略用radius也就是顶面宽度的一半替代实际会偏大一丁点。很多人第一次跳总是跳过头就是这里补多了。可以把radius乘个 0.85 左右的系数微调这个值不大稳定但跟恒定误差相比它已经能让人接受了。3.4 识别结果自检光有识别还不够必须有个“这次识别靠不靠谱”的判断否则一次误识别就会让后面的所有计算都出错。我一般检查三件事坐标是否越界面积是否过小以及这一跳的距离和上一跳差得离谱不离谱。def is_reliable(piece, target, last_dist, img): h, w img.shape[:2] if piece is None or target is None: return False for x, y in (piece, target): if not (0 x w and 0 y h): return False dist ((target[0] - piece[0]) ** 2 (target[1] - piece[1]) ** 2) ** 0.5 if dist 30: # 太近了肯定是把背景当目标了 return False if last_dist and abs(dist - last_dist) last_dist * 1.5: return False # 距离突变大概率识别错了 return True第三个判断尤其有用。跳一跳里方块的水平间距是有范围的一般不会出现“上一跳 200 像素、这一跳 800 像素”这种变化。如果出现了八成是把远处的某个色块当成了目标。这时候跳过这一帧重新截图比硬跳要安全得多。不过这个判断也有个前提第一次跳没有last_dist参照所以第一跳必须靠人工看一眼截图确认。我一般的做法是先在调试模式下把识别结果画在截图上确认几帧没问题之后再打开自动模式。4. 距离到时长那个 1.35 的系数到底怎么来的识别做完剩下的问题就一个知道要跳多远怎么变成按多久。这一步看起来最简单实际上是最容易出玄学问题的地方——同样一段代码在别人手机上能用换台机器就跳一半很多人卡在这里放弃。4.1 线性关系成立的前提游戏内部的实现是手指按住屏幕时棋子开始蓄力松手时按蓄力时间给一个初速度棋子沿抛物线飞出去。在小距离范围内飞行距离和蓄力时间近似成正比这就是那个系数的来源。但这个近似是有前提的。第一按压时间不能太长超出线性区之后飞得更远但超出得不成比例反而很难控制第二游戏的帧率要稳定帧率抖的时候实际蓄力量会有误差第三方块的高低差不能太大因为飞行距离是三维空间的距离而我们的公式算的是二维屏幕距离。跳一跳里有高低方块高处往低处跳的时候同样的二维距离实际需要的蓄力更小。这也是为什么脚本偶尔会在高低交界处失败。知道这些前提之后就不会指望一个系数解决所有情况了。实际做法是先把平坦段落跑稳遇到高低差大的地方接受偶发失败。4.2 手动标定两次跳跃反推系数标定的核心思路是让脚本用已知的系数跳一次量出实际落点和期望落点差多少反推出正确的系数。具体步骤是这样的第一步设一个初始系数比如 1.0让它跳一次这一跳会明显不够因为 1.0 太小第二步从截图里读出棋子的实际落点坐标第三步代进公式反推def recalibrate(coef_old, piece, target, landed): piece/target 是这一跳的期望landed 是实际落点中心 want ((target[0] - piece[0]) ** 2 (target[1] - piece[1]) ** 2) ** 0.5 got ((landed[0] - piece[0]) ** 2 (landed[1] - piece[1]) ** 2) ** 0.5 if got 1: return coef_old return coef_old * want / got这里want是“本来应该跳多远”got是“实际跳了多远”。因为飞行距离和按压时间成正比所以实际落点距离和按压时间成正比那么新系数 旧系数 × 期望距离 / 实际距离。这个公式要成立前提是got不为零所以加了个保护。landed这个坐标从哪来最土的办法是人工看图最省事的办法是让脚本再识别一次——落地之后棋子会在新的方块上停住这时候跑一次find_piece拿到的就是落点。但要注意落地后棋子位置可能和方块边缘有偏差识别出来的坐标本身有误差所以标定最好做三到五次取平均。4.3 自动标定写个脚本扫一圈手动标定做两三次还行做多了会烦。我用的是简单的扫描法固定跳几个不同距离的格子记录每次的偏差率取平均。def auto_calibrate(device, coef01.0, rounds6): coef coef0 history [] for i in range(rounds): img device.grab() piece, target locate(img) if piece is None or target is None: continue dist ((target[0] - piece[0]) ** 2 (target[1] - piece[1]) ** 2) ** 0.5 device.press(int(dist * coef)) time.sleep(1.2) img2 device.grab() landed, _ locate(img2) # 落地后重新识别棋子位置 if landed is None: continue new_coef recalibrate(coef, piece, target, landed) history.append(new_coef) # 只接受合理范围内的修正防止一次误识别把系数带飞 if 0.7 * coef new_coef 1.3 * coef: coef new_coef return sum(history) / len(history) if history else coef0这里的关键是那层0.7 ~ 1.3的钳制。不加这层一次误识别就能把系数拉到离谱的值后面的跳就全废了。加了之后单次误差最多修 30%几轮下来会收敛到真值附近。rounds取 6 是经验值太少不稳定太多没必要因为每一轮都要花掉好几秒。标完之后把coef写进配置文件后面就不用再标了。但手机换了、分辨率变了、游戏更新了都要重新标。4.4 注入命令的写法与上限最后把毫秒数变成一次触摸事件import subprocess def press(ms, x500, y1600): 在 (x, y) 处按住 ms 毫秒后松开 ms max(200, min(900, int(ms))) # 安全区间 subprocess.run([ adb, shell, input, swipe, str(x), str(y), str(x), str(y), str(ms) ], checkTrue)input swipe本来是滑动命令起止点相同就变成了长按。这是最省事的写法不用 root也不用装额外的东西。但这条命令有两个坑我在第 5 章会展开。简单说一是ms的上限很多机型在超过 1000ms 之后行为会变得不可预测二是swipe的起止点如果完全重合个别 ROM 会判定为无效事件直接丢弃。稳妥的做法是把终点往下挪 1 个像素视觉上看不出区别事件却能正常触发。x和y的默认值也要按分辨率调整。1080p 下取屏幕中央偏下比较安全因为这些位置大概率是空地不会误触 UI。如果你换到平板或者折叠屏这个坐标要重新算否则按压点可能落在别的元素上。5. 避坑与排查跳到沟里的五种姿势识别和换算都跑通之后剩下的问题基本都出在“环境”而不是“代码”上。这一章的五条是我自己踩过的按现象、原因、解决写遇到问题可以对着查。5.1 截图全黑或者拿到的是上一帧现象程序跑起来识别模块一直返回 None手动打开/tmp/s.png发现是黑屏或者是几秒前的旧画面。原因有两个。一是手机息屏了截屏命令拿到的是锁屏界面或者一片黑。二是用的adb shell screencap加adb pull两步走第一步在手机上写文件第二步再拉回来中间有延迟高频调用时会拿到上一轮的文件。解决先在最前面加一条亮屏命令并且改成一步取流的写法。def adb_screencap(path/tmp/s.png): with open(path, wb) as f: subprocess.run( [adb, exec-out, screencap, -p], stdoutf, checkTrue ) return pathexec-out把二进制直接吐到标准输出省掉了在手机上写文件再拉回来的一步快而且不会拿到旧帧。配合adb shell input keyevent KEYCODE_WAKEUP提前唤醒屏幕基本能解决。另外把系统里的自动锁屏时间调长或者干脆用开发者选项里的“充电时不锁屏”。5.2 棋子识别不到掩码图全黑现象find_piece一直返回 None把掩码存下来看是一张纯黑图。原因颜色区间不对。最常见的情况是换了手机屏幕色温偏冷或者偏暖同一个棋子的 HSV 值和原来差很多其次是游戏更新换了皮肤颜色完全变了。解决别猜直接采点。在棋子所在位置打印 HSV取几个点看看 H/S/V 的实际范围然后把lower和upper往外扩。如果换了皮肤可以改成模板匹配手工截一张棋子的小图存成模板用cv2.matchTemplate找最大响应位置。模板匹配对颜色不敏感但要求棋子在截图里的尺寸和模板一致换分辨率要重做模板。def find_piece_tpl(img, tpl, thresh0.75): res cv2.matchTemplate(img[:int(img.shape[0] * 0.65)], tpl, cv2.TM_CCOEFF_NORMED) _, max_val, _, max_loc cv2.minMaxLoc(res) if max_val thresh: return None th, tw tpl.shape[:2] return max_loc[0] tw // 2, max_loc[1] ththresh取 0.75 是个折中。调高会漏检调低会误检先从这个值开始试。5.3 按压被截断越远跳得越不够现象近距离的跳没问题距离一远就明显不到位而且误差随距离线性增大。原因input swipe的 duration 有上限。不同 ROM 的上限不一样有的 1000ms有的更低。超过之后事件会被截断或者直接丢弃实际按压时间比你传的短。解决先确认是不是这个问题——把press里的 ms 打印出来看远距离那几跳是不是都在 900 以上。如果是有两个办法。一是降低截图分辨率把图缩到 720p 再算距离这样所需时间也会按比例缩小。二是换成sendevent直接发原始触摸事件绕开input命令的限制代价是要先getevent拿到设备节点不同机型节点名不一样移植性差。我一般优先用第一种因为改起来简单只用一行cv2.resizeimg cv2.resize(img, (720, 1280)) # 缩放后所有坐标同比缩小注意缩放之后系数也要跟着改因为像素距离变了但物理距离没变。5.4 系数对但落点忽左忽右现象平均误差不大但单次误差方向不稳定有时偏左有时偏右有时候差得还挺多。原因识别抖动或者按压过程中有干扰。识别抖动的来源是目标方块的中心估计——radius那个近似本身就带误差方块的大小不同误差大小也不同。干扰的来源是按压期间有通知弹出、屏幕亮度自动调整或者游戏帧率波动。解决一是把目标中心的 y 补偿系数从固定的 1.0 改成可调按方块大小动态算二是关掉系统的自动亮度、免打扰模式三是如果误差主要是横向的检查一下find_target返回的cx是不是被某个偏在一侧的装饰物带偏了可以在取left和right的时候用分位数代替最值排除掉零星噪点。5.5 卡在某一跳不动循环空转现象程序一直在跑但棋子不跳了日志里识别一直返回 None。原因游戏进入了结算界面或者弹出了广告、复活提示。这时候画面内容和游戏进行中完全不同识别逻辑自然失效但主循环不知道发生了什么就一直重试。解决加一个状态判断。最简单的做法是检测画面上有没有特定的按钮颜色或者文字区域命中就跳出循环复杂一点的做法是记录连续失败的次数超过阈值就停下来让人工介入。fail 0 while True: if fail 10: print(连续失败可能进了结算页停下) break ... if piece is None or target is None: fail 1 time.sleep(0.4) continue fail 0这个计数重置的时机很重要一定要在成功识别之后才清零否则偶尔成功一次又把计数归零阈值就形同虚设。6. 二次开发把它改成你自己的自动化骨架这个项目最有价值的地方是它本身可以让人进一步去搭建自己的自动化工具。把这套骨架收干净之后可以做很多同类的事自动点签到、自动翻页截图、自动跑一轮 UI 回归。下面是我实际做过的三个改造方向。6.1 把设备操作抽成接口原始代码里subprocess.run([adb, ...])散在各处换一种设备接入方式就得改好几处。我的做法是抽一个极简的接口出来class Device: def grab(self): 返回一帧 BGR ndarray raise NotImplementedError def press(self, ms, x, y): 在 (x, y) 处按压 ms 毫秒 raise NotImplementedError class AdbDevice(Device): def __init__(self, serialNone): self.serial serial def grab(self): ... def press(self, ms, x, y): ...抽出来之后接投屏协议、接模拟器、接云真机都只是多写一个子类的事识别和换算逻辑一行不动。这个改动看着不起眼但它把“设备”和“策略”彻底分开了后面加任何新玩法都不用担心影响底层。6.2 加一层录制回放调参最痛苦的地方是每次都要真机跑一遍慢而且不可复现。我加了一个录制模式每跳一次把截图和当时的决策系数、距离、按压时长、识别到的坐标一起存下来命名带序号。调试的时候直接读这些图离线跑识别几秒钟就能过一遍比真机快十倍。回放的时候还能对比“当时识别到的坐标”和“现在识别到的坐标”一眼就能看出是阈值调过头了还是算法退化了。这一步做完之后我调识别的效率高了很多。6.3 系数自动标定脚本第 4 章给过一个自动标定的雏形实际用的时候我把它做成了独立脚本接受一个设备对象和初始系数跑完之后输出建议系数和每一轮的误差曲线。改造点现状改法收益设备接入adb 写死抽 Device 接口换设备不改识别代码调试方式真机跑录制回放调参快一个数量级系数标定手工试扫描脚本 钳制换机五分钟内恢复异常退出死循环连续失败计数挂了能停不浪费电状态判断无关键区域颜色检测结算页自动识别表格里最后两项是我踩坑之后才补的不是一开始就想到的。所以说改造这件事不用一次性做完跑到哪一步遇到问题就补哪一块。从那以后我每次做类似的自动化都强制先把“录制回放”这条路走通再去调识别参数。因为这个习惯很多看起来玄学的偏差最后都能落到一张具体的截图上而不是靠一次次重跑碰运气。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。