四、Airtest的图像脚本介绍

1. Airtest图像脚本的本质

在AirtestIDE编写Airtest脚本时,我们最常见的就是类似这样的图像脚本:

image-20211029150105905

其实它本质上还是一条Python脚本,只不过Airtest把图像封装起来了,并且可以直接显示图像,更方便同学们查看:

  1. touch(Template(r"tpl1635489343794.png", record_pos=(-0.365, 0.228), resolution=(1080, 2280)))

其中 Template(r"tpl1635489343794.png", record_pos=(-0.365, 0.228), resolution=(1080, 2280))就是Airtest封装的图像类。

2. 图像识别的配置项

我们可以直接在 Airtest API 文档搜索template来查看这个图像类的配置项:

image-20211029151326515

  • filename:构建Template实例的 必填参数 ,可以是相对路径或者绝对路径(使用AirtestIDE截图时会存放在默认路径下)
  • threshold:图像识别的阙值,只有当图像识别结果的可信度大于阙值,才算找到图像识别的结果;阈值越高,对图像匹配的精度要求也就越高
  • target_pos:用于设置图像的点击位置,为1-9的整数,默认值为5(即图像中心,1为图像左上角位置,9为图像右下角位置)
  • record_pos:计算坐标对应的中点偏移值相对于分辨率的百分比,匹配时会优先匹配这附近的画面(使用AirtestIDE截图时会自动记录该参数值)
  • resolution:记录了截图时的手机分辨率(使用AirtestIDE截图时会自动记录当时的手机分辨率)
  • rgb:强制使用彩色图像识别(未设置 rgb 参数时,Airtest会将图片转成灰度图之后才进行图像识别)
  • scale_max:Airtest1.2.0新增算法mstpl的专属参数,用于调节匹配的最大范围,默认值800, 取值范围 [700 , 2000],推荐值 740, 800, 1000
  • scale_step:Airtest1.2.0新增算法mstpl的专属参数,用于控制搜索比例步长,它代表匹配时搜索的精细程度。进行图像匹配时,会在原始截图的一定缩放范围内以截图最长边 * scale_step 的步长进行搜索。默认值0.01,取值范围 [0.001, 0.1],推荐值 0.02, 0.005, 0.001

filename 之外的所有参数,调节之后都会或多或少影响我们的图像识别效果,所以如果我们了解并善于利用这些参数,就可以使得图像识别结果更加符合我们的期望。

3. Airtest的图像识别算法

Airtest图像脚本的上手并不复杂,可以说只要有一点Python脚本基础就能够轻松入门,然而随着产出的脚本代码越来越多,我们将会发现一个很严峻的问题:我们的脚本的运行结果,有时候似乎不受我们的控制。例如我们想要先判断当前画面上是否存在一个图标,只有在它存在时才进行下一步操作,然而明明画面上并没有,Airtest依然认为这个图标存在,打开报告一看才发现,它将屏幕上另外一个区域的内容判断认为是我们想找的图标了。再比如更加常见的一种情形是:我们框选了画面上的几个字,想要让它识别出来,但是运行结果时好时坏,Airtest常常认为我们选择的内容并不存在,但有时又能运行成功。

这个问题是Airtest的运行原理导致的,我们使用了 图像识别的技术 在当前设备画面中查找对应的图片,然而图像识别并不能达到人眼识别的准确度,它只能尽可能地去寻找一个最符合预期的结果。这就经常导致了我们认为不存在的图片,它认为存在,或者 我们认为在画面上一眼就能找到的内容,Airtest认为不存在

1)Airtest的图像识别算法介绍

那Airtest在图像识别过程中,涉及到哪些图像识别算法呢?我们从执行1条touch脚本的log来看一下:

image-20211029160224639

在Airtest1.2.0版本之后,图像识别过程中,会使用这MultiScaleTemplateMatchingPreTemplateMatchingSURFMatchingBRISKMatching这4个算法依次进行图像识别,找到结果将停止识别,未找到结果将会一直按照这个算法的识别顺序一直循环识别直到超时。

