5

前端 + AI ——从图片识别UI样式

 3 years ago
source link: https://zhuanlan.zhihu.com/p/207308196
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

前端 + AI ——从图片识别UI样式

腾讯科技有限公司 高级工程师

导语:前端智能化,就是通过AI/CV技术,使前端工具链具备理解能力,进而辅助开发提升研发效率,比如实现基于设计稿智能布局和组件智能识别等。

本文要介绍的是前端智能化的一类实践:通过计算机视觉和机器学习实现自动提取图片中的UI样式的能力。

v2-bfa768b18aabbe167909e7ae3523a2c4_b.jpg

具体效果如上图,当用户框选图片中包含组件的区域,算法能准确定位组件位置,并有效识别组件的UI样式。

样式提取方案

本文基于OpenCV-Python实现图像的样式检测,主要分为三步: 1. 从图片检测并分离组件区域; 2. 基于组件区域进行形状检测; 3. 对符合规则形状的组件进行样式计算。

1. 从图片分离组件区域

组件区域分离主要是通过图像分割算法,识别组件区域(前景)和背景区域,本文主要从用户框选操作上考虑,采用了可交互可迭代的Grab Cut算法。 Grab cut 算法允许用户框选作为前景输入,利用混合高斯模型GMM,找到前景和背景的最佳分割路径,具体可参考文章:图像分割——Grab Cut算法

如上图,通过调用OpenCV的cv2.grabCut方法时,我们将组件前景框(x, y, width, height)作为方法入参,识别出的组件像素被存储在mask遮罩。

def extract(img, rect):
  """输入框选区,输出GrabCut遮罩"""
  x, y, w, h = rect
  roi_img = img[y:y+h, x:x+h]
  mask = np.zeros(roi_img.shape[:2], np.uint8) # 初始化遮罩层
  bgdModel = np.zeros((1, 65), np.float64)
  fgdModel = np.zeros((1, 65), np.float64)
  # 函数的返回值是更新的 mask, bgdModel, fgdModel
  cv2.grabCut(img, mask, rect, bgdModel, fgdModel, 4, cv2.GC_INIT_WITH_RECT)
  mask = np.where((mask == 2) | (mask == 0), 0, 255).astype("uint8")
  return mask

通过这一步,我们从背景分离出目标遮罩,它是包含了N个组件区域的二值图。

2. 组件的形状检测

接下来,我们需要通过形状检测从遮罩区筛选出多个可用样式还原的组件,比如矩形、带圆角矩形和圆形。 具体分为两步:1) 提取组件外轮廓 2) 霍夫检测识别轮廓形状

2.1 外轮廓提取

第一步是通过前面图割遮罩进行外轮廓提取,排除组件内部其它线条带来的影响。轮廓提取主要使用Suzuki85轮廓跟踪算法,该算法基于二值图像拓补,能确定连通域的包含关系。

这里采用的是Canny边缘检测来得到图像边缘图,再通过Suzuki85算法cv2.findContours从图像边缘提取外轮廓。

def separate(img, th=5):
    """输入组件区域遮罩,输出多个组件外轮廓列表"""
    new_img = cv2.Canny(img, 50, 150)
    new_img = image_morphology(new_img)
    cnts, _ = cv2.findContours(new_img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
    data = []
    for cnt in cnts:
        x, y, w, h = cv2.boundingRect(cnt)
        if (w < th) | (h < th):
        """剔除噪点"""
            continue
        data.append((cnt, x, y, w, h))
    return data

这一步我们得到了图像中所有组件的外轮廓以及具体的坐标x,y和宽高w,h

2.2 形状检测

第二步则是对每个组件外轮廓进行图形类型识别,其中除了矩形、圆形是样式可还原图形,其它都不可还原,我们的目标就是检测出这两种基本图形。

这里运用霍夫变换(Hough Transform)方法,它是一种识别几何形状的算法,主要采用投票机制从多个特征点拟合图像中线段和曲线的参数方程。

2.2.1 矩形检测

检测矩形主要分两步:1)通过霍夫直线变换检测外轮廓的边;2)根据边(线段)集合判断是否符合矩形特征。

OpenCV提供线段检测方法cv2.HoughLinesP,输入外轮廓,输出检测到的线段,具体代码实现如下:

# 检测矩形
def detectRectangle(img, width, height):
    minLineLength = 10
    maxLineGap = 4
    # 霍夫直线变换输出检测到的线段数组
    lines = cv2.HoughLinesP(img, 1, np.pi/180, 100, minLineLength, maxLineGap)
    segments = lines.reshape(lines.shape[0], 4)
    # 将线段数组进行进一步检测,判断是否命中矩形规则
    return judgeRectangle(segments, width, height)

