在〈OpenCV 與 Matplotlib〉中談過,有時候會想結合 Matplotlib 的功能來顯示圖片,這邊以圖片三角分割來作為示範,網路上一些圖片三角分割器(Image triangulator),可以將圖片處理為三角形拼接,看來就很像是低面數(Low-Poly)的藝術圖像。
圖片三角分割器的原理是:
- 尋找圖像邊緣,取得邊緣的像素座標。
- 進行 Delaunay 三角分割。
- 對每個三角形取內心座標。
- 用內心座標取得圖像對應位置的顏色。
- 用取得的顏色塗滿三角形。
先來分別認識以上各個需求各自要怎麼處理。
尋找圖像邊緣,這邊的邊緣指的是圖像中像素間變化大的位置,而不是指物體整個輪廓(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()
以上範例會建立的圖案如下:
如果想要取得三角分割的各個三角形,可以透過 Triangulation
的 get_masked_triangles
,每一組三角形是以對應 xs
、ys
的索引值提供資料,因此可以用來進一步取得每一個三角形的座標,有了座標的話,就可以求三角形的內心:
# 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()
程式完成後的效果如下: