背景:最近在调研AirTest这个测试工具,据说最近很火,同事说很多平台都在推,但是同事使用AirTest的IDE生成了代码,然后发现在不同分辨率下,window大小不同的情况下,操作一些小的控件,并不是很稳定,后来我们就调研了一下AirTest的图像识别。
1. 如何使用AirTest IDE
1.1 环境搭建:
如果你只想使用AirTest IDE,这就很简单了,你只要下载AirTest IDE, 就可以直接使用了,IDE里面已经把需要的包都已经包含在内了,无需别的操作
解压后的IDE 文件夹:
执行里面的AirtestIDE.exe 就可以启动AirTest IDE了。
1.2 使用 AirTest IDE
关于如何使用AirTest IDE, 官网已经非常详细,操作也比较简单,相信大家去简单看一下就会知道如何使用,就不多叙述了。
这里是一些官网的链接,大家可以参考一下:
2. 查看AirTest 的测试代码
2.1 使用AirTest IDE编写测试代码
假设大家都已经会使用AirTest IDE了,现在展示一下最简单的代码,touch一下:
2.2 代码解释
大家看到代码其实非常简单,第四行是从airtest的包中导入你需要的内容,第六行是进行一个touch操作。
在IDE中展示出来的是touch传入一个图片,后面我们对touch的整个流程进行分析,看看怎么能提高点击的可靠性。
3. touch方法剖析
3.1 touch代码解析
在AirTest IDE中,你看见的传入一张图片,如果你用别的IDE来打开,你看见的是如下的代码:
1 # -*- encoding=utf8 -*- 2 __author__ = "Test" 3 4 from airtest.core.api import * 5 6 touch(Template(r"tpl1583817736399.png", record_pos=(2.282, 4.579), resolution=(500, 424)))
如上所示, 真正的代码中touch传入的是一个 Template的对象,后面我们来一步一步的详细的分析一下
3.2 touch 方法源码解析
第一步,来看一下touch的源码

1 @logwrap 2 def touch(v, times=1, **kwargs): 3 """ 4 Perform the touch action on the device screen 5 6 :param v: target to touch, either a Template instance or absolute coordinates (x, y) 7 :param times: how many touches to be performed 8 :param kwargs: platform specific `kwargs`, please refer to corresponding docs 9 :return: finial position to be clicked 10 :platforms: Android, Windows, iOS 11 """ 12 if isinstance(v, Template): 13 pos = loop_find(v, timeout=ST.FIND_TIMEOUT) 14 else: 15 try_log_screen() 16 pos = v 17 for _ in range(times): 18 G.DEVICE.touch(pos, **kwargs) 19 time.sleep(0.05) 20 delay_after_operation() 21 return pos
大家看到源码中有几个参数,下面也有比较清楚的解释,v是一个目标对象,一个template对象或者是一个绝对坐标,times是你点击多少次,后面的kwargs是传给对应的device.touch的参数
好了,我们在往下看,如果传的是坐标,那就直接点击,如果不是坐标,那么来了,loop_find, 这应该是识图了,后面的点击是根据这个识图的返回值来操作了,那我们就来看一下这个方法。

1 if isinstance(v, Template): 2 pos = loop_find(v, timeout=ST.FIND_TIMEOUT)
touch在拿到需要的坐标之后,使用G.DEVICE.touch(pos, **kwargs) 来调用对应的平台驱动来实现点击事件,在这里就不深入讨论了,后面探讨一下识图问题。
4. 识图解析
4.1 loop_find源码解析