取到线段集合后,我们再判断是否满足矩形边的特征: 1. 存在两条水平方向线段和两条垂直方向线段 1. 上线段到下线段距离≈组件高度,左线段到右线段距离≈组件宽度

"""判断是否为矩形"""
def judgeRectangle(lines, width, height, x=0, y=0):
    th = 2
    horizontal_segments = lines[np.where(abs(lines[:, 1] - lines[:, 3]) < th)]
    vertical_segments = lines[np.where(abs(lines[:, 0] - lines[:, 2]) < th)]
    isRect = False
    h = w = None
    if horizontal_segments.size != 0:
        horizontal_centers = (
            horizontal_segments[:, 1] / 2 + horizontal_segments[:, 3] / 2
        )
        top = horizontal_centers.min()
        bottom = horizontal_centers.max()
        h = bottom - top
        if abs(h - height) > th:
            return False, None, None  # 如果两线间隔非图形高度,则不规则图片
        isRect = True
        h = int(round(h))
    if vertical_segments.size != 0:
        vertical_centers = vertical_segments[:, 0] / 2 + vertical_segments[:, 2] / 2
        left = vertical_centers.min()
        right = vertical_centers.max()
        w = right - left
        if abs(w - width) > th:
            return False, None, None
        isRect = True
        w = int(round(w))
    return isRect, w, h

2.2.2 圆形检测

圆形检测可使用霍夫圆环检测法,对应OpenCV的HoughCircles方法,输入二值图,如果存在圆形,则返回圆形和半径。代码实现如下:

# 检测圆形
def detectCircle(img, width, height):
    circles = cv2.HoughCircles(img,cv2.HOUGH_GRADIENT,1,20,param1=30,param2=15,minRadius=10,maxRadius=0)
    if circles is None: return False
    [radius, rx, ry] = circles[0]
    return judgeCircle(radius, rx, ry, width, height)

def judgeCircle(r, rx, ry, w, h, x=0, y=0, th=4):
    return (
        (abs(w - h) < th)
        & (abs(r - w / 2) < th)
        & (abs(rx - x - w / 2) < th)
        & (abs(ry - y - h / 2) < th)
    )

通过这一步,我们筛选出属于矩形或圆形的组件,以及组件的宽高、圆形以及对应的半径,下一步,我们将针对这两种基本图形进行样式检测。

3. 组件的样式计算

组件样式计算主要对边框、圆角、背景三种常用样式分别计算。

3.1 圆角计算

在样式定义中,圆角被限制在矩形的四个顶点处,圆角弧度取决于它的半径,因此圆角计算的主要目标就是识别圆角的半径。 根据圆角的4个方位,我们将组件区域划分为4块进行逐块分析。 一开始,我们采用直接对圆弧点进行圆的曲线拟合,但由于圆角点的数据过于集中,拟合圆的误差很大,如图:

我们知道,圆角经过十字对称后能构造出一个圆形,因此,只要我们确定了“圆角”的候选区域,构造十字轴对称图,就可以根据圆形拟合准确判断是否为满足圆角特征了。具体步骤如下:

  1. 假设存在圆角,用面积推算圆角半径,确定“候选区域”
  2. 构造“候选区域”水平-竖直轴对称图形,对图形进行霍夫圆环检测,验证是否为圆角

3.1.1 圆角半径推算

我们假设存在圆角,半径为R,如下图黄色色块区域,是组件框与填充组件的差集。

同时,黄色块也是以边长R为正方形与半径R为1/4圆的差集,即s = R² - π × R² × ¼,于是联立方程,可求解圆角半径R,代码如下:

这一步我们根据面积差集计算出半径R,通过R,我们裁剪出“候选区域”,进行下一步验证。

3.1.2 候选区域验证

这一步先构造轴对称图像,主要是在水平和竖直方向依次做翻转+拼接操作。

如图,得到对称图形后,我们沿用上文的霍夫圆环变换来检测是否存在圆形,如果存在,则圆角也存在,反之亦然。

# 推算可能的圆角半径
def getCornerRadius(img):
    cornerRadius = 0
    corner_mask_size = img[img[:, :, 3] != 1].size
    # 
    if corner_mask_size >= 0:
        cornerRadius = round(math.sqrt((corner_mask_size / 3) / (1 - np.pi / 4)))
    return cornerRadius

