原文由hakaboom发表于TesterHome社区,点击原文链接可与作者间接交换。

1)前言

从18年开始,我接触了叉叉助手(平台曾经被请喝茶了),通过图色辨认,用来给常玩的游戏写挂机脚本,写了也有两三年.也算是我转行当游戏测试的理由.
去年11月,也是用了这身技术,混进了外包,薪资还不错,属于是混日子了,岗位是在发行,接触到很多游戏,因为接不了poco,到手只有apk,
与日俱增,游戏越来越多,项目组却还是只有这点人.为了加重本人的压力,就开始了UI自动化的不归路.

2)游戏UI自动化

因为游戏引擎,是无奈通过appium等框架去获取,如果不接入一些SDK,那么辨认的办法只有图像识别.当初常见的开源框架

  1. 网易的Airtest,通过传统辨认进行自动化,还有airtestIDE能够简略疾速的编写airtest代码
  2. 腾讯GameAISDK,通过深度学习进行自动化(没用过,良久不保护了)
  3. 阿里的SoloPi,次要性能是录制、群控,有图像匹配辅助

图像相干的常见办法:

  1. 传统的识别方法: 特色点、模板、轮廓

    • 特色点: SIFT, ORB

      • 下文会具体讲
    • 模板匹配: opencv的matchTemplate

      • 最简略的计划,通过讲模板在指标图像中平移,找到最合乎的指标
    • 轮廓: HALCON Shape-based Matching, Canny

      • 没用过,写不来,halcon的要花钱
  2. 基于深度学习的办法:

    • 文字辨认: PaddleOCR,tesseract

      • paddleOCR基本上开箱即用,然而对于游戏内的艺术字,还须要额定的训练
    • 图像分类: paddleClas

      • 没有理论用过,感觉能够用在辨别场景,而后去做更加具体的辨认.比方辨认弹窗
    • 指标检测: yolo

      • 之前很火的Fps外挂,根本就是靠这个去辨认人体

UI自动化的外围在于查找元素,并且在什么地位.那么重点就会放在图像识别上.
基于深度学习的计划,须要大量的正负样本和标注工作,因而只能放弃.取而代之的是传统的辨认计划.
在社区里、qq的测试群里就能发现,大多数人对传统图像识别的印象是:慢,不准.
今年过年前,去张江面试过一家游戏公司,也是发行公司,聊了一个多小时,聊下来他们的计划是airtest一种机型截一个图去做适配.我大受震撼.
总结下来图像识别的UI自动化难点:

  1. 辨认慢
  2. 辨认后果不精确
  3. 多分辨率不兼容性
  4. 游戏UI更新,治理图片库的老本

3)怎么解决

那么我做了什么,我的项目就在这里:https://github.com/hakaboom/p...
目前也是在重构,重构实现后可能起个好名字:https://github.com/hakaboom/i...

一开始是参考了airtest的aircv局部,过后不想有那么多依赖,就拆出来了.
重构之后,通过对opencv一些api的封装,从新组织了构架和算法.目前成果感觉不错,也曾经给airtest提了pr,后续也会推动合并.

装置opencv-python

倡议版本能够是4.5.5

  1. pypi上有编译好的,然而只能用cpu办法:

    • pip install opencv-python
    • pip install opencv-contrib-python
  2. 从源码编译,能够自定义更多的货色,比方减少cuda反对

    • 先从opencv仓库克隆代码
    • 剩下的看这里 https://github.com/hakaboom/p...

什么是特色点

简略的了解: 用于形容图像特色的关键点

常见的特色点提取算法:

  1. SIFT: 尺度不变特色变换. opencv只有cpu实现
  2. SURF: surf的减速算法. opencv有cpu和cuda实现
  3. ORB: 应用FAST特色检测和BRIEF特征描述子. opencv有cpu和cuda实现

他们的益处是什么: 尺度和旋转不变性,说白了就是兼容不同分辨率、旋转、尺度的变换
速度排序: ORB(cuda)>SURF(cuda)>ORB>SURF>SIFT
成果排序(成果不止是特色点的数量,更重要的是特色点的品质): SIFT>ORB>SURF

例子

  • 6.png(2532x1170)iphone12pro上的截图
  • 4.png(1922x1118 理论游戏渲染是1920x1080,多进去的是windows边框)崩三桌面端的截图, 裁剪了右上角的蓝色加号区域当模板
import cv2import timefrom baseImage import Image, Rectfrom image_registration.matching import SIFTmatch = 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.依据透视变换或坐标计算,获取矩形,而后计算置信度

