话说从 UI 自动化技术诞生起,我们就始终绕不开 Accessibility(辅助功能)。翻阅市面上所有 Android,iOS,Mac,Win 自动化框架,只要是基于控件点击的。底层接口都是基于 Accessibility 的技术,将页面上的元素控件化。可以简单理解为给页面上的元素打了一层标签(text,id,class 等),让你可以轻松的通过 “固定标签 “(注意这个地方,后面会重点提)来找到目标元素,然后再进一步施加操作。这一类控件点击,我且称为自动化 1.0 时代,文本(固定标签)到文本(目标标签)的查找。
这一类技术虽然能够解决 90% 的自动化问题,但仍存在一些难以解决的情况。由于它还高度依赖于开发人员对于自动化或者说是对 Accessibility 控件的化的支持,如果开发人员的没有遵循良好的开发规范,不写 id,不提供唯一标识符(比如我自己在写一些简单 app 时就是这样,想着反正自己又用不到,影响界面显示就不写。)那么你的自动化脚本就需要依赖各种奇淫巧计,比如 xpath(万恶之源)!亦或者各种父级自级兄弟级等等定位方式。结果你为了调通一条自动化测试用例花费了半天时间,然后下个版本 UI 一变,之前写的那些统统作废,又要开始苦苦想用什么定位方式更加稳定。可能几个月过去,自动化率还是维持在一个较低的水平,这也是很多人觉得 UI 自动化投入产出比不高的原因之一。
并且随着跨平台技术的发展,越来越多” 没有控件 “的 app 出现了!跨平台的开发框架为了让一套代码能跑多个平台,通常都是抛弃了原生框架,直接选择使用图形渲染的方式来生成控件(移动端的 Flutter,PC 端的 Qt),而这类应用基本不会考虑 Accessibility,所以在元素查看器里所表现出来的情况就是一大块一大块,没有文本,没有 Id 的没有任何唯一标识符的空白区域,更有甚者干脆直接一整个页面都是渲染出来的,想定位都定位不到,游戏类的 App 更是"重灾区"。
这里拿微博 app 举例,如下图所示,博主的名字 以及 名字下方的 "From 6.4 万人关注了他" 这两个文本就没有体现在 UI 树中,他们直接作为一个整体被渲染,
其节点信息为(Attributes: NAF="true", index="0", text="", id="", class_name="android.view.View", package="com.sina.weibo", content_desc="", checkable="false", checked="false", clickable="true", enabled="true", focusable="true", focused="false", scrollable="false", long-clickable="false", password="false", selected="false", visible-to-user="true", bounds="[0,206][540,316]", xpath="FrameLayout[3]/TabHost[0]/FrameLayout[0]/ViewPager[0]/ListView[0]/LinearLayout[1]/View[0]")
,几乎是没有可用于定位其唯一性的标识。 右边那个"+Follow" 更是连可用于定位的控件都没有。
当然还有一类就是像 Electron 这种,直接内置个浏览器在 app 中。它比前者要好那么一丢丢,至少还能用 web 自动化的技术来驱动,但也仅限于好一点点。因为它无法直接作用于已启动的 App,必须得是基于 debug 模式开启的 Electron 应用才能开启 web 自动化(这玩意非常难开,便利性极差)。
为了解决那剩下的 10%,以及未来的 100%,且慢慢随着图像识别技术的兴起,图像识别点击开始慢慢进入大众视野。从古老的 sikuli(模板匹配),到如今风靡一时的 airtest(图像特征值匹配)。图像识别点击成为了 UI 自动化测试的第二块拼图。它很大程度上解决的一些元素控件定位难,乃至没有控件的元素的定位问题,同时也被广泛运用在一些简单的游戏 App 中,我把它为 2.0 时代。
但这就够了吗?显然是不够,纯图像识别对比,相较于直接的控件识别定位技术,仍只能算一种补充。而且由于它高度依赖图像的精确度,以及识别阈值的调试,高一点找到的太多,低一点干脆找不到。我最经常听到的一句话就是,” 他明明就在那,为什么还是点不到?“。特别时如果图像上存在多个相同的按钮,则很难精确定位到具体的那一个。
随着时代的进步,人工智能的风还是刮到了 UI 自动化测试领域。这是否意味着 UI 自动化 3.0 时代的到来呢?我个人是持保留态度的,我认为想要将 AI 模型大规模落地到实际的自动化测试应用,而不是只简单 demo,还有很长的路要走。为什么这么说?首先我们先看看基于之前的两种技术所开发的框架之所以能够流传开来都具有怎样的特性。1.免费(这里的免费泛指不需要投入高额的财物资源,包括但不限于购买额外的付费账号,独立显卡),2.开源 。免费意味着普适性,开源意味着会有各路神仙协助一起开发,使其变得更完善。缺少上面任意一点,我认为都很难做到批量的投产使用。
就目前所调研的一些市面上我们能看到的大部分号称运用 AI 实现自动化测试的框架,基本包含两个或者至少有其中之一:
其中第二项不在本篇讨论的范围内,私以为这个属于 AI 自动化测试第二阶段。在 AI 自动化测试领域,视觉 UI 分析是一个关键部分。对于许多复杂的软件界面,尤其是那些没有良好的可访问性接口(Accessibility 接口)支持的应用,通过视觉 UI 分析 (让 AI 模型成为你的眼睛) 来识别和操作界面元素是非常重要的。它能够直接从屏幕呈现的内容入手,不依赖应用本身的内部结构信息,这对于黑盒测试场景或者对第三方应用进行测试的情况极为有用。只有在这个基础上,才能更好地结合自然语言处理等技术,实现更高级的、以自然语言驱动的自动化测试。简单来说我们目标即如下图所示(image to text):
我目前调研的能够实现上述功能(image to text)的纯视觉驱动的框架,第一类,AI Agent 类,比较知名的框架:AppAgent ,这一类我不太看好,噱头大于实际运用的意义。(这里先叠个甲,纯属个人观点,无任何贬低的意思,技术术还是牛逼的)它们 80% 都是调用的 gpt4o,抑或是其他在线的 AI 视觉模型。这一类框架乍一看真的很惊艳很酷炫,但实际用起来就差强人意了。它们基本都要求你要先有个付费的 GPT4 账号,这玩意是真的贵啊。即使上国产的平替,效果好不好另说,只要涉及 API 调用就没有免费的。注册送的那点免费额度根本不够用,调用多了还会被临时禁用。你做自动化总不可能只盯着一台设备跑,都不用并发,多跑几条 case 就 “瞎了”。要知道多模态模型是非常吃 gpu 资源的,在保证速度的情况下,别人服务器基本上都是顶配的显卡资源在跑,限制滥用是必须的。而通过这种方式跑自动化,每执行一步就需要丢一张图上去问,然后还想快基本是不可能的。当然,有钱也可以多开几个账号,分开调用分开跑。总的来说不是在线模型不够强,是钱包顶不住。因此这类在此也就不展开叙述了。
第二类,基于离线模型构建的,与前者不同,这一类框架通常是用自己训练的或者市面上开源的离线模型。离线就意味着你不用担心 token 限制的问题,模型跑在自己的机器上想咋用就咋用。
关于可用于 UI 元素查找视觉类模型,我目前能找到的市面上比较有代表性的有这几种(包括但不限于)CLIP、Florence-2、R-CNN 和 YOLO
以下是 GPT 帮我总结的:
模型定位与目标:
模型 | 关键方法 | 工作流程 |
---|---|---|
CLIP | 对比学习,基于大规模图像 - 文本对训练,使图像和文本共享嵌入空间 | 通过图像和文本编码器提取特征,计算两者的相似性,用于任务如检索和分类 |
Florence-2 | 序列到序列架构,将视觉任务统一为从文本提示到文本输出的问题 | 先用预训练模型提取图像特征,再通过提示生成分类、检测、分割等输出 |
R-CNN | 候选区域生成 + CNN 特征提取 + 分类和边界框回归 | 生成候选区域(Selective Search),对每个候选区域提取特征,分类并调整边界框 |
YOLO | 单阶段目标检测,直接从输入图像预测类别和边界框 | 将图像划分为网格,每个网格直接预测目标类别和位置,基于置信度筛选最终输出 |
性能与特点对比:
模型 | 性能描述 | 优缺点 |
---|---|---|
CLIP | 强大的多模态语义理解能力 |
优势:支持零样本学习,适合图像与文本的多模态任务; 劣势:对具体视觉任务(如检测)适配性一般。 |
Florence-2 | 视觉任务的高精度与灵活性 |
优势:支持多任务、多模态;通过大规模数据预训练,表现强大; 劣势:计算资源需求高。 |
R-CNN | 检测精度高,但速度慢 |
优势:首次将深度学习引入目标检测,精度显著提高; 劣势:效率低,不能实时处理。 |
YOLO | 检测速度快,精度略逊于两阶段模型 |
优势:实时性强,适合工业应用; 劣势:对小目标和复杂场景的检测效果稍逊色。 |
对这几个模型的深入介绍,这里就不一一赘述了,更多还是作为一个引用参考,大家想了解网上的资源都挺多的。
接下来我再介绍两个我研究的过的框架,首先是第一个vision-ui。 由美团开源的一个视觉测试工具,它包含以下这些功能:
我只使用了其中 UI 目标检测(本来想试用一下 语义目标识别,但无奈其中依赖 paddle1.x 版本的库,目前 paddle1.x 版本已经无法安装了。且 vision-ui 也没有继续维护。想要用起来还需要改代码,索性算了),顺带提提一下,语义目标识别底层其实就是用了 Clip 的技术
再来看一下它主页里,对于目标检测的展示
以及这是在一个训练库素材里找到的 app 页面截图做的测试
可以看到效果还是差的比较多的,左侧列表中的很多图标都没有被抓取到。究其原因,一方面是由于页面背景色差不够大(灰色背景,白色图标)不利于目标检测,另一方面是我猜是由于训练的图库基本都是美团自己内部的图标,所以对于非美团产品的图标识别率就相对没那么高( 当然美团也提供了一个用于训练的预训练 R-CNN 模型vision-ml,可以提供自己的训练素材进行训练,只是相对比较古早了不推荐)。
除了识别率没有那么高以外,还有一个比较致命的问题就是,它只能够简单的将可能存在控件的部分给你圈出来,然后简单分为 “icon” 和 “pic”。没有对这些控件进行更进一步的分类。也就是说,如果我想通过这种方式来找到目标元素,我只能够用类似 “icon”/“pic” + 索引(页面上第 N 个 icon/pic)的方式查找。如果页面元素数量不变的情况下是还好。但如果页面元素发生变化,数量增加减少,图标顺序变化,都会导致原本的定位逻辑失效,因此这个框架还不符合我们的要求。
后来我找到另一个 叫做 OmniParser(https://github.com/microsoft/OmniParser),是一个由微软提供的多模态的视觉框架。它的功能非常强大,可以是说是 vision-ui 的 “超级升级版”。由于它有上传模型到 hugging face,所以我们可以直接用调用 API 来试用一下。 可以看到,效果非常绝。直接将页面上的所有文本和控件元素都给你圈出来,并且生成了对应的描述信息。
右上角的搜索框图标被识别为 Icon Box ID 27: a search function. 坐标信息为:'27': [0.8850123935275608, 0.01995257042549752, 0.04813181559244792, 0.025454863986453493] 。
但它与 VsionUI 一样存在一个致命问题——并不是所有图标都能够被捕捉出来。虽然比 vision-ui 获取的信息要更多,但左侧的列表的图标基本都没能被识别出来。
为了彻底搞懂为什么会出现这个识别不全问题,我深入研究了其中代码。简单来说,这个框架是由三个模块组成
而整个框架的核心方法是位于根目录下的 utils.py 中的 get_som_labeled_img()
, 这个方法导入了上述三个模块。
首先利用 OCR 技术识别提取文本区域(包含其坐标信息)
def check_ocr_box(image_path, display_img = True, output_bb_format='xywh', goal_filtering=None, easyocr_args=None, use_paddleocr=False):
use_paddleocr = True
if use_paddleocr:
if easyocr_args is None:
text_threshold = 0.5
else:
text_threshold = easyocr_args['text_threshold']
ocr_start_time = time.time()
result = paddle_ocr.ocr(image_path, cls=False)[0]
ocr_stop_time = time.time()
ocr_execution_time = ocr_stop_time - ocr_start_time
print(f"Time taken for ocr on cpu: {ocr_execution_time}")
conf = [item[1] for item in result]
coord = [item[0] for item in result if item[1][1] > text_threshold]
text = [item[1][0] for item in result if item[1][1] > text_threshold]
else: # EasyOCR
if easyocr_args is None:
easyocr_args = {}
result = reader.readtext(image_path, **easyocr_args)
# print('goal filtering pred:', result[-5:])
coord = [item[0] for item in result]
text = [item[1] for item in result]
# read the image using cv2
if display_img:
opencv_img = cv2.imread(image_path)
opencv_img = cv2.cvtColor(opencv_img, cv2.COLOR_RGB2BGR)
bb = []
for item in coord:
x, y, a, b = get_xywh(item)
# print(x, y, a, b)
bb.append((x, y, a, b))
cv2.rectangle(opencv_img, (x, y), (x+a, y+b), (0, 255, 0), 2)
# Display the image
plt.imshow(opencv_img)
else:
if output_bb_format == 'xywh':
bb = [get_xywh(item) for item in coord]
elif output_bb_format == 'xyxy':
bb = [get_xyxy(item) for item in coord]
# print('bounding box!!!', bb)
return (text, bb), goal_filtering
ocr_bbox_rslt, is_goal_filtered = check_ocr_box(image_save_path, display_img = False, output_bb_format='xyxy', goal_filtering=None, easyocr_args={'paragraph': False, 'text_threshold':0.9}, use_paddleocr=use_paddleocr)
text, ocr_bbox = ocr_bbox_rslt
ocr_bbox_elem = [{'type': 'text', 'bbox':box, 'interactivity':False, 'content':txt} for box, txt in zip(ocr_bbox, ocr_text)]
然后使用YOLO模型来提取识别所有控件包含文本控件的区域
def predict_yolo(model, image_path, box_threshold, imgsz, scale_img, iou_threshold=0.7):
""" Use huggingface model to replace the original model
"""
# model = model['model']
if scale_img:
result = model.predict(
source=image_path,
conf=box_threshold,
imgsz=imgsz,
iou=iou_threshold, # default 0.7
)
else:
result = model.predict(
source=image_path,
conf=box_threshold,
iou=iou_threshold, # default 0.7
)
boxes = result[0].boxes.xyxy#.tolist() # in pixel space
conf = result[0].boxes.conf
phrases = [str(i) for i in range(len(boxes))]
return boxes, conf, phrases
xyxy, logits, phrases = predict_yolo(model=model, image_path=img_path, box_threshold=BOX_TRESHOLD, imgsz=imgsz, scale_img=scale_img, iou_threshold=0.1)
xyxy_elem = [{'type': 'icon', 'bbox':box, 'interactivity':True, 'content':None} for box in xyxy.tolist()]
最后调用remove_overlap_new
过滤掉所有重叠的框,通过这系列操作可得到整个 UI 页面所有的控件元素框 + 所有纯文本类的信息
filtered_boxes = remove_overlap_new(boxes=xyxy_elem, iou_threshold=iou_threshold, ocr_bbox=ocr_bbox_elem)
直到这一步,我之前提到的问题就开始暴露,所以问题的核心在于YOLO模型无法将页面上的所有控件都抓取。为什么呢?前文我们提到 yolo 是一种目标检测模型,它主要侧重于快速检测出图像中目标物体的大致位置和类别,是从宏观角度去定位目标。而 UI 界面上的控件往往种类繁多且形态各异,部分控件可能尺寸很小(比如一些小型的图标按钮等),对于这些小目标,YOLO 模型可能由于其检测粒度相对较粗,难以精准地将它们识别出来,容易出现遗漏情况。
特别 UI 界面有时候布局很复杂,存在控件相互重叠、遮挡的情况,或者背景与控件颜色、纹理相近,干扰了模型的识别。例如一些界面上有半透明的提示框覆盖在部分控件上,或者背景花纹与控件图案容易混淆视觉特征,YOLO模型在设计时主要是针对通用的目标检测场景进行训练优化,面对这种复杂的 UI 界面特殊场景,其适应性就稍显不足,不能很好地分辨出所有的控件。
且YOLO 更多其实还是被用于现实生活中的物体追踪和识别,对于这类场景,大多数时候要求还是比较宽松的,比如一群人里少识别到一个人,一堆书本里少圈出一本书。这影响大吗?并不大,只要它整体被认为是书,或者是人,其实目的就达到了。但是在 UI 页面这样的场景下,少一个元素都是致命的,无法捕获到意味着测试点的遗漏或者影响自动化测试的执行。
说完这一趴,我们继续往下看。拿到所有元素框之后,它利用这些元素坐标,长宽信息,对图像进行分割。分割完成后再 for 循环调用Florence-2(默认,否则使用phi3_v)来为每一个元素生成对应的描述信息。也就达到了我们之前展示的那个效果
# get parsed icon local semantics
if use_local_semantics:
caption_model = caption_model_processor['model']
if 'phi3_v' in caption_model.config.model_type:
parsed_content_icon = get_parsed_content_icon_phi3v(filtered_boxes, ocr_bbox, image_source, caption_model_processor)
else:
parsed_content_icon = get_parsed_content_icon(filtered_boxes, starting_idx, image_source, caption_model_processor, prompt=prompt,batch_size=batch_size)
ocr_text = [f"Text Box ID {i}: {txt}" for i, txt in enumerate(ocr_text)]
icon_start = len(ocr_text)
parsed_content_icon_ls = []
# fill the filtered_boxes_elem None content with parsed_content_icon in order
for i, box in enumerate(filtered_boxes_elem):
if box['content'] is None:
box['content'] = parsed_content_icon.pop(0)
for i, txt in enumerate(parsed_content_icon):
parsed_content_icon_ls.append(f"Icon Box ID {str(i+icon_start)}: {txt}")
parsed_content_merged = ocr_text + parsed_content_icon_ls
else:
ocr_text = [f"Text Box ID {i}: {txt}" for i, txt in enumerate(ocr_text)]
parsed_content_merged = ocr_text
filtered_boxes = box_convert(boxes=filtered_boxes, in_fmt="xyxy", out_fmt="cxcywh")
这一部分运行还是很好的,但是问题就是太耗时了。可以看到 OCR 耗时 3 秒左右,YOLO耗时 1.6 秒左右,最后整体运行时间 60 秒,
意味着 Florence-2 这一部分耗时至少在 50 秒以上,此时的页面元素大概 20 个左右。如果使用 GPU 来跑,这个速度能降到 5 秒左右,差不多是十倍。但成本确实高啊。
那么有没有能够解决上述两个问题的办法呢?接下来让我们来介绍一下我的一个实现思路。
首先我们要解决YOLO模型识别丢失的问题,这个问题其实很无解。为什么这么说呢,如果继续使用模型来识别,那就意味着要重新寻找一个或者自己重新训练一个。
前面也说过YOLO模型的特性就是速度快,但精度没办法做到那么高。如果既要高效又想要准,那就需要投喂大量的标记好的 UI 数据,这需要大量的人工标记,个人是基本没法完成的。我在各大模型网站也找了遍,没有任何专门用于 UI 图标识别的模型可用。
既然如此,那就意味着我们需要换一个思路。由于之前我研究过一段时间 OpenCV,我发现 OpenCV 中有一个非常强大的功能叫做轮廓识别。它对下现实中的不规则物体的识别率比较差,同时现实生活的照片还会受到各种光照的影响,反而 UI 界面没有这种烦恼。这使得轮廓识别反而成为捕捉页面元素的一大利器,来看看实际效果如何。
左侧是初步的边缘检测结果,可以看到有一些多余的元素被抓取了。但最终我们优化后(右侧),可以看到但是整体上是没有图标被遗漏的。
执行速度也很快
然后我们参考 OmniParser 的思路再加上 OCR
解决了图标抓取不全的问题之后,重头戏来了——图标识别。前面我们只做了文本识别,图标这块还只是简单以 “icon” 命名,这不足以支撑我用于元素查找工作。总不能从 icon1~20 这样来定位吧,这样位置顺序稍微一变就直接废了。OmniParser 采取的是使用生成式推理模型,基于图像加上 promt 来生成一段描述,这样很准,但也很慢(对只有 CPU 的电脑来说)。
那么有没有一种更快的方案呢?搜寻了一圈,结论就是想要运行快:不要做图像生成文本,只做图像类型检测。于是我开始搜索有没有预训练好的用于 UI 图标检测的模型,很可惜没有!能搜到的都仅仅是将图标从 UI 界面中圈出来,没有任何做进一步分类的模型。
那么有没有开源的训练数据呢?再次搜寻了一圈,只找到了一个叫做 rico data 的
我抱着侥幸心理下载解压之后,发现根本没啥用啊,只有一堆手机截图,还是大概 10 年前的 UI 界面。。也没有做任何标注(网站正文里还说提供人工大量标注的数据,json 格式的文件是对应的 UI 树结构,里面也没有什么有用信息。)
这时候我又想到,市面上有很多专门提供 UI 图标的网站,可以通过简单的搜索来查找各类的图标,这不也是一种人工分类好的数据模型吗。于是我让 gpt 生成了一套常见的 UI 图标类型名称的列表,然后又写了一个爬虫脚本来爬取。大致效果就是不断打开这个网页输入各种图标类型
但理想很美满,现实很骨感,随便打开一个类型的文件夹(这里以 "Search" 距离)就会发现里面的图标参杂了很多不属于当前类型的图标也混入其中。而且数量也只有 1000 多个(因为越到后面的图标跟选定类型的图标差距越大)。就这 1000 个 * 数百种类型的图标拿来,如果要人工过滤出有效的,也是一项庞大的工程。同时我也考虑过调 AI 接口来做过滤分析,但还是前面提到过的问题,当调用次数达到一定量级之后就会出现各种临时禁用导致 API 无法使用,更不要提 AI 也不能做到 100% 准确判断。因此这条思路到这里也就断了。
当我近乎绝望的时候,我又找到了一个基于 CNN 的预训练库 Semantic Icon Classifier
,虽然已经已经是 5 年前的 “老古董了”。从其主页对 python 版本要求还停留在 python2.7 就可以看出来
,
但这个作者非常 Nice 的公开了他的训练集。可以看到,他非常贴心的做了数据分类,还对图像做了预处理
我尝试用这个数据进行了训练,最终训练出来的效果嘛。。图标足够标准的话,效果还行(列表从左到右按找符合度排序),单个图标计算时间 0.2 秒
1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 204ms/step
The top ten predicted classes are: ['settings', 'close', 'share', 'star', 'add', 'avatar', 'weather', 'group', 'fullscreen', 'follow']
The time taken for prediction is: 0.2670462131500244 seconds
但如果遇到一些比较抽象的或者不在训练类别列表中的图标(训练集提供了 99 种图标类别),就比较抓瞎了。
我本想就此放弃了,但转念一想,识别不准重要吗?好像又不那么重要。在做元素查找时,比起准确的类型描述,我们似乎更需要的是一种 “唯一标识符”。就像我在前言中提到的,我们只需要一个能够在不同 UI 布局来对指定控件做标记的东西即刻。如果识别准确,那很好。如果识别不准确,那也不要紧,我们大可把其当作一个了类似 automation 的东西。
如下图所示,虽然开关按钮识别不准,他们被识别成了” check", 但不同位置的开关还是拥有相同的”唯一标识符“(”_index“字段是我额外加上的)。并且,当我再次进入这个界面,只要 UI 图标不变,这个唯一标识符“也不会发生变化,至此我们想要的效果也基本都能够实现了。
from cathin.Android.android_driver import AndroidDriver
cat = AndroidDriver("988e5041555a4a434c")
# cat(text_contains="Smart stay").right().click()
print(cat(text_contains="Smart stay").right().description)
当然 Florence-2 我也没有浪费,我将其写进了description
这个属性中,可以在一些场景里帮忙做更精准的判断
运行结果如下。
虽然目前的研究取得了一定进展,但肉眼可见的,图标分类的准确度还有待提高,希望未来能在开源模型社区看到这类专向模型的身影,或者如果我能找到更优秀的训练数据。。我一定及时更新。
最后放上 github 项目地址:https://github.com/letmeNo1/Cathin ,还希望各位能够捧个场,送上一个免费的 star 以支持我后续开发工作!