Airtest图像识别测试工具原理解读&最佳实践 | 京东云技术团队

1 Airtest简介

Airtest是一个跨平台的、基于图像识别的UI自动化测试框架,适用于游戏和App,支持平台有Windows、Android和iOS。Airtest框架基于一种图形脚本语言Sikuli,引用该框架后,不再需要一行行的写代码,通过截取按钮或输入框的图片,用图片组成测试场景,这种方式学习成本低,简单易上手。

2 Airtest实践

APP接入流水线过程中,赛博平台只支持air脚本,因此需要对京管家APP的UI自动化脚本进行的改造。如截图可见,AirtestIDE的主界面由菜单栏、快捷工具栏和多个窗口组成,初始布局中的“设备窗口”是工具的设备连接交互区域。
air脚本生成步骤:

  1. 通过adb连接手机或模拟器
  2. 安装应用APK
  3. 运行应用并截图
  4. 模拟用户输入(点击、滑动、按键)
  5. 卸载应用

通过以上步骤自动生成了 .air脚本,调试过程中我们可以在IDE中运行代码,支持多行运行以及单行运行,调试通过后可在本地或服务器以命令行的方式运行脚本:
.air脚本运行方式:airtest run “path to your .air dir” —device Android
.air脚本生成报告的方式:airtest report “path to your .air dir”

3 Airtest定位方式解析

IDE的log查看窗口会时时打印脚本执行的日志,从中可以看出通过图片解析执行位置的过程。下面就以touch方法为例,解析Airtest如何通过图片获取到元素位置从而触发点击操作。

1@logwrap 2def touch(v, times=1, **kwargs): 3 """ 4 Perform the touch action on the device screen 5 :param v: target to touch, either a ``Template`` instance or absolute coordinates (x, y) 6 :param times: how many touches to be performed 7 :param kwargs: platform specific `kwargs`, please refer to corresponding docs 8 :return: finial position to be clicked, e.g. (100, 100) 9 :platforms: Android, Windows, iOS 10 """ 11 if isinstance(v, Template): 12 pos = loop_find(v, timeout=ST.FIND_TIMEOUT) 13 else: 14 try_log_screen() 15 pos = v 16 for _ in range(times): 17 G.DEVICE.touch(pos, **kwargs) 18 time.sleep(0.05) 19 delay_after_operation() 20 return pos 21 22click = touch # click is alias of t

该方法通过loop_find获取坐标,然后执行点击操作 G.DEVICE.touch(pos, kwargs),接下来看loop_find如何根据模板转换为坐标。