那么以上步骤会存在什么问题

  1. 在第二步,假如图片中存在n个指标图片,那么还是k=2的话,就会导致特色点数量变少
  2. 在第三步,筛选的办法不太正当,理论debug中会发现,一些特色点即便distance数值很高,但从后果上看,还是合乎指标的,那么就意味着单纯依据间隔去筛选特色点
    的办法是不靠谱的
  3. 在第四步,获取完特色点后,airtest的形式是,依据透视变换获取指标的四个顶点坐标,计算出最小外接矩形.
    那么如果指标图片存在旋转/形变,那么最初获取的图片会裁剪到多余指标,造成置信度升高

既然airtest存在这些问题,那么我做了什么改变,我把步骤一个个拆分

我的特色点匹配

1.读取图片

from baseImage import Imageim_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 Imagefrom baseImage.constant import Place    Image(data='tests/image/0.png', place=Place.Ndarray)  # 应用numpyImage(data='tests/image/0.png', place=Place.Mat)  # 应用MatImage(data='tests/image/0.png', place=Place.UMat)  # 应用UmatImage(data='tests/image/0.png', place=Place.GpuMat)  # 应用cuda

2.创立特色点检测类
这边会有一些参数,除了threshold(过滤阈值)、rgb(是否通过rgb通道检测)认为,还有能够退出特色点提取器的一些配置,个别默认就好,具体能够查opencv文档

from image_registration.matching import SIFTmatch = SIFT(threshold=0.8, rgb=True, nfeatures=50000)

3.辨认

from image_registration.matching import SIFTfrom baseImage import Image, Rectim_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对应的queryIdxtrainIdx的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)怎么优化速度

  1. airtest慢的一个起因在于,只用了cpu计算.如果能开释算力到gpu上,速度就会有成倍的增长.

    opencv曾经给咱们做好了很多接口.咱们能够通过cv2.cuda.GpuMat, cv2.UMat调用cuda和opencl的算法.

    通过baseImage能够疾速的创立对应格局的图像
from baseImage import Imagefrom 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
  • matchTemplateimage_registration.matching.template.matchTemplate.CudaMatchTemplate

能够用opencl减速的识别方法, 只须要传图像参数的时候,格局是UMat,opencv会主动的调用opencl办法

  • surf
  • orb
  • matchTemplate

这边只讲了特色点获取/模板匹配的办法,在其余的图像处理函数中cudaopencl也能有肯定的减速,然而不如以上办法显著

  1. 从框架设计上进行减速.(可能只限于游戏利用,传统app用不了)
  2. 从游戏上讲,咱们事后晓得一些控件,在屏幕中的坐标地位.分辨率进行转换时,咱们能够通过计算控件的地位,裁剪对应地位的图像,通过模板匹配进行疾速的辨认.

    • 举个例子,上面两张图,一个是1280x720下的截图,一个是2532x1170下的截图
    • 1280x720下邮件控件的坐标范畴是Rect(372,69,537,583)
    • 通过上面的计算形式,咱们能够得出2532x1170下,范畴是Rect(828,110,874,949),通过裁剪软件获得的范畴是Rect(830,112,874,948)
    • 具体的原理是利用了,引擎的缩放和锚点原理,反向求出坐标范畴.去适应一些黑边,刘海的状况.
    • 求出范畴后,裁剪范畴的图片,和模板去做匹配,就能够疾速的辨认一些固定地位的控件
from baseImage import Rectfrom baseImage.coordinate import Anchor, screen_display_type, scale_mode_typeanchor = 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]]


  1. 建设模板库,事后加载模板,失去屏幕图片后,通过一些类似度计算baseImage.utils.ssim对场景进行辨认与分类,而后去辨认相应场景的特色点.用这样的办法去缩小计算量

    • 这边其实有想法去扩大到深度学习,比方之前说的图像分类.首先咱们建设了一个很大的模板库,能够拆分进去界面1, 界面2,界面3和一些通用控件
    • 再通过分类去取得以后在什么界面,而后只辨认这个界面的控件,达到缩小计算量的作用

6)备注

有其余疑难的话,能够在testerhome的游戏测试qq群里找到我581529846

原文由hakaboom发表于TesterHome社区,点击原文链接可与作者间接交换。


今日份的常识已摄入~
想理解更多前沿测试开发技术:欢送关注「第十届MTSC大会·上海」>>>
1个主会场+12大专场,大咖星散精英齐聚
12个专场包含:
知乎、OpenHarmony、开源、游戏、酷家乐、音视频、客户端、服务端、数字经济、效力晋升、品质保障、智能化测试