共计 5899 个字符,预计需要花费 15 分钟才能阅读完成。
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 框架用图片进行定位。每个框架都有优劣势,组合应用能力更好的实现目标。
作者:京东物流 范文君
起源:京东云开发者社区