1@logwrap 2def 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 Args: 6 query: image template to be found in screenshot 7 timeout: time interval how long to look for the image template 8 threshold: default is None 9 interval: sleep interval before next attempt to find the image template 10 intervalfunc: function that is executed after unsuccessful attempt to find the image template 11 Raises: 12 TargetNotFoundError: when image template is not found in screenshot 13 Returns: 14 TargetNotFoundError if image template not found, otherwise returns the position where the image template has 15 been found in screenshot 16 """ 17 G.LOGGING.info("Try finding: %s", query) 18 start_time = time.time() 19 while True: 20 screen = G.DEVICE.snapshot(filename=None, quality=ST.SNAPSHOT_QUALITY) 21 if screen is None: 22 G.LOGGING.warning("Screen is None, may be locked") 23 else: 24 if threshold: 25 query.threshold = threshold 26 match_pos = query.match_in(screen) 27 if match_pos: 28 try_log_screen(screen) 29 return match_pos 30 if intervalfunc is not None: 31 intervalfunc() 32 # 超时则raise,未超时则进行下次循环: 33 if (time.time() - start_time) > timeout: 34 try_log_screen(screen) 35 raise TargetNotFoundError('Picture %s not found in screen' % query) 36 else: 37 t

首先截取手机屏幕match_pos = query.match_in(screen),然后对比传参图片与截屏来获取图片所在位置match_pos = query.match_in(screen)。接下来看match_in方法的逻辑:

1def 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 = self._cv_match(screen)

1@logwrap 2def _cv_match(self, screen): 3 # in case image file not exist in current directory: 4 ori_image = self._imread() 5 image = self._resize_image(ori_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 if method in ["mstpl", "gmstpl"]: 14 ret = self._try_match(func, ori_image, screen, threshold=self.threshold, rgb=self.rgb, record_pos=self.record_pos, 15 resolution=self.resolution, scale_max=self.scale_max, scale_step=self.scale_step) 16 else: 17 ret = self._try_match(func, image, screen, threshold=self.threshold, rgb=self.rgb) 18 if ret: 19 break 20 return ret

首先读取图片调整图片尺寸,从而提升匹配成功率:
image = self._resize_image(ori_image, screen, ST.RESIZE_METHOD)
接下来是循环遍历匹配方法for method in ST.CVSTRATEGY。而ST.CVSTRATEGY的枚举值:

1CVSTRATEGY = ["mstpl", "tpl", "surf", "brisk"] 2if LooseVersion(cv2.__version__) > LooseVersion('3.4.2'): 3 CVSTRATEGY = ["mstpl", "tpl", "sift", "brisk"]

func = MATCHING_METHODS.get(method, None),func可能的取值有mstpl、tpl、surf、shift、brisk,无论哪种模式都调到了共同的方法_try_math

1if method in ["mstpl", "gmstpl"]: 2 ret = self._try_match(func, ori_image, screen, threshold=self.threshold, rgb=self.rgb, record_pos=self.record_pos, 3 resolution=self.resolution, scale_max=self.scale_max, scale_step=self.scale_step) 4else: 5 ret = self._try_match(func, image, screen, threshold=self.threshold, rgb=self.rgb)

而_try_math方法中都是调用的func的方法find_best_result()

1@staticmethod 2def _try_match(func, *args, **kwargs): 3 G.LOGGING.debug("try match with %s" % func.__name__) 4 try: 5 ret = func(*args, **kwargs).find_best_result() 6 except aircv.NoModuleError as err: 7 G.LOGGING.warning("'surf'/'sift'/'brief' is in opencv-contrib module. You can use 'tpl'/'kaze'/'brisk'/'akaze'/'orb' in CVSTRATEGY, or reinstall opencv with the contrib module.") 8 return None 9 except aircv.BaseError as err: 10 G.LOGGING.debug(repr(err)) 11 return None 12 else: 13 return ret

以TemplateMatching类的find_best_result()为例,看一下内部逻辑如何实现。

1@print_run_time 2def find_best_result(self): 3 """基于kaze进行图像识别,只筛选出最优区域.""" 4 """函数功能:找到最优结果.""" 5 # 第一步:校验图像输入 6 check_source_larger_than_search(self.im_source, self.im_search) 7 # 第二步:计算模板匹配的结果矩阵res 8 res = self._get_template_result_matrix() 9 # 第三步:依次获取匹配结果 10 min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(res) 11 h, w = self.im_search.shape[:2] 12 # 求取可信度: 13 confidence = self._get_confidence_from_matrix(max_loc, max_val, w, h) 14 # 求取识别位置: 目标中心 + 目标区域: 15 middle_point, rectangle = self._get_target_rectangle(max_loc, w, h) 16 best_match = generate_result(middle_point, rectangle, confidence) 17 LOGGING.debug("[%s] threshold=%s, result=%s" % (self.METHOD_NAME, self.threshold, best_match)) 18 return best_match if confidence >= self.threshold else Non

重点看第二步:计算模板匹配的结果矩阵res,res = self._get_template_result_matrix()

1def _get_template_result_matrix(self): 2 """求取模板匹配的结果矩阵.""" 3 # 灰度识别: cv2.matchTemplate( )只能处理灰度图片参数 4 s_gray, i_gray = img_mat_rgb_2_gray(self.im_search), img_mat_rgb_2_gray(self.im_source) 5 return cv2.matchTemplate(i_gray, s_gray, cv2.TM_CCOEFF_NORMED)

可以看到最终用的是openCV的方法,cv2.matchTemplate,那个优先匹配上就返回结果。

4 总结

使用过程中可以发现Airtest框架有两个缺点:一是对于背景透明的按钮或者控件,识别难度大;二是无法获取文本内容,但这一缺点可通过引入文字识别库解决,如:pytesseract。
对不能用UI控件定位的部件,使用图像识别定位还是非常方便的。UI自动化脚本编写过程中可以将几个框架结合使用,uiautomator定位速度较快,但对于flutter语言写的页面经常有一些部件无法定位,此时可以引入airtest框架用图片进行定位。每个框架都有优劣势,组合使用才能更好的实现目的。

作者:京东物流 范文君

来源:京东云开发者社区

点赞
收藏

评论区

加载中...

相关推荐

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

MySQL部分从库上面因为大量的临时表tmp_table造成慢查询

背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_

手机自动化测试IDE-----Airtest基本操作方法

大家好,我是IT共享者,人称皮皮。这篇我们来讲讲手机自动化测试IDEAirtest。前言前面我们讲到了Airtest的界面和安装技巧,今天我们就来说说Airtest的具体操作方法吧,让我们轻松实现软件自动化,真正解放我们的双手吧。一、基本API介绍我们在做自动化脚本的时候,每个生成的脚本文件会给我们自动生成一些代码,如图:这里所导入的模块其实就是跨平台的

手机自动化测试IDE-----Airtest基本操作方法

大家好,我是IT共享者,人称皮皮。这篇我们来讲讲手机自动化测试IDEAirtest。前言前面我们讲到了Airtest的界面和安装技巧,今天我们就来说说Airtest的具体操作方法吧,让我们轻松实现软件自动化,真正解放我们的双手吧。一、基本API介绍我们在做自动化脚本的时候,每个生成的脚本文件会给我们自动生成一些代码,如图:这里所导入的模块其实就是跨平台的

Android自动化测试,5个必备的测试框架

AppiumAppium是一个开源的移动测试工具,支持iOS和Android,它可以用来测试任何类型的移动应用(原生、网络和混合)。作为一个跨平台的工具,你可以在不同的平台上运行相同的测试。为了实现跨平台的功能,Appium使用了供应商提供的AndroidUI框架来实施测试:在iOS中使用XCTest,在Android中使用UIAutom

Katalon 学习笔记(一)

工具介绍:KatalonStudio是一个能提供一整套功能来实现Web,API和Mobile的全自动测试解决方案的自动化测试平台。KatalonStudio构建于开源Selenium和Appium框架之上,通过减少学习和集成这些框架以满足自动化测试需求所需的工作量和专业知识,使团队能够快速开始测试自动化。与其他流行工具