1 @logwrap 2 def loop_find(query, timeout=ST.FIND_TIMEOUT, threshold=None, interval=0.5, intervalfunc=None): 3 """ 4 Search for image template in the screen until timeout 5 6 Args: 7 query: image template to be found in screenshot 8 timeout: time interval how long to look for the image template 9 threshold: default is None 10 interval: sleep interval before next attempt to find the image template 11 intervalfunc: function that is executed after unsuccessful attempt to find the image template 12 13 Raises: 14 TargetNotFoundError: when image template is not found in screenshot 15 16 Returns: 17 TargetNotFoundError if image template not found, otherwise returns the position where the image template has 18 been found in screenshot 19 20 """ 21 G.LOGGING.info("Try finding:\n%s", query) 22 start_time = time.time() 23 while True: 24 screen = G.DEVICE.snapshot(filename=None, quality=ST.SNAPSHOT_QUALITY) 25 26 if screen is None: 27 G.LOGGING.warning("Screen is None, may be locked") 28 else: 29 if threshold: 30 query.threshold = threshold 31 match_pos = query.match_in(screen) 32 if match_pos: 33 try_log_screen(screen) 34 return match_pos 35 36 if intervalfunc is not None: 37 intervalfunc() 38 39 # ???raise,??????????: 40 if (time.time() - start_time) > timeout: 41 try_log_screen(screen) 42 raise TargetNotFoundError('Picture %s not found in screen' % query) 43 else: 44 time.sleep(interval)
参数解析:
- query:你想要查找的图片的template
- timeout: 你查找的图片的超时时间
- threshold:你查图的阈值,就是打分了,你想找的图片在整个的图像里面的打分,超过就算识别到,低于就是没识别到,你可以降低这个阈值,但是也有可能导致找错,按照你的需求来,不给的话会有一个默认值
- interval:查图有时并不是一次,会等待一段时间后继续查询
- intervalfunc:查图失败之后做的一些操作,当前不讨论, 在touch函数中并不涉及此参数
流程解析:
loop_find所做的操作流程是这样的:
- 先拿到当前操作界面的整个截图
- 拿你传入的截图template来进行匹配
- 拿匹配值和当前的期望阈值进行比价
- 比较成功则返回预期坐标,失败则等待后继续匹配
- 若匹配超时,最终返回异常
由上述流程来看,其核心操作其实就是在用你的template进行匹配,其代码如下所示

1 if threshold: 2 query.threshold = threshold 3 match_pos = query.match_in(screen)
大家看到,图像匹配其实在match_in方法之中,我们再来看下这个方法。
4.2 match_in源码解析
match_in这个方法是从哪来的呢,从参数来看,是query的方法,query是什么,是template,那就看下这个方法的源码:

1 def match_in(self, screen): 2 match_result = self._cv_match(screen) 3 G.LOGGING.debug("match result: %s", match_result) 4 if not match_result: 5 return None 6 focus_pos = TargetPos().getXY(match_result, self.target_pos) 7 return focus_pos
源码中最主要的是match_result, 后面都是对该变量的处理,那图像匹配的方法就在_cv_match这个方法里面了,下面再看下_cv_match的源码。
4.3 _cv_match源码解析

1 @logwrap 2 def _cv_match(self, screen): 3 # in case image file not exist in current directory: 4 image = self._imread() 5 image = self._resize_image(image, screen, ST.RESIZE_METHOD) 6 ret = None 7 for method in ST.CVSTRATEGY: 8 # get function definition and execute: 9 func = MATCHING_METHODS.get(method, None) 10 if func is None: 11 raise InvalidMatchingMethodError("Undefined method in CVSTRATEGY: '%s', try 'kaze'/'brisk'/'akaze'/'orb'/'surf'/'sift'/'brief' instead." % method) 12 else: 13 ret = self._try_match(func, image, screen, threshold=self.threshold, rgb=self.rgb) 14 if ret: 15 break 16 return ret
根据源码,我们可以看到,整个匹配的核心流程就是先resize image然后根据ST.CVSTRATEGY里面所包含的策略方法进行轮训匹配, 最后返回匹配结果。
ST.CVSTRATEGY里面的方法都是在open cv的基础上进行封装的,是不变的模式,没有办法调整,那么我们能调整的是什么,是image resize 这个行为,图片到底是怎么resize的,我们来进一步看一下
4.4 _resize_image源码解析

