Airtest图像识别测试工具原理解读&最佳实践
1 Airtest简介
Airtest是一个跨平台的、基于图像识别的UI自动化测试框架,适用于游戏和App,支持平台有Windows、Android和iOS。Airtest框架基于一种图形脚本语言Sikuli,引用该框架后,不再需要一行行的写代码,通过截取按钮或输入框的图片,用图片组成测试场景,这种方式学习成本低,简单易上手。
2 Airtest实践
APP接入流水线过程中,赛博平台只支持air脚本,因此需要对京管家APP的UI自动化脚本进行的改造。如截图可见,AirtestIDE的主界面由菜单栏、快捷工具栏和多个窗口组成,初始布局中的“设备窗口”是工具的设备连接交互区域。
air脚本生成步骤:
- 通过adb连接手机或模拟器
- 安装应用APK
- 运行应用并截图
- 模拟用户输入(点击、滑动、按键)
- 卸载应用
通过以上步骤自动生成了 .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如何通过图片获取到元素位置从而触发点击操作。
@logwrap
def touch(v, times=1, **kwargs):
"""
Perform the touch action on the device screen
:param v: target to touch, either a ``Template`` instance or absolute coordinates (x, y)
:param times: how many touches to be performed
:param kwargs: platform specific `kwargs`, please refer to corresponding docs
:return: finial position to be clicked, e.g. (100, 100)
:platforms: Android, Windows, iOS
"""
if isinstance(v, Template):
pos = loop_find(v, timeout=ST.FIND_TIMEOUT)
else:
try_log_screen()
pos = v
for _ in range(times):
G.DEVICE.touch(pos, **kwargs)
time.sleep(0.05)
delay_after_operation()
return pos
click = touch # click is alias of t
该方法通过loop_find获取坐标,然后执行点击操作 G.DEVICE.touch(pos, kwargs),接下来看loop_find如何根据模板转换为坐标。
@logwrap
def loop_find(query, timeout=ST.FIND_TIMEOUT, threshold=None, interval=0.5, intervalfunc=None):
"""
Search for image template in the screen until timeout
Args:
query: image template to be found in screenshot
timeout: time interval how long to look for the image template
threshold: default is None
interval: sleep interval before next attempt to find the image template
intervalfunc: function that is executed after unsuccessful attempt to find the image template
Raises:
TargetNotFoundError: when image template is not found in screenshot
Returns:
TargetNotFoundError if image template not found, otherwise returns the position where the image template has
been found in screenshot
"""
G.LOGGING.info("Try finding: %s", query)
start_time = time.time()
while True:
screen = G.DEVICE.snapshot(filename=None, quality=ST.SNAPSHOT_QUALITY)
if screen is None:
G.LOGGING.warning("Screen is None, may be locked")
else:
if threshold:
query.threshold = threshold
match_pos = query.match_in(screen)
if match_pos:
try_log_screen(screen)
return match_pos
if intervalfunc is not None:
intervalfunc()
# 超时则raise,未超时则进行下次循环:
if (time.time() - start_time) > timeout:
try_log_screen(screen)
raise TargetNotFoundError('Picture %s not found in screen' % query)
else:
t
首先截取手机屏幕match_pos = query.match_in(screen),然后对比传参图片与截屏来获取图片所在位置match_pos = query.match_in(screen)。接下来看match_in方法的逻辑:
def match_in(self, screen):
match_result = self._cv_match(screen)
G.LOGGING.debug("match result: %s", match_result)
if not match_result:
return None
focus_pos = TargetPos().getXY(match_result, self.target_pos)
return focus_pos
里面有个关键方法:match_result = self._cv_match(screen)
@logwrap
def _cv_match(self, screen):
# in case image file not exist in current directory:
ori_image = self._imread()
image = self._resize_image(ori_image, screen, ST.RESIZE_METHOD)
ret = None
for method in ST.CVSTRATEGY:
# get function definition and execute:
func = MATCHING_METHODS.get(method, None)
if func is None:
raise InvalidMatchingMethodError("Undefined method in CVSTRATEGY: '%s', try 'kaze'/'brisk'/'akaze'/'orb'/'surf'/'sift'/'brief' instead." % method)
else:
if method in ["mstpl", "gmstpl"]:
ret = self._try_match(func, ori_image, screen, threshold=self.threshold, rgb=self.rgb, record_pos=self.record_pos,
resolution=self.resolution, scale_max=self.scale_max, scale_step=self.scale_step)
else:
ret = self._try_match(func, image, screen, threshold=self.threshold, rgb=self.rgb)
if ret:
break
return ret
首先读取图片调整图片尺寸,从而提升匹配成功率:
image = self._resize_image(ori_image, screen, ST.RESIZE_METHOD)
接下来是循环遍历匹配方法for method in ST.CVSTRATEGY。而ST.CVSTRATEGY的枚举值:
CVSTRATEGY = ["mstpl", "tpl", "surf", "brisk"]
if LooseVersion(cv2.__version__) > LooseVersion('3.4.2'):
CVSTRATEGY = ["mstpl", "tpl", "sift", "brisk"]
func = MATCHING_METHODS.get(method, None),func可能的取值有mstpl、tpl、surf、shift、brisk,无论哪种模式都调到了共同的方法_try_math
if method in ["mstpl", "gmstpl"]:
ret = self._try_match(func, ori_image, screen, threshold=self.threshold, rgb=self.rgb, record_pos=self.record_pos,
resolution=self.resolution, scale_max=self.scale_max, scale_step=self.scale_step)
else:
ret = self._try_match(func, image, screen, threshold=self.threshold, rgb=self.rgb)
而_try_math方法中都是调用的func的方法find_best_result()
@staticmethod
def _try_match(func, *args, **kwargs):
G.LOGGING.debug("try match with %s" % func.__name__)
try:
ret = func(*args, **kwargs).find_best_result()
except aircv.NoModuleError as err:
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.")
return None
except aircv.BaseError as err:
G.LOGGING.debug(repr(err))
return None
else:
return ret
以TemplateMatching类的find_best_result()为例,看一下内部逻辑如何实现。
@print_run_time
def find_best_result(self):
"""基于kaze进行图像识别,只筛选出最优区域."""
"""函数功能:找到最优结果."""
# 第一步:校验图像输入
check_source_larger_than_search(self.im_source, self.im_search)
# 第二步:计算模板匹配的结果矩阵res
res = self._get_template_result_matrix()
# 第三步:依次获取匹配结果
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(res)
h, w = self.im_search.shape[:2]
# 求取可信度:
confidence = self._get_confidence_from_matrix(max_loc, max_val, w, h)
# 求取识别位置: 目标中心 + 目标区域:
middle_point, rectangle = self._get_target_rectangle(max_loc, w, h)
best_match = generate_result(middle_point, rectangle, confidence)
LOGGING.debug("[%s] threshold=%s, result=%s" % (self.METHOD_NAME, self.threshold, best_match))
return best_match if confidence >= self.threshold else Non
重点看第二步:计算模板匹配的结果矩阵res,res = self._get_template_result_matrix()
def _get_template_result_matrix(self):
"""求取模板匹配的结果矩阵."""
# 灰度识别: cv2.matchTemplate( )只能处理灰度图片参数
s_gray, i_gray = img_mat_rgb_2_gray(self.im_search), img_mat_rgb_2_gray(self.im_source)
return cv2.matchTemplate(i_gray, s_gray, cv2.TM_CCOEFF_NORMED)
可以看到最终用的是openCV的方法,cv2.matchTemplate,那个优先匹配上就返回结果。
4 总结
使用过程中可以发现Airtest框架有两个缺点:一是对于背景透明的按钮或者控件,识别难度大;二是无法获取文本内容,但这一缺点可通过引入文字识别库解决,如:pytesseract。
对不能用UI控件定位的部件,使用图像识别定位还是非常方便的。UI自动化脚本编写过程中可以将几个框架结合使用,uiautomator定位速度较快,但对于flutter语言写的页面经常有一些部件无法定位,此时可以引入airtest框架用图片进行定位。每个框架都有优劣势,组合使用才能更好的实现目的。
作者:京东物流 范文君
来源:京东云开发者社区
Airtest图像识别测试工具原理解读&最佳实践的更多相关文章
- Windows Azure 安全最佳实践 - 第 7 部分:提示、工具和编码最佳实践
在撰写这一系列文章的过程中,我总结出了很多最佳实践.在这篇文章中,我介绍了在保护您的WindowsAzure应用程序时需要考虑的更多事项. 下面是一些工具和编码提示与最佳实践: · 在操作系统上运行 ...
- Spark Shuffle调优原理和最佳实践
对性能消耗的原理详解 在分布式系统中,数据分布在不同的节点上,每一个节点计算一部份数据,如果不对各个节点上独立的部份进行汇聚的话,我们计算不到最终的结果.我们需要利用分布式来发挥Spark本身并行计算 ...
- paip.sqlite 管理最好的工具 SQLite Expert 最佳实践总结
paip.sqlite 管理最好的工具 SQLite Expert 最佳实践总结 一般的管理工具斗可以...就是要是sqlite没正常地关闭哈,有shm跟wal文件..例如ff的place.sqlit ...
- [AapacheBench工具]web性能压力测试工具的应用与实践
背景:网站性能压力测试是性能调优过程中必不可少的一环.服务器负载太大而影响程序效率是很常见的事情,一个网站到底能够承受多大的用户访问量经常是我们最关心的问题.因此,只有让服务器处在高压情况下才能真正体 ...
- 学习笔记TF061:分布式TensorFlow,分布式原理、最佳实践
分布式TensorFlow由高性能gRPC库底层技术支持.Martin Abadi.Ashish Agarwal.Paul Barham论文<TensorFlow:Large-Scale Mac ...
- Session 的原理及最佳实践
Http协议是基于请求和响应的一种无状态的协议,而通过session可以使得Http应用变得有状态,即可以"记住"客户端的信息.今天就来说说这个session和cookie. Se ...
- SVN分支/合并原理及最佳实践
转自:http://blog.csdn.net/e3002/article/details/21469437 使用svn几年了,一直对分支和合并敬而远之,一来是因为分支的管理不该我操心,二来即使涉及到 ...
- (转)SVN分支/合并原理及最佳实践
先说说什么是branch.按照Subversion的说法,一个branch是某个development line(通常是主线也即trunk)的一个拷贝,见下图: branch存在的意义在于,在不干扰t ...
- Docker镜像原理和最佳实践
https://yq.aliyun.com/articles/68477 https://yq.aliyun.com/articles/57126 DockerCon 2016 深度解读: Dock ...
- SparkShuffle调优原理和最佳实践
在网络层,互联网提供所有应用程序都要使用的两种类型的服务,尽管目前理解这些服务的细节并不重要,但在所有TCP/IP概述中,都不能忽略他们: 无连接分组交付服务(Connectionless Packe ...
随机推荐
- Swift之struct二进制大小分析
作者:京东零售 邓立兵 随着Swift的日渐成熟和给开发过程带来的便利性及安全性,京喜App中的原生业务模块和基础模块使用Swift开发占比逐渐增高.本次讨论的是struct对比Class的一些优劣势 ...
- 重新编译Unity Mono遇到的坑,大坑,巨坑!!!
因为项目需要,要整一个DLL热更功能,本身也想对DLL进行加密,所以需要修改Mono的代码,并重新编译.参考了网上的教程后,决定在linux下做重编的工作. 然而在实际执行的时候,一开始就碰到了巨大的 ...
- cephadm 安装部署 ceph 集群
介绍 手册: https://access.redhat.com/documentation/zh-cn/red_hat_ceph_storage/5/html/architecture_guide/ ...
- python之PySimpleGUI(三)dome
dome1第一个程序其实会了第一个程序后面基本就都通了,就这么简单,后面只需要注意一下细节就可以import PySimpleGUI as sgsg.theme('Dark Blue 3') # pl ...
- Linux搭建docker
### 1. 查看系统的内核版本 ```bash[root@wide ~]# uname -r3.10.0-1160.el7.x86_64``` ### 2. 将yum更新到最新版本 ```bash[ ...
- 【书籍阅读】【Spring实战】二 装配Bean
Spring配置的可选方案 ①自动化装配Bean:@Component ②Java代码装配Bean:@Bean ③XML装配Bean 自动化装配Bean Spring从两个角度实现对 @Compone ...
- 全网最硬核 JVM 内存解析 - 1.从 Native Memory Tracking 说起
个人创作公约:本人声明创作的所有文章皆为自己原创,如果有参考任何文章的地方,会标注出来,如果有疏漏,欢迎大家批判.如果大家发现网上有抄袭本文章的,欢迎举报,并且积极向这个 github 仓库 提交 i ...
- 笔记:C++学习之旅---初识C++
笔记:C++学习之旅---初识C++ 博主也是一个新手,学习编程才一年左右,刚大学毕业不久,以前在学校学习的语言主要是C,本人是从嵌入式学起的!我现在从事的公司主要是C++,所以我也 ...
- 工作中,Oracle常用函数
目录 1.序言 2.Oracle函数分类 3.数值型函数 3.1 求绝对值函数 3.2 求余函数 3.3 判断数值正负函数 3.4 三角函数 3.5 返回以指定数值为准整数的函数 3.6 指数.对数函 ...
- 微擎删除分类无法删除解决-select in效率低解决办法
今天朋友微擎后台微网站里的分类要删除,可是怎么删除也不能删除,同样的系统另一套却可以迅速删除. 后来查询到是查询语句的问题,朋友的平台用户量太大,数据太大,用了以下语句,造成效率太低: SELECT ...