原文由 hakaboom 发表于 TesterHome 社区,点击原文链接可与作者间接交换。
1)前言
从 18 年开始, 我接触了叉叉助手 (平台曾经被请喝茶了), 通过图色辨认, 用来给常玩的游戏写挂机脚本, 写了也有两三年. 也算是我转行当游戏测试的理由.
去年 11 月, 也是用了这身技术, 混进了外包, 薪资还不错, 属于是混日子了, 岗位是在发行, 接触到很多游戏, 因为接不了 poco, 到手只有 apk,
与日俱增, 游戏越来越多, 项目组却还是只有这点人. 为了加重本人的压力, 就开始了 UI 自动化的不归路.
2)游戏 UI 自动化
因为游戏引擎, 是无奈通过 appium 等框架去获取, 如果不接入一些 SDK, 那么辨认的办法只有图像识别. 当初常见的开源框架
- 网易的 Airtest, 通过传统辨认进行自动化, 还有 airtestIDE 能够简略疾速的编写 airtest 代码
- 腾讯 GameAISDK, 通过深度学习进行自动化(没用过, 良久不保护了)
- 阿里的 SoloPi, 次要性能是录制、群控, 有图像匹配辅助
图像相干的常见办法:
-
传统的识别方法: 特色点、模板、轮廓
-
特色点: SIFT, ORB
- 下文会具体讲
-
模板匹配: opencv 的 matchTemplate
- 最简略的计划, 通过讲模板在指标图像中平移, 找到最合乎的指标
-
轮廓: HALCON Shape-based Matching, Canny
- 没用过, 写不来,halcon 的要花钱
-
-
基于深度学习的办法:
-
文字辨认: PaddleOCR,tesseract
- paddleOCR 基本上开箱即用, 然而对于游戏内的艺术字, 还须要额定的训练
-
图像分类: paddleClas
- 没有理论用过, 感觉能够用在辨别场景, 而后去做更加具体的辨认. 比方辨认弹窗
-
指标检测: yolo
- 之前很火的 Fps 外挂, 根本就是靠这个去辨认人体
-
UI 自动化的外围在于查找元素, 并且在什么地位. 那么重点就会放在图像识别上.
基于深度学习的计划, 须要大量的正负样本和标注工作, 因而只能放弃. 取而代之的是传统的辨认计划.
在社区里、qq 的测试群里就能发现, 大多数人对传统图像识别的印象是: 慢, 不准.
今年过年前, 去张江面试过一家游戏公司, 也是发行公司, 聊了一个多小时, 聊下来他们的计划是 airtest 一种机型截一个图去做适配. 我大受震撼.
总结下来图像识别的 UI 自动化难点:
- 辨认慢
- 辨认后果不精确
- 多分辨率不兼容性
- 游戏 UI 更新, 治理图片库的老本
3)怎么解决
那么我做了什么, 我的项目就在这里:https://github.com/hakaboom/p…
目前也是在重构, 重构实现后可能起个好名字:https://github.com/hakaboom/i…
一开始是参考了 airtest 的 aircv 局部, 过后不想有那么多依赖, 就拆出来了.
重构之后, 通过对 opencv 一些 api 的封装, 从新组织了构架和算法. 目前成果感觉不错, 也曾经给 airtest 提了 pr, 后续也会推动合并.
装置 opencv-python
倡议版本能够是 4.5.5
-
pypi 上有编译好的, 然而只能用 cpu 办法:
pip install opencv-python
pip install opencv-contrib-python
-
从源码编译, 能够自定义更多的货色, 比方减少 cuda 反对
- 先从 opencv 仓库克隆代码
- 剩下的看这里 https://github.com/hakaboom/p…
什么是特色点
简略的了解: 用于形容图像特色的关键点
常见的特色点提取算法:
- SIFT: 尺度不变特色变换. opencv 只有 cpu 实现
- SURF: surf 的减速算法. opencv 有 cpu 和 cuda 实现
- ORB: 应用 FAST 特色检测和 BRIEF 特征描述子. opencv 有 cpu 和 cuda 实现
他们的益处是什么: 尺度和旋转不变性, 说白了就是兼容不同分辨率、旋转、尺度的变换
速度排序: ORB(cuda)>SURF(cuda)>ORB>SURF>SIFT
成果排序(成果不止是特色点的数量, 更重要的是特色点的品质): SIFT>ORB>SURF
例子
- 6.png(2532×1170)iphone12pro 上的截图
- 4.png(1922×1118 理论游戏渲染是 1920×1080, 多进去的是 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)
后果能够失去三个加号的地位
[{'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/AirtestPro…
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=2
的话, 就会导致特色点数量变少 - 在第三步, 筛选的办法不太正当, 理论 debug 中会发现, 一些特色点即便
distance
数值很高, 但从后果上看, 还是合乎指标的, 那么就意味着单纯依据间隔去筛选特色点
的办法是不靠谱的 - 在第四步, 获取完特色点后,airtest 的形式是, 依据透视变换获取指标的四个顶点坐标, 计算出最小外接矩形.
那么如果指标图片存在旋转 / 形变, 那么最初获取的图片会裁剪到多余指标, 造成置信度升高
既然 airtest 存在这些问题, 那么我做了什么改变, 我把步骤一个个拆分
我的特色点匹配
1. 读取图片
from baseImage import Image
im_source = Image('tests/image/6.png')
这边用到了我另外一个库 https://github.com/hakaboom/b…
次要的用途对 opencv 的图像数据进行格局和类型的转换, 以及一些接口的包装
-
应用 place 参数, 批改数据格式
- Ndarray: 格局为 numpy.ndarray 格局
- Mat: 和 numpy 基本一致
- 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.Mat) # 应用 Mat
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
: 特色点的聚类 IDoctave
: 特色点在图像金字塔的层级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 用不了)
-
从游戏上讲, 咱们事后晓得一些控件, 在屏幕中的坐标地位. 分辨率进行转换时, 咱们能够通过计算控件的地位, 裁剪对应地位的图像, 通过模板匹配进行疾速的辨认.
- 举个例子, 上面两张图, 一个是 1280×720 下的截图, 一个是 2532×1170 下的截图
- 1280×720 下邮件控件的坐标范畴是
Rect(372,69,537,583)
- 通过上面的计算形式, 咱们能够得出 2532×1170 下, 范畴是
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)备注
有其余疑难的话,能够在 testerhome 的游戏测试 qq 群里找到我 581529846
原文由 hakaboom 发表于 TesterHome 社区,点击原文链接可与作者间接交换。
今日份的常识已摄入~
想理解更多前沿测试开发技术:欢送关注「第十届 MTSC 大会·上海」>>>
1 个主会场 +12 大专场,大咖星散精英齐聚
12 个专场包含:
知乎、OpenHarmony、开源、游戏、酷家乐、音视频、客户端、服务端、数字经济、效力晋升、品质保障、智能化测试