# 验证候选区域是否为圆角,以左上圆角为例
def vertifyCorner(img, cornerRadius):
    cornerArea = img[:cornerRadius, :cornerRadius] # 裁剪出候选区域
    binary_image = np.zeros(cornerArea.shape[0:2],dtype=np.uint8) # 构造二值图
    binary_image[cornerArea[:,:,3] != 0] = 255
    horizontal = cv2.flip(img, 1, dst=None) # 水平镜像
    img=cv2.hconcat([img, horizontal]) # 水平拼接
    vertical = cv2.flip(img, 0, dst=None) # 垂直镜像
    img=cv2.vconcat([img, vertical]) # 垂直拼接
    img = cv2.copyMakeBorder(img, 5, 5, 5, 5, cv2.BORDER_CONSTANT, value = [0])
    circles = cv2.HoughCircles(img, cv2.HOUGH_GRADIENT,1,20)
    if circles is None: return False
    else: return True

3.2 边框计算

对于边框的计算,我们同样是先确定边框的描述特征:A. 边框内的颜色连续与相近;B. 外轮廓和内轮廓是形状相似的。基于这个特征,我制定了以下步骤:

  1. 色块分离:对图像基于颜色聚类,相近色区聚类同一色块
  2. 内外轮廓相似度计算:遍历不同色块,提取每个色块内外轮廓,并计算其相似度

3.2.1 色块分离

边框具有颜色相近的特征,我们通过聚类算法对目标图像让颜色相近的区域归类,这里采用k-means算法聚类,聚类特征基于图像的HSV色彩空间

"""k-means聚类"""
def image_kmeansSegement(img, k=6):
    # 将图片从RGB空间转为HSV
    img = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
    data = img.reshape((-1, 3))
    data = np.float32(data)

    # MAX_ITER最大迭代次数,EPS最高精度
    criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 10, 1.0)
    num_clusters = k
    ret, label, center = cv2.kmeans(
        data, num_clusters, None, criteria, num_clusters, cv2.KMEANS_RANDOM_CENTERS
    )

    center = cv2.cvtColor(np.array([center], dtype=np.uint8), cv2.COLOR_HSV2BGR)[0]
    labels = label.flatten()
    return labels, center

3.2.2 内外轮廓相似度计算

这一步是遍历k个候选色块,对色块分别进行外轮廓和内轮廓提取,再判断色块内外轮廓是否形状相似。 其中外轮廓的提取直接复用前面的cv2.findContours方法,输入色块,输出外轮廓填充图。 内轮廓则需要分两步,首先对外轮廓填充图与色块填充图进行差运算得到“内域”,再对内域进行cv2.findContours

拿到内外轮廓后,我使用感知哈希pHash + 汉明距离进行相似度计算,它主要通过颜色低采样将图片统一缩小到32×32尺寸并输出图像签名,很好地解决相似形状中大小不一致带来的误差。

"""验证每个色块是否存在边框特征B"""
def borderExtract(labels, center, img_filled):
    # 遍历k-means分离的k个色块
    for i in range(labels.max()):
        area = np.zeros((labels.size), dtype=np.uint8)
        area[labels == i] = 255
        area = area.reshape(img_filled.shape)
        # 获取当前色块外轮廓,用白色填充
        outter_filled, *_ = image_contours(area)
        # 获取当前色块内轮廓,用白色填充
        result = outter_filled - area
        result[result < 0] = 0
        inner_filled, *_ = image_contours(result)
        # 判断外轮廓和内轮廓是否相似
        if isSimilar(outter_filled, inner_filled) & isSimilar(img_filled, filled1):
            s1 = np.where(filled1 > 0)[0].size
            s2 = np.where(filled2 > 0)[0].size
            scale = (1.0 - math.sqrt(s2 / s1)) * 0.5
            _drawBorder(filled1 - filled2, center[i])
            return scale, center[i], filled2
    return None

"""使用pHash算法计算轮廓之间相似度"""
def isSimilar(img1, img2, th=0.8):
    HASH1 = PHash.pHash(img1)
    HASH2 = PHash.pHash(img2)
    distance, score = PHash.hammingDist(HASH1, HASH2)
    print(score)
    return score > th

本文通过OpenCV系列算法分别实现简单组件区域的分离和样式的检测,对于组件的区域检测,目前是通过手工框选的手段确定组件区域,如果要完全自动化实现Pixels to Code,还需要借助深度卷积网络进行组件检测与识别。

本人将于9月5号参与腾讯live开发者大会,届时将介绍更多前端智能化实践内容,欢迎有兴趣童鞋前来观摩

腾讯Live开发者大会​2020.tlc.ivweb.io

更多文章欢迎关注

最全综述 | 图像分割算法:https://zhuanlan.zhihu.com/p/70758906

pHash图像相似度比较算法汇总:https://blog.csdn.net/mago2015/article/details/81137089

机器学习算法实践——K-Means算法与图像分割:https://blog.csdn.net/google19890102/article/details/52911835

霍夫变换:https://en.wikipedia.org/wiki/Hough_transform

Suzuki85轮廓跟踪算法:https://blog.csdn.net/yiqiudream/article/details/76864722


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK