圖片三角分割


在〈OpenCV 與 Matplotlib〉中談過,有時候會想結合 Matplotlib 的功能來顯示圖片,這邊以圖片三角分割來作為示範,網路上一些圖片三角分割器(Image triangulator),可以將圖片處理為三角形拼接,看來就很像是低面數(Low-Poly)的藝術圖像。

圖片三角分割器的原理是:

  1. 尋找圖像邊緣,取得邊緣的像素座標。
  2. 進行 Delaunay 三角分割
  3. 對每個三角形取內心座標。
  4. 用內心座標取得圖像對應位置的顏色。
  5. 用取得的顏色塗滿三角形。

先來分別認識以上各個需求各自要怎麼處理。

尋找圖像邊緣,這邊的邊緣指的是圖像中像素間變化大的位置,而不是指物體整個輪廓(contour),若透過 OpenCV,比較簡單的方式是透過 cv2.Canny 函式,它的基本原理是,將圖片轉為灰階、透過模糊處理(將圖片平滑化)去除雜訊,然後計算像素間灰階度的變化。

cv2.Canny 第一個參數接受要處理的圖像,第二個參數指定像素變化門檻上限,超過這個門檻的被視為邊緣,不過單只有這個門檻,邊緣看來會斷斷續續,第三個參數指定像素變化門檻下限,變化在上下門檻間的像素,會被視為邊緣,最後得到的圖片資料會是黑白圖片。例如:

import cv2

img = cv2.imread('caterpillar.jpg')
canny = cv2.Canny(img, 50, 150)

cv2.imshow('canny', canny)
cv2.waitKey(0)
cv2.destroyAllWindows()

門檻上限與下限要如何調整,就看你對處理後的結果是否能夠接受,就上面的範例來說,會產生以下的結果:

圖片三角分割

接下來是三角分割,在〈Matplotlib 三角曲面〉談過,plot_trisurf 會自動以 x 與 y 進行 Delaunay 三角分割,其底層會使用到 matplotlib.tri.Triangulation,你可以自行建立該實例,透過 triplot 來繪製三角分割:

import numpy as np
import matplotlib.tri as mtri
import matplotlib.pyplot as plt

n = 20

points = np.random.rand(n, 2)
xs = points[:,0]
ys = points[:,1]

tri = mtri.Triangulation(xs, ys)
plt.triplot(tri, marker = 'o')

plt.show()

以上範例會建立的圖案如下:

圖片三角分割

如果想要取得三角分割的各個三角形,可以透過 Triangulationget_masked_triangles,每一組三角形是以對應 xsys 的索引值提供資料,因此可以用來進一步取得每一個三角形的座標,有了座標的話,就可以求三角形的內心:

# tri 包含三角形的三個頂點座標 
def incenter(tri):
    pa = np.array(tri[0])
    pb = np.array(tri[1])
    pc = np.array(tri[2])
    abc = [
       np.linalg.norm(pb - pc),
       np.linalg.norm(pc - pa),
       np.linalg.norm(pa - pb)
    ]
    s = np.sum(abc)
    return (np.dot([pa[0], pb[0], pc[0]], abc) / s, np.dot([pa[1], pb[1], pc[1]], abc) / s) 

要用取得的顏色塗滿三角形,可以使用〈Matplotlib 多邊形繪製〉中談到的 PolyCollection,指定 facecolor 就可以了。

接下來將是將以上試著結合起來,細節部份就直接看以下程式碼的註解:

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.tri as mtri
from matplotlib.collections import PolyCollection
import cv2

def incenter(tri):
    pa = np.array(tri[0])
    pb = np.array(tri[1])
    pc = np.array(tri[2])
    abc = [
       np.linalg.norm(pb - pc),
       np.linalg.norm(pc - pa),
       np.linalg.norm(pa - pb)
    ]
    s = np.sum(abc)
    return (np.dot([pa[0], pb[0], pc[0]], abc) / s, np.dot([pa[1], pb[1], pc[1]], abc) / s) 

# 讀取後 BGR 轉 RGB
# 因為 Matplotlib 與圖片座標 Y 軸相反,因此用 np.flip 翻轉圖片
img = np.flip(cv2.imread('caterpillar.jpg')[:,:,::-1], axis = 0)

# 邊緣偵測
canny = cv2.Canny(img, 30, 150)

# 取得 255 的座標點
ys, xs = np.where(canny == 255)

# 減少取樣點,這樣三角形才不會太多個
# 才會有低面數的感覺
ys = ys[::4]
xs = xs[::4]

# 三角分割取三角形索引清單
tri_indices = mtri.Triangulation(xs, ys).get_masked_triangles()

# 取得三角形座標清單
tris = np.dstack([
    xs[tri_indices], 
    ys[tri_indices]
])

ax = plt.gca()
# 逐一繪製三角形
# 這一定得逐一取三角形的,就直接 for 迴圈吧!
for tri in tris:
    # 取內心座標
    cx, cy = incenter(tri) 
    ax.add_collection(
        PolyCollection(
            [tri], 
            # 取顏色
            facecolor = img[round(cy), round(cx)] / 255
        )
    )

plt.autoscale() 
plt.show()  

程式完成後的效果如下:

圖片三角分割