这些算法有的是模板匹配算法,也有的是特征点匹配算法,关于Airtest使用的图像识别算法的详细介绍,我们可以参考下面2篇推文:

2)程序如何判定图像识别成功/失败

那图像识别算法找到匹配结果之后,程序如何判断这次的匹配结果是否通过,也就是此次的图像识别是成功还是失败的呢?

这里需要介绍两个很重要的名词:阙值 (上文的配置项中有提到)和 可信度 ,他们的取值范围都是[0,1]。在每一条图像识别的脚本中,都会有1个用于结果筛选的阙值,默认值为0.7。

当上述几种算法在执行过程中识别到初始结果时,就会计算出来这个初始结果的可信度,当 可信度>阙值 的时候,程序会认为 找到了最佳的匹配结果 ;而当 可信度<阙值 的时候,程序则会认为 没有找到最佳的匹配结果

我们可以在执行截图脚本的时候,查看log窗口,观察算法识别结果的可信度:

① 可信度>阙值,程序判定找到匹配结果

image-20211029162022455

② 可信度<阙值,程序判定未找到匹配结果,循环用三种算法继续查找直到超时

image-20211029161820146

3)如何修改默认的图像识别算法

如果我们想修改默认的算法查找顺序,或者指定其中某些算法来进行图像识别,我们可以通过下述方式修改Airtest默认的算法:

  1. from airtest.core.settings import Settings as ST
  2. ST.CVSTRATEGY = ["tpl", "sift","brisk"]

4. 进阶:如何提高图像脚本的兼容性

通过上文的内容,我们可以了解到一点:图像识别不是万能的!!! 它是有 成功率 的,假设一个脚本里有10张图片,每张图片的识别成功率都能达到95%之高,10张图片全部都正确识别的概率也不过60%而已。更何况,有许多图片因为种种原因,识别成功率远远低于95%这个值,要想脚本运行100%正确就更难了。

因此,我们在编写完脚本后,可以让脚本多运行几次,然后对成功率低的部分进行改进,提高图像脚本的成功率(兼容性),以下是编写图像脚本的一些技巧:

1)截取图标时尽量不要截入过多的背景内容

举个简单的例子,比如我们想通过点击网易云音乐的应用图标来打开网易云音乐的app,为了能在不同设备上都有更好的识别结果,我们应该尽可能选取下图中第一种截图,而不是混入了过多背景的第二种截图:

5

为了让大家直观地看到差别,我们在设备1上截好了上述俩张图,然后分别在设备2中执行,结果如下:

6

7

可以看到,没有截入过多背景的截图,识别出来的可信度高达0.95;而截入了背景的图标截图,可信度下降到了0.88。所以在截取这些特定图标的时候,尽量减少截入背景,可以有效提高这类截图脚本的兼容性。

2)尽量减少截取纯文字的图像

Airtest图像识别使用的算法 更适合用来识别按钮类(带边框)、图标类的图像 ,仅仅单独截取几个文字容易导致识别成功率较低,请尽量调整图片截取内容来达到较好的识别效果,避免截取识别效果较差的内容:

image-20211029164246766

如图,下面那种截图就比上面的纯文字截图要好很多。

3)合理调整阙值

上文我们就提到过阙值,它起到结果筛选的作用。也就是说,如果我们设置的阙值过低,就更容易让错误的结果通过;而阙值设置得过高,就有可能把可信度达不到要求的正确结果也过滤掉,导致很难得出有效的识别结果。

所以我们可以通过合理调整阙值大小,更好地过滤出我们想要的识别结果。举个例子,某个截图默认阙值为0.7,但是我们多次运行之后发现,有一定概率会识别到错误结果。这时候我们不妨将阙值调高一点试试,看看能不能提高正确识别的概率,如果可以,说明我们的阙值调整是有效的。

在IDE中,我们可以双击截图打开图片编辑器,在右侧修改截图的阙值:

图片

设置好并关掉图片编辑器后,我们在脚本编写窗口右键切换成代码模式,可以看到刚才那条截图脚本多了个 threshold=0.8 参数:

  1. touch(Template(r"tpl1598952570968.png", threshold=0.8, record_pos=(-0.021, 0.121), resolution=(900.0, 1600.0)))

