如何使用深度学习识别 UI 界面组件?
导读:智能生成代码平台 imgcook 以 Sketch、PSD、静态图片等形式的视觉稿作为输入,可以一键生成可维护的前端代码,但从设计稿中获取的都是 div、img、span 等元件,而前端大多是组件化开发,我们希望能从设计稿直接生成组件化的代码,这就需要能够将设计稿中的组件化元素,例如 Searchbar、Button、Tab 等识别出来。识别网页中的 UI 元素在人工智能领域是一个典型的的目标检测问题,我们可以尝试使用深度学习目标检测手段来自动化解决。
本文介绍了使用机器学习的方式来识别 UI 界面元素的完整流程,包括:现状问题分析、算法选型、样本准备、模型训练、模型评估、模型服务开发与部署、模型应用等。
应用背景
imgcook 以 Sketch、PSD、静态图片等形式的视觉稿作为输入,通过智能化技术一键生成可维护的前端代码,Sketch/Photoshop 设计稿的代码生成需要安装插件,在设计稿中通过 imgcook 插件导出视觉稿的 JSON 描述信息(D2C Schema)粘贴到 imgcook 可视化编辑器,在编辑器中可以进行视图编辑、逻辑编辑等来改变 JSON 描述信息。
我们可以选择 DSL 规范来生成对应的代码。例如生成 React 规范的代码,需要实现从 JSON 树转换成 React 代码 (自定义 DSL)。
如下图,左侧为 Sketch 中的视觉稿, 右侧为使用 React 开发规范生成的按钮部分的代码。
从 Sketch 视觉稿「导出数据」生成「React 开发规范」的代码,图为按钮部分代码片段。
生成的代码都是由 div、img、span 这些标签组成,但实际应用开发有这样的问题:
web页面开发为提升可复用性,页面组件化,例如:Searchbar、Button、Tab、Switch、Stepper 一些原生组件不需要生成代码,例如状态栏 Statusbar、Navbar、Keyboard
// Antd Mobile React 规范
import { Button } from "antd-mobile";
<div style={styles.ft}>
<Button style={styles.col1}>进店抢红包</Button>
<Button style={styles.col2}>加购物车</Button>
</div>
"smart": {
"layerProtocol": {
"component": {
"type": "Button"
}
}
}
#component:组件名?属性=值#
#component:Button?id=btn#
找到组件信息:类别、位置、尺寸等信息。 找到组件中的属性, 例如 button 中的文字为“提交”
Use R-CNN to detect UI elements in a webpage? Is machine learning suitable for detecting screen elements? Any thoughts on a good way to detect UI elements in a webpage? How can I detect elements of GUI using opencv? How to recognize UI elements in image?
期望通过识别 UI 界面元素来做 Web 页面自动化测试的应用场景。 期望通过识别 UI 界面元素来自动生成代码。
先从设计稿中提取或使用 CV 技术提取 UI 界面元信息,例如边界框(位置、尺寸)。
2、分类 Classification
再使用大型软件仓库挖掘、自动动态分析得到 UI 界面中出现的组件,并用此数据作为 CNN 技术的数据集学习将提取出的元素分类为特定类型,例如 Radio、Progress Bar、Button 等。
3、组装 Assembly
UI组件类别,197 个文本按钮概念和 99 个图标类。
机器学习
计算机视觉(Computer Vision,CV) 用于车牌识别和面部识别等的应用。 信息检索 用于诸如搜索引擎的应用 - 包括文本搜索和图像搜索。 市场营销 针对自动电子邮件营销和目标群体识别等的应用。 医疗诊断 诸如癌症识别和异常检测等的应用。 自然语言处理(Natural Language Processing, NLP) 如情绪分析和照片标记等的应用。
基于 Haar 功能的 Viola–Jones 目标检测框架 尺度不变特征变换(SIFT) 定向梯度直方图(HOG)特征
深度学习目标检测方法
✎ One-stage
SSD(Single Shot MultiBox Detector)系列 YOLO (You Only Look Once)系列(YOLOv1、YOLOv2、YOLOv3) RetinaNet
✎ Two-stage
R-CNN,Fast R-CNN,Faster R-CNN
✎ 其他(RefineDet)
✎ 传统方法 VS 深度学习
✎ One-stage VS Two-stage
✎ 算法优缺点
机器学习框架
Facebook AI 研究院于 2019 年 10 月 10 日开源的 Detectron2 目标检测框架。我们做 UI 界面组件识别也是用的 Detectron2, 后面会有使用示例代码。tron、maskrcn
Pipcook Github | Docs pipcook - 让前端拥抱智能化的一站式算法框架 怎样基于 tfjs-node 构建一个高阶前端机器学习框架
阿里系应用的 UI 界面图片。目前移动端 UI 界面 25647 张图片,人工标注了 10 个分类共计 49120 个组件。 代码自动生成的图片。支持 10 个分类的样本生成,生成图片时自动标注。
人工标注时需要根据明确定义的特征来标注组件 自动生成时需要根据明确定义的特征来编写样式代码。
✎ 下载 Sketch 文件
/**
* 【用途】下载 Sketch 文件
* 【命令】ts-node 1-download-sketch.ts
*/
✎ 使用 sketchtool 批量导出为图片
# 【用途】使用 sketchtool 导出 Sketch 中的 Artboards 保存为 1x 的 png 图片
# 【命令】sh 2-export-image-from-sketch.sh $inputDir $outputDir
for file in $1"/*"
do
sketchtool export artboards $file --output=$2 --formats='png' --scales=1.0
done
✎ 按尺寸分类过滤
# 【用途】将图片按尺寸分类剔除
# 【命令】python3 3-classify-by-size.py $inputDir $outputDir
# 删除尺寸不规范的图片,width_list 中是数量大于 100 的尺寸
if width not in width_list:
print('move {}'.format(img_name))
move_file(img_dir, other_img_dir, img_name)
# 删除 高度小于 30 的图片
elif height < 30:
print('move {}'.format(img_name))
move_file(img_dir, other_img_dir, img_name)
# 按尺寸归档
else:
width_dir = os.path.join(img_dir, str(width))
if not os.path.exists(width_dir):
print('mkdir:{}'.format(width))
os.mkdir(width_dir)
print('move {}'.format(img_name))
move_file(img_dir, width_dir, img_name)
✎ 图片去重
✎ 半自动标注
✎ 人工标注
// 下载 labelImg
git clone https://github.com/tzutalin/labelImg.git
// 进入 labelImg
cd labelImg-master
// 之后按照 github 中的提示安装环境
// 执行一下命令就会打开可视化界面
python3 labelImg.py
w 新建立一个矩形框
d 下个图片
a 上个图片
del/fn + del 删除选中的矩形框,我的电脑需要 fn + del
Ctrl/Command++ 放大
Ctrl/Command-- 缩小
↑→↓← 移动矩形框
const pptr = require('puppeteer')
// 存放 COCO 格式的样本数据
const mdObj = {};
const browser = await pptr.launch();
const page = await browser.newPage();
await page.goto(`http://127.0.0.1:3333/#/generator/${Date.now()}`)
await page.evaluate(() => {
const container: HTMLElement | null = document.querySelector('.container');
const elements = document.querySelectorAll('.element');
const msg: any = {bbox: []};
// 获取页面中所有带 .element 选择器的元素
elements.forEach((element) => {
const classList = Array.from(element.classList).join(',')
if (classList.match('element-')) {
// 获取类别
const type = classList.split('element-')[1].split(',')[0];
// 计算边界框并保存至 msg
pushBbox(element, type);
}
});
});
// 保存 COCO 格式样本数据
logToFile(mdObj);
// 保存 UI截图
await page.screenshot({path: 'xxx.png'});
// 关闭浏览器
await browser.close();
Detectron 2
from detectron2.data import MetadataCatalog
from detectron2.evaluation import PascalVOCDetectionEvaluator
from detectron2.engine import DefaultTrainer,hooks
from detectron2.config import get_cfg
cfg = get_cfg()
cfg.merge_from_file("./lib/detectron2/configs/COCO-Detection/faster_rcnn_R_50_C4_3x.yaml")
cfg.DATASETS.TRAIN = ("train_dataset",)
cfg.DATASETS.TEST = ('val_dataset',) # no metrics implemented for this dataset
cfg.DATALOADER.NUM_WORKERS = 4 # 多开几个worker 同时给GPU喂数据防止GPU闲置
cfg.MODEL.WEIGHTS = "detectron2://ImageNetPretrained/MSRA/R-50.pkl" # initialize from model zoo
cfg.SOLVER.IMS_PER_BATCH = 4
cfg.SOLVER.BASE_LR = 0.000025
cfg.SOLVER.NUM_GPUS = 2
cfg.SOLVER.MAX_ITER = 100000 # 300 iterations seems good enough, but you can certainly train longer
cfg.MODEL.ROI_HEADS.BATCH_SIZE_PER_IMAGE = 128 # faster, and good enough for this toy dataset
cfg.MODEL.ROI_HEADS.NUM_CLASSES = 29 # only has one class (ballon)
# 训练集
register_coco_instances("train_dataset", {}, "data/train.json", "data/img")
# 测试集
register_coco_instances("val_dataset", {}, "data/val.json", "data/img")
import os
os.makedirs(cfg.OUTPUT_DIR, exist_ok=True)
class Trainer(DefaultTrainer):
@classmethod
def build_evaluator(cls, cfg, dataset_name, output_folder=None):
### 按需求重写
@classmethod
def test_with_TTA(cls, cfg, model):
### 按需求重写
trainer = Trainer(cfg)
trainer.resume_or_load(resume=True)
trainer.train()
const {DataCollect, DataAccess, ModelLoad, ModelTrain, ModelEvaluate, PipcookRunner} = require('@pipcook/pipcook-core');
const imageCocoDataCollect = require('@pipcook/pipcook-plugins-image-coco-data-collect').default;
const imageDetectronAccess = require('@pipcook/pipcook-plugins-detection-detectron-data-access').default;
const detectronModelLoad = require('@pipcook/pipcook-plugins-detection-detectron-model-load').default;
const detectronModelTrain = require('@pipcook/pipcook-plugins-detection-detectron-model-train').default;
const detectronModelEvaluate = require('@pipcook/pipcook-plugins-detection-detectron-model-evaluate').default;
async function startPipeline() {
// collect detection data
const dataCollect = DataCollect(imageCocoDataCollect, {
url: 'http://ai-sample.oss-cn-hangzhou.aliyuncs.com/image_classification/datasets/autoLayoutGroupRecognition.zip',
testSplit: 0.1,
annotationFileName: 'annotation.json'
});
const dataAccess = DataAccess(imageDetectronAccess);
const modelLoad = ModelLoad(detectronModelLoad, {
device: 'cpu'
});
const modelTrain = ModelTrain(detectronModelTrain);
const modelEvaluate = ModelEvaluate(detectronModelEvaluate);
const runner = new PipcookRunner( {
predictServer: true
});
runner.run([dataCollect, dataAccess, modelLoad, modelTrain, modelEvaluate])
}
startPipeline();
评估指标
召回率可以理解成查全率,比如实际有 60 个是 Button, 成功预测了 40 个, 召回率是 40 / 60。
Average Precision (AP) @[ IoU=0.50:0.95 | area= all | maxDets=100 ] = 0.772
Average Precision (AP) @[ IoU=0.50 | area= all | maxDets=100 ] = 0.951
Average Precision (AP) @[ IoU=0.75 | area= all | maxDets=100 ] = 0.915
from pycocotools.cocoeval import COCOeval
from pycocotools.coco import COCO
annType = 'bbox'
# 测试集 ground truth
gt_path = '/Users/chang/coco-test-sample/data.json'
# 测试集 预测结果
dt_path = '/Users/chang/coco-test-sample/predict.json'
gt = COCO(gt_path)
gt.loadCats(gt.getCatIds())
dt = COCO(dt_path)
imgIds=sorted(gt.getImgIds())
cocoEval = COCOeval(gt,dt,annType)
for cat in gt.loadCats(gt.getCatIds()):
cocoEval.params.imgIds = imgIds
cocoEval.params.catIds = [cat['id']]
print '------------------------------ ' cat['name'] ' ---------------------------------'
cocoEval.evaluate()
cocoEval.accumulate()
cocoEval.summarize()
------------------------------ searchbar ---------------------------------
Running per image evaluation...
Evaluate annotation type *bbox*
DONE (t=2.60s).
Accumulating evaluation results...
DONE (t=0.89s).
Average Precision (AP) @[ IoU=0.50:0.95 | area= all | maxDets=100 ] = 0.772
Average Precision (AP) @[ IoU=0.50 | area= all | maxDets=100 ] = 0.951
Average Precision (AP) @[ IoU=0.75 | area= all | maxDets=100 ] = 0.915
Average Precision (AP) @[ IoU=0.50:0.95 | area= small | maxDets=100 ] = -1.000
Average Precision (AP) @[ IoU=0.50:0.95 | area=medium | maxDets=100 ] = 0.795
Average Precision (AP) @[ IoU=0.50:0.95 | area= large | maxDets=100 ] = 0.756
Average Recall (AR) @[ IoU=0.50:0.95 | area= all | maxDets= 1 ] = 0.816
Average Recall (AR) @[ IoU=0.50:0.95 | area= all | maxDets= 10 ] = 0.830
Average Recall (AR) @[ IoU=0.50:0.95 | area= all | maxDets=100 ] = 0.830
Average Recall (AR) @[ IoU=0.50:0.95 | area= small | maxDets=100 ] = -1.000
Average Recall (AR) @[ IoU=0.50:0.95 | area=medium | maxDets=100 ] = 0.838
Average Recall (AR) @[ IoU=0.50:0.95 | area= large | maxDets=100 ] = 0.823
from detectron2.config import get_cfg
from detectron2.engine.defaults import DefaultPredictor
with open('label.json') as f:
mp = json.load(f)
cfg = get_cfg()
cfg.merge_from_file("./config/faster_rcnn_R_50_C4_3x.yaml")
cfg.MODEL.WEIGHTS = "./output/model_final.pth" # initialize from model zoo
cfg.MODEL.ROI_HEADS.NUM_CLASSES = len(mp) # only has one class (ballon)
cfg.MODEL.DEVICE='cpu'
model = DefaultPredictor(cfg)
def predict(image):
im2 = cv2.imread(image)
out = model(x)
data = {'status': 200}
data['content'] = trans_data(out)
# EAS python sdk
import spark
num_io_threads = 8
endpoint = '' # '127.0.0.1:8080'
spark.default_properties().put('rpc.keepalive', '60000')
context = spark.Context(num_io_threads)
queued = context.queued_service(endpoint)
while True:
receive_data = srv.read()
try:
msg = json.loads(receive_data.decode())
ret = predict(msg["image"])
srv.write(json.dumps(ret).encode())
except Exception as e:
srv.error(500, str(e))
const detectUrl = 'http://example.com/api/predict/detect';
const res = await request(detectUrl, {
method: 'post',
dataType: 'json',
timeout: 1000 * 10,
content: JSON.stringify({
image: image,
}),
});
const json = res.content;
未来展望
简历投递至📮:suchuan.cyf@alibaba-inc.com或加微信:onlychang92, 备注:淘系技术公众号-招聘欢迎加入我们的社区群,钉钉群号:32918052
✿ 拓展阅读