導(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樣式的能力。

具體效果如上圖,當(dāng)用戶框選圖片中包含組件的區(qū)域,算法能準(zhǔn)確定位組件位置,并有效識(shí)別組件的UI樣式。
樣式提取方案
對圖像的樣式檢測涉及計(jì)算機(jī)視覺領(lǐng)域知識(shí),本文基于OpenCV進(jìn)行代碼實(shí)現(xiàn),主要分為三步:
- 從圖片檢測并分離組件區(qū)域;
- 基于組件區(qū)域進(jìn)行形狀檢測;
- 對符合規(guī)則形狀的組件進(jìn)行樣式計(jì)算。
1. 從圖片分離組件區(qū)域
組件區(qū)域分離主要是通過圖像分割算法,識(shí)別組件區(qū)域(前景)和背景區(qū)域,本文主要從用戶框選操作上考慮,采用了可交互可迭代的Grab Cut算法。
Grab cut 算法允許用戶框選作為前景輸入,利用混合高斯模型GMM,找到前景和背景的最佳分割路徑,具體可參考文章:圖像分割——Grab Cut算法

如上圖,通過調(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)就是檢測出這兩種基本圖形。

這里運(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)
取到線段集合后,我們再判斷是否滿足矩形邊的特征:
- 存在兩條水平方向線段和兩條垂直方向線段
- 上線段到下線段距離≈組件高度,左線段到右線段距離≈組件寬度
代碼實(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ù)過于集中,擬合圓的誤差很大,如圖:

我們知道,圓角經(jīng)過十字對稱后能構(gòu)造出一個(gè)圓形,因此,只要我們確定了“圓角”的候選區(qū)域,構(gòu)造十字軸對稱圖,就可以根據(jù)圓形擬合準(zhǔn)確判斷是否為滿足圓角特征了。具體步驟如下:
- 假設(shè)存在圓角,用面積推算圓角半徑,確定“候選區(qū)域”
- 構(gòu)造“候選區(qū)域”水平-豎直軸對稱圖形,對圖形進(jìn)行霍夫圓環(huán)檢測,驗(yàn)證是否為圓角
3.1.1 圓角半徑推算
我們假設(shè)存在圓角,半徑為R,如下圖黃色色塊區(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)+拼接操作。

如圖,得到對稱圖形后,我們沿用上文的霍夫圓環(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è)特征,我制定了以下步驟:
- 色塊分離:對圖像基于顏色聚類,相近色區(qū)聚類同一色塊
- 遍歷不同色塊,提取每個(gè)色塊內(nèi)外輪廓,并計(jì)算其相似度
3.2.1 色塊分離
邊框具有顏色相近的特征,我們通過聚類算法對目標(biāo)圖像讓顏色相近的區(qū)域歸類,這里采用k-means算法聚類,聚類特征基于圖像的HSV色彩空間。

代碼實(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。

拿到內(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