YouQu使用手册【元素定位】
元素定位
文章目录
- 前言
-
- 一、气泡识别
- 二、不依赖OpenCV的图像识别方案
- 三、动态图像识别
- 四、背景
- 五、sniff(嗅探器)使用
- 六、元素操作
- 七、框架封装
- 八、背景【OCR识别】
- 九、实现原理
- 十、使用说明
- 十一、RPC服务端部署
- 十二、负载均衡
- 十三、链式调用
- 十四、背景【相对坐标定位】
- 十五、实现原理
- 十六、使用方法
- 十七、现有右键菜单定位的方案及问题【去干扰识别】
- 十八、去干扰识别
前言
图像识别在 UI 自动化中是不可缺少的,市面上甚至有完全基于图像识别的自动化测试框架,比如 Airtest、Sikuli 等,在游戏等特定领域也有不错的效果,这些工具实际上也是用的 OpenCV 进行了封装,YouQu 框架基于 OpenCV 开发了自己的图像识别功能,它可以方便的用于界面元素的定位和断言;
YouQu 的图像识别功能几乎满足了你的所有要求,我们在长时间的思考和摸索中,针对常规场景及一些特殊场景探索出了一些实用且有效的方案,且听我慢慢道来。
常规识别
【背景】
常规识别很好理解,一句话讲就是,要获取到目标元素在屏幕中的位置。
【原理实现】
在测试过程中需要获取的坐标是相对于整个屏幕的坐标,我们可以截取到整个屏幕的图片(screen);
在元素识别的过程中,我们需要截取某个元素的小图进行识别,比如截取播放按钮:
那么实际上,元素定位的问题就转换为,将截图的小图(play_btn)拿到整个屏幕的大图(screen)中去做匹配,如果匹配成功,返回小图在大图中的坐标( x, y )即可。
为了方便描述,以下我将整个屏幕的截图称为:大图,某个元素图片的截图称为:小图。
基于 OpenCV 的模板匹配 cv.matchTemplate() 功能,我们实现了图像定位的功能,框架提供了一个图像识别的底层接口(一般不对上层提供调用):
def _match_image_by_opencv(
image_path: str,
rate: float = None,
multiple: bool = False,
picture_abspath: str = None,
screen_bbox: List[int] = None
):
"""
图像识别,匹配小图在屏幕中的坐标 x, y
:param image_path: 图像识别目标文件的存放路径
:param rate: 匹配度
:param multiple: 是否返回匹配到的多个目标
:param picture_abspath: 大图,默认大图是截取屏幕,否则使用传入的图片;
:param screen_bbox: 截取屏幕上指定区域图片(仅支持X11下使用);
[x, y, w, h]
x: 左上角横坐标;y: 左上角纵坐标;w: 宽度;h: 高度;根据匹配度返回坐标
"""
# 详细代码太长不贴了,感兴趣请查看源码
【参数介绍】
(1)image_path
image_path 是小图的绝对路径;
-
通常在 AT 工程里面,我们约定将用于元素定位的图片资源放到 widget/pic_res 目录下,图片的名称以实际的元素名称命名,如:play_btn.png ;
-
用于用例断言的图片资源放到 case/assert_res 目录下,图片的名称以用例的名称命名,如:music_001.png ;
这样是为了方便管理和维护。
(2)rate
图像识别的的匹配度或者说相似度,框架默认的配置为 0.9,也就是说小图在大图中存在一个相似度 90% 的图标即返回其在大图中的坐标;
如果你在用例中需要调整识别度,你可以在调用函数的时候,传入不同的识别度的值。
(3)multiple
默认情况下 multiple=False,表示只返回识别到的第一个,如果 multiple=True 返回匹配到的多个目标,因为大图中可能存在多个相同的小图,在某些场景下你可能需要全部获取到所有匹配到的坐标。
(4)picture_abspath
默认情况下 picture_abspath=None 表示大图为截取的屏幕截图,如果你不希望大图是屏幕的截图,而是你自定义传入的某个图片,你只需要将你的图片路径传递给这个参数就行,比如: picture_abspath=“~/Desktop/big.png” ;
(5)screen_bbox
大图默认情况下是截取整个屏幕,screen_bbox = [x, y, w, h] 可以指定截取屏幕中的固定区域,某些场景下,可以排除部分区域对识别结果的影响。
【隐式等待】
用例执行过程中进行图像识别时,有时候页面跳转有延时,有可能存在识别的那一刻页面也没有跳转出来,或者或者识别的那一刻;
因此我们需要一种等待机制,即在一定的时间内,如果识别不到,重复去识别:
def find_image(
cls,
*widget, rate: [float, int] = None,
multiple: bool = False,
match_number: int = None,
picture_abspath: str = None,
screen_bbox: List[int] = None
):
"""
在屏幕中区寻找小图,返回坐标,
如果找不到,根据配置重试次数,每次间隔1秒
:param widget: 模板图片路径
:param rate: 相似度
:param multiple: 是否返回匹配到的多个目标
:param match_number: 图像识别重试次数
:return: 坐标元组
"""
if rate is None:
rate = float(GlobalConfig.IMAGE_RATE)
try:
for element in widget:
for _ in range((match_number or int(GlobalConfig.IMAGE_MATCH_NUMBER)) + 1):
locate = cls._match_image_by_opencv(
element,
rate,
multiple=multiple,
picture_abspath=picture_abspath,
screen_bbox=screen_bbox
)
if not locate:
sleep(int(GlobalConfig.IMAGE_MATCH_WAIT_TIME))
else:
return locate
raise TemplateElementNotFound(*widget)
except Exception as e:
raise e
参数 match_number 用于控制重复识别的次数,默认不传参,取全局配置 setting/globalconfig.ini 里面的 IMAGE_MATCH_NUMBER 配置项的值,默认IMAGE_MATCH_NUMBER = 1,即重试 1 次;
find_image 是框架提供的常规图像识别函数接口,这个函数提供了隐式等待的功能,且包含上面介绍的 _match_image_by_opencv 函数的所有功能。
一、气泡识别
【背景】
气泡识别指的是,某些场景下要定位的元素是一些会消失的小弹窗,这类场景在用例执行过程中进行图像识别时就可能存在不稳定性,有可能图像识别的时候气泡已经消失了,也有可能气泡出现的时间太短了,不容易捕捉到,就像气泡一样,出现一下就消失,因此我们形象的称之为 “气泡识别”;
【原理实现】
为了能稳定的识别气泡类场景,我们采用的方案是:
在一段时间内(包含气泡从出现到消失),不停的截取这段时间内的大图,以此确保在截取的一堆图片中,肯定有至少一张图片能捕捉到气泡,最后再对这一堆图片逐个进行图像识别;
代码示例:
def get_during(
cls,
image_path: str,
screen_time: [float, int],
rate: float = None,
pause: [int, float] = None,
max_range: int = 10000
):
"""
在一段时间内截图多张图片进行识别,其中有一张图片识别成功即返回结果;
适用于气泡类的断言,比如气泡在1秒内消失,如果用常规的图像识别则有可能无法识别到;
:param image_path: 要识别的模板图片;
:param screen_time: 截取屏幕图片的时间,单位秒;
:param rate: 识别率;
:param pause: 截取屏幕图片的间隔时间,默认不间隔;
:param max_range: 截图的最大次数,这是一个预设值,一般情况下不涉及修改;
"""
【参数介绍】
(1)screen_time
截取屏幕图片的时间,在此时间内会不断的进行截图操作,就像录制视频一样;
(2)pause
每次截取图片的间隔时间,默认情况下是一刻不停的截图,如果你想每次截图存在一些间隔时间传入对应的时间间隔即可,单位是秒,比如:pause = 0.03,表示 30 ms,相当于帧率为 30 帧;
二、不依赖OpenCV的图像识别方案
- 自研图像识别技术
【原理】
为了实现识别图像的目的,我们可以通过将图片的每个像素的RGB值,与整个屏幕中的RGB进行对比,如果小图上的RGB值与对应大图位置的RGB都相等,则匹配成功,即可返回小图在大图中的中心坐标点。
读取小图和大图的RGB值
(1)小图的RGB值
small_data = small_pic.load()
# load()会将图片的RGB值获取到,数据格式为一个二维列表,赋值给一个变量small_data。
(2)大图的RGB值
big_data = big_pic.load()
将小图与大图的RGB值进行匹配
(1)匹配从大图的坐标(0,0)开始匹配,匹配小图里面所有的坐标点(