1 def _resize_image(self, image, screen, resize_method): 2 """模板匹配中,将输入的截图适配成 等待模板匹配的截图.""" 3 # 未记录录制分辨率,跳过 4 if not self.resolution: 5 return image 6 screen_resolution = aircv.get_resolution(screen) 7 # 如果分辨率一致,则不需要进行im_search的适配: 8 if tuple(self.resolution) == tuple(screen_resolution) or resize_method is None: 9 return image 10 if isinstance(resize_method, types.MethodType): 11 resize_method = resize_method.__func__ 12 # 分辨率不一致则进行适配,默认使用cocos_min_strategy: 13 h, w = image.shape[:2] 14 w_re, h_re = resize_method(w, h, self.resolution, screen_resolution) 15 # 确保w_re和h_re > 0, 至少有1个像素: 16 w_re, h_re = max(1, w_re), max(1, h_re) 17 # 调试代码: 输出调试信息. 18 G.LOGGING.debug("resize: (%s, %s)->(%s, %s), resolution: %s=>%s" % ( 19 w, h, w_re, h_re, self.resolution, screen_resolution)) 20 # 进行图片缩放: 21 image = cv2.resize(image, (w_re, h_re)) 22 return image
通过源码,其实注释已经很清楚了,如果你没有记录了截图时的分辨率,那么就会用原图的原始比例在window的截图上匹配,如果记录了分辨率,会根据你的分辨率和当前window截图的分辨率根据策略来进行图片缩放。
那这个就简单了,核心就是说怎么才能让你的截图在不同的分辨率下更接近当前的window截图中的图片,后面我们来分析一下。
5. 优化图片缩放
5.1 图片缩放策略
当你测试的window分辨率变化时,图片大体上会有几种变化,如果还有别的模式,欢迎补充。
- 图片不随window的变化而变化
- 图片随window的变化而同比例变化
- 图片有不同的尺寸,在window特定的尺寸范围内变化
- 图片随window特定尺寸变化,且尺寸有缩放 (有种window,在不同尺寸下,整体布局也不一样,这种操作就比较复杂,我就描述了。)
5.2 测试前提条件
现在我们来模拟一下测试场景。
我打开了Microsoft Store的一个game 页面,如下图,在图片最大时,在图中截取了一块图片,后面开始匹配。
此图片在不同的window大小情况下会呈现不同的大小,上述第三种情况。
测试代码如下所示:
1 from vendor.airtest.core.api import Template, touch, auto_setup, connect_device, loop_find 2 3 auto_setup(__file__) 4 connect_device("Windows:///?title_re=.*Microsoft Store.*") 5 6 pos = loop_find(Template("./data/h.png", record_pos=(1.269, 0.013), resolution=(820, 679)))
我先连接了Microsoft Store的window,然后去查找我截图的部分,看下匹配结果。(使用loop_find来替换touch,结果更清晰。)
5.3 测试策略优化
上面谈到了图片的几种变化,我们就来根据图片的变化来优化我们的代码,来看一下效果如何。
- 场景一: window放大,图片尺寸不变
我这次放大了window的尺寸,然后看一下匹配的结果是多少。
1 threshold=0.7, result={'confidence': 0.45194211602211, 'result': (129, 499), 'rectangle': ((105, 477), (105, 521), (154, 521), (154, 477))}
如上所示,匹配结果只有0.45,远小于0.7的期望值,查询失败了。
我放大了size,但是图片不变,其中的resize执行之后就匹配不到了,这不太科学,那我们这次改变测试代码,不传入分辨率。
1 pos = loop_find(Template("./data/h.png", record_pos=(1.269, 0.013)))
结果如下所示:
1 threshold=0.7, result={'confidence': 1.0, 'result': (129, 500), 'rectangle': ((110, 483), (110, 517), (148, 517), (148, 483))}
嗯,1.0完成,也就是百分百匹配到,也就是说对于window尺寸来说,不变化的图像,就删除分辨率属性,保持原有尺寸,不进行缩放来匹配,这样的效果会比较好。
- 场景二:图片尺寸随着window的变化而变化
由于Microsoft Store上面没有随window比例变化的图片,我就把图片截图然后放在相册里来测试,因为相册不是完全随比例变化的,所有查询精度会变低,但是不影响结果,如下所示:
1 threshold=0.7, result={'confidence': 0.96263587474823, 'result': (98, 227), 'rectangle': ((85, 217), (85, 238), (111, 238), (111, 217))}
这说明如果是随比例缩放的图片,就保持AirTest IDE中的代码,保持分辨率参数,就可以很好的查找图片了。
- 场景三:图片随window变化而伴随几种特定尺寸
这种就比较麻烦了,第一不是不变的,第二不是随比例变化的。
我推荐的策略是:
第一,测试过程中先将你的window resize到固定的尺寸大小,这样测试也会相对稳定
第二,如果window并不能固定,那我推荐把图片变成一个变量,根据不同的分辨率,先初始化一套符合当前的图片组,然后进行测试
具体哪套更合适你,或者有更好的方式,可以留言,大家共同探讨一下。
以上就是关于AirTest识图准确性的一点个人看法,欢迎探讨。