前端智能化實(shí)踐——從圖片識(shí)別UI樣式

導(dǎo)語:前端智能化,就是通過AI/CV技術(shù),使前端工具鏈具備理解能力,進(jìn)而輔助開發(fā)提升研發(fā)效率,比如實(shí)現(xiàn)基于設(shè)計(jì)稿智能布局和組件智能識(shí)別等。

本文要介紹的是前端智能化的一類實(shí)踐:通過計(jì)算機(jī)視覺和機(jī)器學(xué)習(xí)實(shí)現(xiàn)自動(dòng)提取圖片中的UI樣式的能力。

demo實(shí)現(xiàn)效果

具體效果如上圖,當(dāng)用戶框選圖片中包含組件的區(qū)域,算法能準(zhǔn)確定位組件位置,并有效識(shí)別組件的UI樣式。

樣式提取方案

對圖像的樣式檢測涉及計(jì)算機(jī)視覺領(lǐng)域知識(shí),本文基于OpenCV進(jìn)行代碼實(shí)現(xiàn),主要分為三步:

  1. 從圖片檢測并分離組件區(qū)域;
  2. 基于組件區(qū)域進(jìn)行形狀檢測;
  3. 對符合規(guī)則形狀的組件進(jìn)行樣式計(jì)算。

1. 從圖片分離組件區(qū)域

組件區(qū)域分離主要是通過圖像分割算法,識(shí)別組件區(qū)域(前景)和背景區(qū)域,本文主要從用戶框選操作上考慮,采用了可交互可迭代的Grab Cut算法。
Grab cut 算法允許用戶框選作為前景輸入,利用混合高斯模型GMM,找到前景和背景的最佳分割路徑,具體可參考文章:圖像分割——Grab Cut算法

輸入框選區(qū),GrabCut輸出組件遮罩

如上圖,通過調(diào)用OpenCV的cv2.grabCut方法時(shí),我們將組件前景框(x, y, width, height)作為方法入?yún)?,識(shí)別出的組件像素被存儲(chǔ)在mask遮罩。

代碼實(shí)現(xiàn)

