1)前言
从 18 年开始,我接触了叉叉助手 (平台已经被请喝茶了),通过图色识别,用来给常玩的游戏写挂机脚本,写了也有两三年.也算是我转行当游戏测试的理由.
去年 11 月,也是用了这身技术,混进了外包,薪资还不错,属于是混日子了,岗位是在发行,接触到很多游戏,因为接不了 poco,到手只有 apk,
日积月累,游戏越来越多,项目组却还是只有这点人.为了减轻自己的压力,就开始了 UI 自动化的不归路.
2)游戏 UI 自动化
因为游戏引擎,是无法通过 appium 等框架去获取,如果不接入一些 SDK,那么识别的方法只有图像识别.现在常见的开源框架文章源自玩技e族-https://www.playezu.com/191654.html
- 网易的 Airtest,通过传统识别进行自动化,还有 airtestIDE 可以简单快速的编写 airtest 代码
- 腾讯 GameAISDK,通过深度学习进行自动化 (没用过,好久不维护了)
- 阿里的 SoloPi,主要功能是录制、群控,有图像匹配辅助
图像相关的常见方法:文章源自玩技e族-https://www.playezu.com/191654.html
- 传统的识别方法: 特征点、模板、轮廓
- 特征点: SIFT, ORB
- 下文会详细讲
- 模板匹配: opencv 的 matchTemplate
- 最简单的方案,通过讲模板在目标图像中平移,找到最符合的目标
- 轮廓: HALCON Shape-based Matching, Canny
- 没用过,写不来,halcon 的要花钱
- 特征点: SIFT, ORB
- 基于深度学习的方法:
- 文字识别: PaddleOCR,tesseract
- paddleOCR 基本上开箱即用,但是对于游戏内的艺术字,还需要额外的训练
- 图像分类: paddleClas
- 没有实际用过,感觉可以用在区分场景,然后去做更加详细的识别.比如识别弹窗
- 目标检测: yolo
- 之前很火的 Fps 外挂,基本就是靠这个去识别人体
- 文字识别: PaddleOCR,tesseract
UI 自动化的核心在于查找元素,并且在什么位置.那么重点就会放在图像识别上.
基于深度学习的方案,需要大量的正负样本和标注工作,因此只能放弃.取而代之的是传统的识别方案.
在社区里、qq 的测试群里就能发现,大多数人对传统图像识别的印象是:慢,不准.
今年过年前,去张江面试过一家游戏公司,也是发行公司,聊了一个多小时,聊下来他们的方案是 airtest 一种机型截一个图去做适配.我大受震撼.
总结下来图像识别的 UI 自动化难点:文章源自玩技e族-https://www.playezu.com/191654.html
- 识别慢
- 识别结果不准确
- 多分辨率不兼容性
- 游戏 UI 更新,管理图片库的成本
3)怎么解决
那么我做了什么,项目就在这里:https://github.com/hakaboom/py_image_registration
目前也是在重构,重构完成后可能起个好名字:https://github.com/hakaboom/image_registration文章源自玩技e族-https://www.playezu.com/191654.html
一开始是参考了 airtest 的 aircv 部分,当时不想有那么多依赖,就拆出来了.
重构之后,通过对 opencv 一些 api 的封装,重新组织了构架和算法.目前效果感觉不错,也已经给 airtest 提了 pr,后续也会推进合并.文章源自玩技e族-https://www.playezu.com/191654.html
安装 opencv-python
建议版本可以是 4.5.5文章源自玩技e族-https://www.playezu.com/191654.html
- pypi 上有编译好的,但是只能用 cpu 方法:
pip install opencv-python
pip install opencv-contrib-python
- 从源码编译,可以自定义更多的东西,比如增加 cuda 支持
- 先从 opencv 仓库克隆代码
- 剩下的看这里 https://github.com/hakaboom/py_image_registration/blob/master/doc/cuda_opencv.md
什么是特征点
简单的理解: 用于描述图像特征的关键点文章源自玩技e族-https://www.playezu.com/191654.html
常见的特征点提取算法:文章源自玩技e族-https://www.playezu.com/191654.html
- SIFT: 尺度不变特征变换. opencv 只有 cpu 实现
- SURF: surf 的加速算法. opencv 有 cpu 和 cuda 实现
- ORB: 使用 FAST 特征检测和 BRIEF 特征描述子. opencv 有 cpu 和 cuda 实现
他们的好处是什么: 尺度和旋转不变性,说白了就是兼容不同分辨率、旋转、尺度的变换
速度排序: ORB(cuda)>SURF(cuda)>ORB>SURF>SIFT
效果排序 (效果不止是特征点的数量,更重要的是特征点的质量): SIFT>ORB>SURF文章源自玩技e族-https://www.playezu.com/191654.html
例子
- 6.png(2532x1170) iphone12pro 上的截图
- 4.png(1922x1118 实际游戏渲染是 1920x1080,多出来的是 windows 边框) 崩三桌面端的截图, 裁剪了右上角的蓝色加号区域当模板
import cv2
import time
from baseImage import Image, Rect
from image_registration.matching import SIFT
match = SIFT()
im_source = Image('tests/image/6.png')
im_search = Image('tests/image/4.png').crop(Rect(1498,68,50,56))
start = time.time()
result = match.find_all_results(im_source, im_search)
print(time.time() - start)
print(result)
img = im_source.clone()
for _ in result:
img.rectangle(rect=_['rect'], color=(0, 0, 255), thickness=3)
img.imshow('ret')
cv2.waitKey(0)
结果可以得到三个加号的位置文章源自玩技e族-https://www.playezu.com/191654.html
[
{'rect': <Rect [Point(1972.0, 33.0), Size[56.0, 58.0]], 'confidence': 0.9045119285583496},
{'rect': <Rect [Point(2331.0, 29.0), Size[52.0, 66.0]], 'confidence': 0.9046278297901154},
{'rect': <Rect [Point(1617.0, 30.0), Size[51.0, 64.0]], 'confidence': 0.9304171204566956}
]
怎么进行匹配
Airtest 的 aircv 做了什么
https://github.com/AirtestProject/Airtest/blob/d41737944738e651dd29564c29b88cc4c2e71e2e/airtest/aircv/keypoint_base.py#L133
1.获取特征点
2.匹配特征点
def match_keypoints(self, des_sch, des_src):
"""Match descriptors (特征值匹配)."""
# 匹配两个图片中的特征点集,k=2表示每个特征点取出2个最匹配的对应点:
return self.matcher.knnMatch(des_sch, des_src, k=2)
我们可以看到,这边k=2
代表,一个模板上的特征点,去匹配两个目标图像的特征点
3.筛选特征点
good = []
for m, n in matches:
if m.distance < self.FILTER_RATIO * n.distance:
good.append(m)
通过计算两个描述符之间的距离差,来筛选结果
4.根据透视变换或坐标计算,获取矩形,然后计算置信度
那么以上步骤会存在什么问题:
- 在第二步,假设图片中存在
n
个目标图片,如果`k<n',就会导致匹配特征点数量变少 - 在第三步,筛选的方法不太合理,实际 debug 中会发现,一些特征点即使
distance
数值很高,但从结果上看,还是符合目标的,那么就意味着单纯根据距离去筛选特征点 的方法是不靠谱的 - 在第四步,获取完特征点后,airtest 的方式是,根据透视变换获取目标的四个顶点坐标,计算出最小外接矩形. 那么如果目标图片存在旋转/形变,那么最后获取的图片会裁剪到多余目标,造成置信度降低
修改后的特征点匹配
核心在于利用特征点尺度和旋转不变性
1.读取图片
from baseImage import Image
im_source = Image('tests/image/6.png')
这边用到了我另外一个库 https://github.com/hakaboom/base_image
主要的用处对 opencv 的图像数据进行格式和类型的转换,以及一些接口的包装
- 使用 place 参数,修改数据格式
- Ndarray: 格式为 numpy.ndarray 格式
- Umat: python 的绑定不多,没有 ndarray 灵活,可以用于 opencl 加速
- GpuMat: opencv 的 cuda 格式,需要注意显存消耗
from baseImage import Image
from baseImage.constant import Place
Image(data='tests/image/0.png', place=Place.Ndarray) # 使用numpy
Image(data='tests/image/0.png', place=Place.UMat) # 使用Umat
Image(data='tests/image/0.png', place=Place.GpuMat) # 使用cuda
2.创建特征点检测类
这边会有一些参数,除了 threshold(过滤阈值)、rgb(是否通过 rgb 通道检测) 以为,还有可以加入特征点提取器的一些配置,一般默认就好,具体可以查 opencv 文档
from image_registration.matching import SIFT
match = SIFT(threshold=0.8, rgb=True, nfeatures=50000)
3.识别
from image_registration.matching import SIFT
from baseImage import Image, Rect
im_source = Image('tests/image/6.png')
im_search = Image('tests/image/4.png').crop(Rect(1498,68,50,56))
match = SIFT(threshold=0.8, rgb=True, nfeatures=50000)
result = match.find_all_results(im_source, im_search)
4.解析下find_all_results
里做了什么,可以在image_registration.matching.keypoint.base
里找到基类
- 第一步: 创建特征点提取器
BaseKeypoint.create_matcher
例:image_registration.matching.keypoint.sift
def create_detector(self, **kwargs) -> cv2.SIFT:
nfeatures = kwargs.get('nfeatures', 0)
nOctaveLayers = kwargs.get('nOctaveLayers', 3)
contrastThreshold = kwargs.get('contrastThreshold', 0.04)
edgeThreshold = kwargs.get('edgeThreshold', 10)
sigma = kwargs.get('sigma', 1.6)
detector = cv2.SIFT_create(nfeatures=nfeatures, nOctaveLayers=nOctaveLayers, contrastThreshold=contrastThreshold,
edgeThreshold=edgeThreshold, sigma=sigma)
return detector
- 第二步: 创建特征点匹配器
BaseKeypoint.create_detector
用于匹配模板和目标图片的特征点 有两种匹配器,-
BFMatcher
: 暴力匹配, 总是尝试所有可能的匹配 -
FlannBasedMatcher
: 算法更快,但是也能找到最近邻的匹配
-
- 第三步: 提取特征点
BaseKeypoint.get_keypoint_and_descriptor
用第一步创建的提取器去获取特征点.ORB 这种,还需要额外的去增加描述器.具体就看代码实现吧. - 第四步: 匹配特征点 用第二步创建的匹配器,获取特征点集
- 第五步: 筛选特征点
BaseKeypoint.filter_good_point
首先要解释下会用到的两个类-
cv2.DMatch
opencv 的匹配关键点描述符类-
distance
: 两个描述符之间的距离 (欧氏距离等),越小表明匹配度越高 -
imgIdx
: 训练图像索引 -
queryIdx
: 查询描述符索引 (对应模板图像) -
trainIdx
: 训练描述符索引 (对应目标图像)
-
-
cv2.Keypoint
opencv 的特征点类-
angle
: 特征点的旋转方向 (0~360) -
class_id
: 特征点的聚类 ID -
octave
:特征点在图像金字塔的层级 -
pt
: 特征点的坐标 (x,y) -
response
: 特征点的响应强度 -
size
: 特征点的直径大小
-
-
知道了这两种类之后,我们就可以通过第四步获取的特征点集进行筛选
- 步骤 1: 根据 queryIdx 的索引对列表进行重组,主要目的是,让一个模板的特征点只可以对应一个目标的特征点
- 步骤 2: 根据 distance 的升序,对特征点集进行排序,提取出第一个点,也就是当前点集中,
distance
数值最小的点,为待匹配点A
- 步骤 3. 获取点
待匹配点A
对应的queryIdx
和trainIdx
的 keypoint(query_keypoint
,train_keypoint
,通过两个特征点的angle
可以计算出,特征点的旋转方向 - 步骤 4. 计算
train_keypoint
与其他特征点的夹角,根据旋转不变性,我们可以根据模板上query_keypoint
的夹角, 去筛选train_keypoint
的夹角 - 步骤 5. 计算以
query_keypoint
为原点,其他特征点的旋转角,还是根据旋转不变性,我们可以再去筛选以train_keypoint
原点,其他特征的的旋转角 - 最后,我们就可以获取到,所有匹配的点、图片旋转角度、基准点 (
待匹配点A
)
5.筛选完点集后,就可以进行匹配了,这边会有几种情况BaseKeypoint.extract_good_points
- 没有特征点 (其实肯定会有一个特征点)
- 有 1 组特征点
BaseKeypoint._handle_one_good_points
- 根据两个特征点的
size
大小,获取尺度的变换 - 根据步骤 4 中返回的旋转角度,获取变换后的矩形顶点
- 通过透视变换,获取目标图像区域,与目标图像进行模板匹配,计算置信度
- 根据两个特征点的
- 有 2 组特征点
BaseKeypoint._handle_two_good_points
- 计算两组特征点的两点之间距离,获取尺度的变换
- 根据步骤 4 中返回的旋转角度,获取变换后的矩形顶点
- 通过透视变换,获取目标图像区域,与目标图像进行模板匹配,计算置信度
- 有 3 组特征点
BaseKeypoint._handle_three_good_points
- 根据三个特征点组成的三角形面积,获取尺度的变换
- 根据步骤 4 中返回的旋转角度,获取变换后的矩形顶点
- 通过透视变换,获取目标图像区域,与目标图像进行模板匹配,计算置信度
- 有大于等于 4 组特征点
BaseKeypoint._handle_many_good_points
- 使用单矩阵映射
BaseKeypoint._find_homography
,获取变换后的矩形顶点 - 通过透视变换,获取目标图像区域,与目标图像进行模板匹配,计算置信度
- 使用单矩阵映射
6.删除特征点
匹配完成后,如果识别成功,则删除目标区域的特征点,然后进入下一次循环
4)基准测试
设备环境:
- i7-9700k 3.6GHz
- NvidiaRTX 3080Ti
- cuda 版本 11.3
- opencv 版本:4.5.5-dev(从源码编译)
测试内容: 循环 50 次,获取目标图片和模板图片的特征点.
注:没有进行特征点的筛选, 特征点方法没有进行模板匹配计算置信度,因此实际速度会比测试的速度要慢
从图中可以看出 cuda 方法的速度最快,同时 cpu 的占用也小,原因是这部分算力给到了 cuda
因为没有用代码获取 cuda 使用率,这边在任务管理器看的,只能说个大概数
- cuda_orb: cuda 占用在 35%~40% 左右
- cuda_tpl: cuda 占用在 15%~20% 左右
- opencl_surf: cuda 占用在 13% 左右
- opencl_akaze: cuda 占用在 10%~15% 左右
还有其他的算法,opencv 没有提供 cuda 或者是 opencl 的实现,只能用 cpu 加速
5)怎么优化速度
- airtest 慢的一个原因在于,只用了 cpu 计算.如果能释放算力到 gpu 上,速度就会有成倍的增长.
opencv 已经给我们做好了很多接口.我们可以通过cv2.cuda.GpuMat
,cv2.UMat
调用 cuda 和 opencl 的算法.
通过baseImage
可以快速的创建对应格式的图像
from baseImage import Image
from baseImage.constant import Place
Image('tests/images/1.png', place=Place.GpuMat)
Image('tests/images/1.png', place=Place.UMat)
可以用 cuda 加速的识别方法, 需要调用其他的类函数,且图片格式需要是cv2.cuda.GpuMat
- surf: 没写,下次再补
- orb: 对应函数
image_registration.matching.keypoint.orb.CUDA_ORB
- matchTemplate
image_registration.matching.template.matchTemplate.CudaMatchTemplate
可以用 opencl 加速的识别方法, 只需要传图像参数的时候,格式是UMat
,opencv 会自动的调用opencl
方法
- surf
- orb
- matchTemplate
这边只讲了特征点获取/模板匹配的方法,在其他的图像处理函数中cuda
和opencl
也能有一定的加速,但是不如以上方法明显
- 从框架设计上进行加速.(可能只限于游戏应用,传统 app 用不了)
- 从游戏上讲,我们预先知道一些控件,在屏幕中的坐标位置.分辨率进行转换时,我们可以通过计算控件的位置,裁剪对应位置的图像,通过模板匹配进行快速的识别.
- 举个例子,下面两张图,一个是 1280x720 下的截图,一个是 2532x1170 下的截图
- 1280x720 下邮件控件的坐标范围是
Rect(372,69,537,583)
- 通过下面的计算方式,我们可以得出 2532x1170 下,范围是
Rect(828,110,874,949)
,通过裁剪软件取得的范围是Rect(830,112,874,948)
- 具体的原理是利用了,引擎的缩放和锚点原理,反向求出坐标范围.去适应一些黑边,刘海的情况.
- 求出范围后,裁剪范围的图片,和模板去做匹配,就可以快速的识别一些固定位置的控件
from baseImage import Rect
from baseImage.coordinate import Anchor, screen_display_type, scale_mode_type
anchor = Anchor(
dev=screen_display_type(width=1280, height=720),
cur=screen_display_type(width=2532, height=1170, top=0, bottom=0, left=84, right=84),
orientation=1, mainPoint_scale_mode=scale_mode_type(), appurtenant_scale_mode=scale_mode_type()
)
rect = Rect(371, 68, 538, 584)
point = anchor.point(rect.x, rect.y, anchor_mode='Middle')
size = anchor.size(rect.width, rect.height)
print(Rect.create_by_point_size(point, size))
# <Rect [Point(828.9, 110.5), Size[874.2, 949.0]]
- 建立模板库,预先加载模板,得到屏幕图片后,通过一些相似度计算
baseImage.utils.ssim
对场景进行识别与分类,然后去识别相应场景的特征点.用这样的方法去减少计算量- 这边其实有想法去扩展到深度学习,比如之前说的图像分类.首先我们建立了一个很大的模板库,可以拆分出来
界面1
,界面2
,界面3
和一些通用控件
- 再通过分类去获得当前在什么界面,然后只识别这个界面的控件,达到减少计算量的作用
- 这边其实有想法去扩展到深度学习,比如之前说的图像分类.首先我们建立了一个很大的模板库,可以拆分出来
#6)备注
有其他疑问的话,可以在 玩技博客 的游戏测试 qq 群里找到我 157875133
#7) 更新
2022/5/15
- 最新的 pr 上,用 numpy 代替了原本的 for 循环计算,特征点筛选的速度快了 10 多倍.
- 之前的基准测试不太严谨,现在用了 nvidia Nsight Systems 去拉数据.
这边遇到了一个问题,不清楚是不是 bug. windows 环境下 opencv 调用
cuFFT
库时,大部分时间都在加载库上,就是图中的黄色色块.甚至在1050ti
上的速度都比3080Ti
快.目前还没有解决,建议还是在 linux 下部署
下图是 ubuntu 下的数据. 识别流程在 40~50 毫秒. SM 的占用也有空余,可以使用 Mps 等服务,开多进程去多并发.
未知地区 1F
不错,好文!~ 学习了,很有用~ 学习了加精理由:游戏测试那边自己对 Ocr 进行改造,值得其他人包含我自己在内学习。tql 带一带6 啊大佬很久没看到这样的好文了,支持牛逼,学习学习大佬牛逼,我也在用 airtest 测试,正好能用上,学习了~好文!!!请教大佬一个问题哈,pyautogui 利用截图可以直接在当前屏幕上获取匹配到的图片的中心坐标,你这种方法两个图片的匹配都是提前获取了的吗,如果获取图片在当前屏幕上的位置呢?请教大佬一个问题哈,pyautogui 利用截图可以直接在当前屏幕上获取匹配到的图片的中心坐标,你这种方法两个图片的匹配都是提前获取了的吗,如果获取图片在当前屏幕上的位置呢?模板图片 (代码示例中的im_search) 是提前裁剪的,目标图片 (代码示例中的im_source) 才是你这边说的”当前屏幕”.Place.Mat 如何拿到的。
class Place(object):
Ndarray = np.ndarray
GpuMat = None
UMat = cv2.UMat
新版本我删掉了,python 里 Mat 和 Ndarray 区别不大楼主有心了,正好在撸图像元素识别,讲解很细致。6666Linux 机器上没有显卡怎么办
还有截图的是啥游戏 没显卡就 cpu 咯,截图是碧蓝航线,之前在肝,就正好用了干货,刚好想学习一下