当然,我们也可以设置全局的 threshold

  1. from airtest.core.setting import Settings as ST
  2. ST.THRESHOLD = 0.7 # 其他语句的默认阈值

不过上述的修改方式只适用于除断言语句之外的截图语句,如果在断言语句的截图中,双击进入图片编辑器,再修改里面的 threshold ,最终也是不会生效的。因为断言语句的阙值与其它截图语句的阙值是不一样的,它只能通过下述方式进行设置:

  1. from airtest.core.setting import Settings as ST
  2. ST.THRESHOLD_STRICT = 0.7

4)开启RGB彩色识别

在识别图像时,Airtest会先将图像转为灰度图再进行识别。因此假如有两个按钮,形状内容相同,只有颜色不同的情况下,Airtest将认为它们都是相同内容。如下图,如果仅截图第二个红色的【删除】按钮,Airtest会把另外俩个灰黑色的【删除】按钮认为是相同的。 图片 通过勾选rgb选项(双击图片打开图片管理器勾选),或在代码中加入rgb=True,我们可以强制指定使用彩色图像进行识别。这样就能比较好地识别出那个红色的【删除】按钮了。

  1. touch(Template(r"tpl1637052349130.png", rgb=True, record_pos=(-0.38, -0.11), resolution=(850, 909)))

5)巧用target_pos点击截图的不同位置

先来了解下什么是 target_pos 。默认情况下,我们的截图脚本都是点击截图的中心位置,即 target_pos=5 。对于一张截图来说,总共有9个 target_pos ,当我们把截图的 target_pos 设置成不同的值时,脚本会点击在截图不同的位置上:

图片

双击IDE中的截图即可打开图片编辑器,右侧可以修改 target_pos 的值:

图片

修改完成之后,把截图脚本切换成代码模式,我们就可以看到此时的截图脚本里面多了 target_pos 这个参数:

  1. touch(Template(r"tpl1598948415043.png", target_pos=6, record_pos=(-0.434, -0.773), resolution=(900, 1600)))

我们在做自动化的时候,经常会遇到某个图标堆叠的情况,比如在网易云音乐的某个歌曲列表中,右侧便罗列了三个完全一样的播放按钮:

图片

如果我们的需求是点击中间那个按钮的,仅仅依靠截1个播放按钮来做识别,是很难保证具体识别到三个按钮中的具体哪个按钮的。

此时我们可以有2种截图方式来实现,一种是在竖着的方向上,扩大截图范围,让中间那个按钮处于 target_pos=5 的位置上:

图片

另一种是在横着的方向上,扩大截图范围把左侧的歌曲介绍也一起截图了,让中间那个按钮处于 target_pos=6 的位置上:

图片

这2种方式都可以确保我们点到的是中间那个按钮(假设列表歌曲不变的情况下)。

所以,当精准截图(仅截取某个按钮/图标)不能满足唯一定位时,我们可以考虑加大截图范围,增加更多的特征点,确保截图定位的准确性。

6)切勿过度依赖录制功能

IDE自带的录制功能,可以帮助我们的新手同学快速上手Airtest这个基于图像识别的测试框架,但是自动录制出来的截图语句,并不是都会很符合我们的实际需求,所以我们不能过度依赖录制功能。

我们可以在录制完毕之后,检查下有哪些截图并不是截的很好的,自己再手动截取一下,以提升整个脚本的兼容性。

7)指定游戏的分辨率适配规则

在使用不同分辨率的设备进行图像识别时,可能会导致识别成功率不佳,因此Airtest提供了默认的分辨率适配规则(使用的是Cocos引擎的默认缩放规则),代码在这里。

想要提高2d游戏的识别精度,最好的办法就是明确指定你的游戏的分辨率适配规则,例如,直接在.air脚本文件的开头这样写:

  1. from airtest.core.api import *
  2. def custom_resize_method(w, h, sch_resolution, src_resolution):
  3. return int(w), int(h)
  4. # 替换默认的RESIZE_METHOD
  5. ST.RESIZE_METHOD = custom_resize_method

上面的代码指定了一个自定义的缩放规则:直接return原来的值,不管屏幕分辨率,所有UI都不进行缩放(有的游戏就是这种策略)。

这里的RESIZE_METHOD,即我们定义的custom_resize_method使用的输入参数为:

  • w, h # 录制下来的UI图片的宽高
  • sch_resolution # 录制时的屏幕分辨率
  • src_resolution # 回放时的屏幕分辨率

输出为:

  • 回放时的UI图片宽高

若要自定义你的RESIZE_METHOD,只需要知道你测试的游戏的缩放规则,然后在custom_resize_method中用代码实现即可。这样做,能够大大提升不同分辨率设备下的图像识别成功率。

8)自定义语句提高图像脚本兼容性

对于设备长宽比不同、设备分辨率不同、多种字体的情况,我们可以通过语法来提高兼容性。这种方式需要连接上脚本兼容性有问题的设备,把对应截图纳入搜索列表。代码脚本如下:

  1. picList = [pic1,pic2,pic3] # 截图的图片对象列表
  2. for pic in picList:
  3. pos = exists(pic)
  4. if pos:
  5. touch(pos)
  6. break # 只要找到图片列表中的任何一张图片,就执行touch

注意:如果for循环中没有break语句,会导致次逻辑运行时将所有的图片都找一遍(找到后执行touch),而非找到合适结果立即返回。

9)巧用坐标进行点击/滑动

有时候,我们在打开一个app时,会遇到一些过场动画或者是几张应用的介绍页。这些过场动画和介绍页可能会随着版本更新而变化,那么利用截图点击,可能需要花费我们比较大的精力去维护这些截图脚本。

其实这时候我们完全可以用坐标点击来替代截图点击,因为这些过场动画或者介绍页,只要有任意的点击动作,都可以跳过。

在比如说网易云音乐首页的轮播图,可能每天登录上去都是不一样的,如果我们用截图脚本来滑动/点击,那天天都需要维护这些脚本,还不如替换成坐标滑动/点击,更加省心省力:

图片

10)巧用keyevent(“BACK”)替代返回的截图脚本

很多时候,我们需要从APP的某个页面,回到APP首页,一些同学可能会使用一堆的返回图标的截图语句,来实现这个需求:

图片

实际上,如果同学们测的是安卓设备,完全可以用 keyevent("BACK") 来替代这个返回的截图语句,更加稳定高效:

图片

11)画面切换的时候,可以多使用wait或者sleep,再进行点击操作

很多新手同学都很容易犯1个错误,就是一不小心就写了很多连续点击操作;其实在每一个点击操作之后,应用画面也是在实时变化的。如果画面正在加载的时候,下一个点击操作就被执行了,就会很容易导致识别到错误位置或者识别超时。

举个例子,进入网易云音乐的app时,我们同意了服务条款之后,会有1个很长的启动动画,我们只有等待启动动画结束之后,才能够进行下一步的点击 “立即体验” 的操作,否则这个点击操作很可能因为在等待启动动画的过程中而识别超时:

图片

图片

另外,为保证连续点击都能够正常被执行,我们还可以在连续点击之间用 sleep(1.0) 来缓冲下,减少画面切换对连续点击操作的影响。

12)打开应用尽量使用start_app而不是截图脚本

start_app() 支持Android和iOS设备,相对用截图脚本来启动应用,脚本会更加简洁,兼容性也会更好:

  1. # 打开网易云音乐
  2. start_app("com.netease.cloudmusic")

13)如果可以用poco框架,还可以用poco语句代替截图脚本

如果同学们测试的项目可以使用poco框架,建议大家在自动化脚本的时候,可以灵活混用Airtest和Poco脚本,以帮助同学们的脚本达成更好的兼容性:

举个例子,在网易云音乐的某个歌单中,想选择前10首歌曲,如果用截图脚本的话,需要编写10条截图脚本,但如果用poco框架的话,仅需要几行遍历节点的脚本(以选择前3首歌曲为例):

图片

并且当歌曲名称变化时,脚本截图也需要跟着维护;这时候选择不变的节点作为操作对象,显然可以提升我们脚本的兼容性。