def extract(img, rect):
  """輸入框選區(qū),輸出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)
  # 函數(shù)的返回值是更新的 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

通過這一步,我們從背景分離出目標(biāo)遮罩,它是包含了N個(gè)組件區(qū)域的二值圖。

2. 形狀檢測

接下來,我們需要通過形狀檢測從遮罩區(qū)篩選出多個(gè)可用樣式還原的組件,比如矩形、帶圓角矩形和圓形。
具體分為兩步:1) 提取組件外輪廓 2) 霍夫檢測識(shí)別輪廓形狀

2.1 外輪廓提取

第一步是通過前面圖割遮罩進(jìn)行外輪廓提取,排除組件內(nèi)部其它線條帶來的影響。輪廓提取主要使用Suzuki85輪廓跟蹤算法,該算法基于二值圖像拓補(bǔ),能確定連通域的包含關(guān)系。

外輪廓提取流程

這里采用的是Canny邊緣檢測來得到圖像邊緣圖,再通過Suzuki85算法cv2.findContours從圖像邊緣提取外輪廓。

代碼實(shí)現(xiàn)

def separate(img, th=5):
    """輸入組件區(qū)域遮罩,輸出多個(gè)組件外輪廓列表"""
    new_img = img.copy()
    new_img = cv2.Canny(new_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):
        """剔除噪點(diǎn)"""
            continue
        data.append((cnt, x, y, w, h))
    return data

這一步我們得到了圖像中所有組件的外輪廓以及具體的坐標(biāo)x,y和寬高w,h。

2.2 組件的形狀檢測

第二步則是對每個(gè)組件外輪廓進(jìn)行圖形類型識(shí)別,其中除了矩形、圓形是樣式可還原圖形,其它都不可還原,我們的目標(biāo)就是檢測出這兩種基本圖形。

檢測目標(biāo):圓形&矩形

這里運(yùn)用霍夫變換(Hough Transform)方法,它是一種識(shí)別幾何形狀的算法,主要采用投票機(jī)制從多個(gè)特征點(diǎn)擬合圖像中線段和曲線的參數(shù)方程。

霍夫檢測直線步驟

2.2.1 矩形檢測

檢測矩形主要分兩步:1)通過霍夫直線變換檢測外輪廓的邊;2)根據(jù)邊(線段)集合判斷是否符合矩形特征。

OpenCV提供線段檢測方法cv2.HoughLinesP,輸入外輪廓,輸出檢測到的線段,具體代碼實(shí)現(xiàn)如下:

# 檢測矩形
def detectRectangle(img, width, height):
    minLineLength = 10
    maxLineGap = 4
    # 霍夫直線變換輸出檢測到的線段數(shù)組
    lines = cv2.HoughLinesP(img, 1, np.pi/180, 100, minLineLength, maxLineGap)
    segments = lines.reshape(lines.shape[0], 4)
    # 將線段數(shù)組進(jìn)行進(jìn)一步檢測,判斷是否命中矩形規(guī)則
    return judgeRectangle(segments, width, height)

取到線段集合后,我們再判斷是否滿足矩形邊的特征:

  1. 存在兩條水平方向線段和兩條垂直方向線段
  2. 上線段到下線段距離≈組件高度,左線段到右線段距離≈組件寬度

代碼實(shí)現(xiàn)

"""判斷是否為矩形"""
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  # 如果兩線間隔非圖形高度,則不規(guī)則圖片
        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 圓形檢測

圓形檢測可使用霍夫圓環(huán)檢測法,對應(yīng)OpenCV的HoughCircles方法,輸入二值圖,如果存在圓形,則返回圓形和半徑。代碼實(shí)現(xiàn)如下:

# 檢測圓形
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)
    )

通過這一步,我們篩選出屬于矩形或圓形的組件,以及組件的寬高、圓形以及對應(yīng)的半徑,下一步,我們將針對這兩種基本圖形進(jìn)行樣式檢測。

3. 組件的樣式計(jì)算

組件樣式計(jì)算主要對邊框、圓角、背景三種常用樣式分別計(jì)算。

組件的樣式檢測

3.1 圓角計(jì)算

在樣式定義中,圓角被限制在矩形的四個(gè)頂點(diǎn)處,圓角弧度取決于它的半徑,因此圓角計(jì)算的主要目標(biāo)就是識(shí)別圓角的半徑。
根據(jù)圓角的4個(gè)方位,我們將組件區(qū)域劃分為4塊進(jìn)行逐塊分析。
一開始,我們采用直接對圓弧點(diǎn)進(jìn)行圓的曲線擬合,但由于圓角點(diǎn)的數(shù)據(jù)過于集中,擬合圓的誤差很大,如圖:

最小二乘法對集中數(shù)據(jù)點(diǎn)集擬合效果差

我們知道,圓角經(jīng)過十字對稱后能構(gòu)造出一個(gè)圓形,因此,只要我們確定了“圓角”的候選區(qū)域,構(gòu)造十字軸對稱圖,就可以根據(jù)圓形擬合準(zhǔn)確判斷是否為滿足圓角特征了。具體步驟如下:

  1. 假設(shè)存在圓角,用面積推算圓角半徑,確定“候選區(qū)域”
  2. 構(gòu)造“候選區(qū)域”水平-豎直軸對稱圖形,對圖形進(jìn)行霍夫圓環(huán)檢測,驗(yàn)證是否為圓角

3.1.1 圓角半徑推算

我們假設(shè)存在圓角,半徑為R,如下圖黃色色塊區(qū)域,是組件框與填充組件的差集。

面積區(qū)域推算圓角半徑

同時(shí),黃色塊也是以邊長R為正方形與半徑R為1/4圓的差集,即s = R2 - π × R2 × ?,于是聯(lián)立方程,可求解圓角半徑R,代碼如下:

這一步我們根據(jù)面積差集計(jì)算出半徑R,通過R,我們裁剪出“候選區(qū)域”,進(jìn)行下一步驗(yàn)證。

3.1.2 候選區(qū)域驗(yàn)證

這一步先構(gòu)造軸對稱圖像,主要是在水平和豎直方向依次做翻轉(zhuǎn)+拼接操作。

非圓角和圓角的候選區(qū)域驗(yàn)證對比

如圖,得到對稱圖形后,我們沿用上文的霍夫圓環(huán)變換來檢測是否存在圓形,如果存在,則圓角也存在,反之亦然。

代碼實(shí)現(xiàn)

# 推算可能的圓角半徑
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

# 驗(yàn)證候選區(qū)域是否為圓角,以左上圓角為例
def vertifyCorner(img, cornerRadius):
    cornerArea = img[:cornerRadius, :cornerRadius] # 裁剪出候選區(qū)域
    binary_image = np.zeros(cornerArea.shape[0:2],dtype=np.uint8) # 構(gòu)造二值圖
    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 邊框計(jì)算

對于邊框的計(jì)算,我們同樣是先確定邊框的描述特征:A. 邊框內(nèi)的顏色連續(xù)與相近;B. 外輪廓和內(nèi)輪廓是形狀相似的?;谶@個(gè)特征,我制定了以下步驟:

  1. 色塊分離:對圖像基于顏色聚類,相近色區(qū)聚類同一色塊
  2. 遍歷不同色塊,提取每個(gè)色塊內(nèi)外輪廓,并計(jì)算其相似度

3.2.1 色塊分離

邊框具有顏色相近的特征,我們通過聚類算法對目標(biāo)圖像讓顏色相近的區(qū)域歸類,這里采用k-means算法聚類,聚類特征基于圖像的HSV色彩空間。

k-means對圖片進(jìn)行色塊分離效果

代碼實(shí)現(xiàn)
"""k-means聚類"""
def image_kmeansSegement(img, k=6):
    # 將圖片從RGB空間轉(zhuǎn)為HSV
    img = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
    data = img.reshape((-1, 3))
    data = np.float32(data)

    # MAX_ITER最大迭代次數(shù),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 候選區(qū)域驗(yàn)證

這一步是遍歷k個(gè)候選色塊,對色塊分別進(jìn)行外輪廓和內(nèi)輪廓提取,再判斷色塊內(nèi)外輪廓是否形狀相似。
其中外輪廓的提取直接復(fù)用前面的cv2.findContours方法,輸入色塊,輸出外輪廓填充圖。
內(nèi)輪廓?jiǎng)t需要分兩步,首先對外輪廓填充圖與色塊填充圖進(jìn)行差運(yùn)算得到“內(nèi)域”,再對內(nèi)域進(jìn)行cv2.findContours。

pHash計(jì)算外輪廓與內(nèi)輪廓相似度

拿到內(nèi)外輪廓后,我使用感知哈希pHash + 漢明距離進(jìn)行相似度計(jì)算,它主要通過顏色低采樣將圖片統(tǒng)一縮小到32×32尺寸并輸出圖像簽名,很好地解決相似形狀中大小不一致帶來的誤差。

代碼實(shí)現(xiàn)
"""驗(yàn)證每個(gè)色塊是否存在邊框特征B"""
def borderExtract(labels, center, img_filled):
    # 遍歷k-means分離的k個(gè)色塊
    for i in range(labels.max()):
        area = np.zeros((labels.size), dtype=np.uint8)
        area[labels == i] = 255
        area = area.reshape(img_filled.shape)
        # 獲取當(dāng)前色塊外輪廓,用白色填充
        outter_filled, *_ = image_contours(area)
        # 獲取當(dāng)前色塊內(nèi)輪廓,用白色填充
        result = outter_filled - area
        result[result < 0] = 0
        inner_filled, *_ = image_contours(result)
        # 判斷外輪廓和內(nèi)輪廓是否相似
        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算法計(jì)算輪廓之間相似度"""
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

總結(jié)

本文通過OpenCV系列算法分別實(shí)現(xiàn)簡單組件區(qū)域的分離和樣式的檢測,對于組件的區(qū)域檢測,目前是通過手工框選的手段確定組件區(qū)域,如果要完全自動(dòng)化實(shí)現(xiàn)Pixels to Code,還需要借助深度卷積網(wǎng)絡(luò)進(jìn)行組件檢測與識(shí)別。

最后,本人將于9月5號參與2020騰訊live開發(fā)者大會(huì),介紹更多前端智能化內(nèi)容,歡迎有興趣童鞋前來觀摩??
騰訊Live開發(fā)者大會(huì)?:https://2020.tlc.ivweb.io/detail/?number=8

更多文章歡迎關(guān)注:https://zhuanlan.zhihu.com/webxr

相關(guān)資料

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

pHash圖像相似度比較算法匯總:https://blog.csdn.net/mago2015/article/details/81137089

機(jī)器學(xué)習(xí)算法實(shí)踐——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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容