AirTest : 关于识别效果的探索


背景:最近在调研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, 官网已经非常详细,操作也比较简单,相信大家去简单看一下就会知道如何使用,就不多叙述了。

这里是一些官网的链接,大家可以参考一下:

5分钟上手自动化测试

AirtestIDE使用文档

快速上手系列教程

 

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
touch源码

大家看到源码中有几个参数,下面也有比较清楚的解释,v是一个目标对象,一个template对象或者是一个绝对坐标,times是你点击多少次,后面的kwargs是传给对应的device.touch的参数

好了,我们在往下看,如果传的是坐标,那就直接点击,如果不是坐标,那么来了,loop_find, 这应该是识图了,后面的点击是根据这个识图的返回值来操作了,那我们就来看一下这个方法。

1 if isinstance(v, Template):
2         pos = loop_find(v, timeout=ST.FIND_TIMEOUT)
touch 识图部分源码

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)
loop_find源码

 参数解析:

  • 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)
loop_find图像匹配部分代码

 大家看到,图像匹配其实在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_in源码

源码中最主要的是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
_cv_match源码

根据源码,我们可以看到,整个匹配的核心流程就是先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
_resize_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识图准确性的一点个人看法,欢迎探讨。

 


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM