用 Canvas 實作 low-poly art 心得

文章待補,圖片。

前言

說到 low poly,最讓我驚豔的是 species in pieces 這個網站,他使用 low-poly 的方式畫出 30 種現在地球上瀕臨絕種的動物,是用 CSS 的 clip-path 做出來的,為了兼容瀏覽器,甚至還有做 fallback 的圖片,非常的用心。

之前有在知乎上有看見 low poly 的實作方式,但看了一下原始碼,回答是用 THREE.js 做的。一來我對 three 的掌握程度還太低,再來是 2D 的圖形用 WebGL 加速實在有點奢侈啊。這邊記錄一下思路跟實現過程。

其實要做到 low poly 的效果並不難,最直覺的方式就是在畫面上取點,然後把點連接成三角形。再把三角形著色就完成了。不過隨機取點的方式會造成圖片很大的失真。

有以下四大步驟:

  • 轉為灰階
  • 使用 sobel 做邊緣偵測
  • 選擇 vertices
  • 使用 Delaunay 三角化
  • 幫三角形上色

圖片轉灰階

有了 canvas 之後,我們可以使用 getImageData 這個 API 對圖片中的任何 pixel 做操作,這給 canvas 更大的彈性跟靈活度,代表圖片的操作有更多的可能性了。

而灰階的實現並不難,將圖片中的 r g b 相加取平均即可。示意的程式碼大概像這樣:

1
2
3
4
5
6
7
var resultArr = [];
for (var row = 0; row < height; y++) {
var i = row * width * 4 + 4;
for(var col = 1; col < width; col++, i+=4) {
}
}

其實在實際應用中,要取得邊緣的方式並不是那麼簡單,有可能會影響的因素有:

  • 陰影
  • 聚焦模糊

Sobel 取邊緣

通常檢測邊界的算法原理在於,將此像素的資料與其他像素比較,如果比較起來發現差距很大(顏色),那麼即可判斷這一點是邊緣。取邊界的算法有很多種,一般最暴力也最直接的方法就是 Sobel。

這邊就不解釋 Sobel 的原理了,維基百科上面寫得蠻清楚的。
$$Gx = \begin{vmatrix} \mathbf{-1} & \mathbf{0} & \mathbf{1} \\ \mathbf{-2} & \mathbf{0} & \mathbf{2} \\ \mathbf{-1} & \mathbf{0} & \mathbf{1} \end{vmatrix} Gy = \begin{vmatrix} \mathbf{-1} & \mathbf{-2} & \mathbf{-1} \\ \mathbf{0} & \mathbf{0} & \mathbf{0} \\ \mathbf{1} & \mathbf{2} & \mathbf{1} \end{vmatrix}$$
將矩陣做乘法之後就可以得到梯度大小了。

取完邊界之後,重頭戲就是如何將它三角化了!

取點

如果是隨機取點,很快就會發現一些盲點,邊緣常常沒有辦法很完整的保存下來,因為取點的方式隨機,三角形就會破壞邊緣的形狀,所以這邊的思路是三角形盡量不要出現在邊緣上,而是用邊緣當做三角形的點,這樣才能將邊緣比較完整的保存下來。

但取邊緣的點也不能全部都取,不然這張圖就根本沒有任何藝術效果了,像這個樣子。

醜不拉機,而且裡頭還有很多過小的三角形造成瑣碎的效果,這顯然不是我們想的樣子。那麼,如果都用邊緣的點當作組成三角形的點呢?

我們來看看效果,因為都是用邊緣取點,很容易造成三角形過於尖銳的效果,為了避免這樣的情形發生,我們必須有限制地取邊緣的點。所以,除了邊緣上的點會被選中之外,我們也加入一些邊緣外的點,避免銳角三角形的情況發生。

此外,為了更有效的避免銳角三角形發生,我們可以使用 Delaunay 三角化的算法更有效率的取點。這樣子的效果就更完美了,但顯然我的實作上還有一些小問題:

  • 在一些細節當中,如何保存比較完整的圖片訊息
  • 失真太大,有些圖片被三角化之後會完全走樣

結論

關於圖學領域真的是一門很深奧的學問啊…,不但要有一定的數學基礎與背景,還需要跟 openCV 或是 webGL、canvas 打交道,而最重要的是創意。

光是圖形的轉換跟變化,就足以寫一本厚厚的教科書了,更何況是用程式語言實作。不過第一次接觸這個領域,挺好玩的。

這個演算法最重要的部分在於選取點的方式,如果更有技巧地取點,那麼三角化後的圖形失真會越少。許多論文跟網路上的實現都比我胡亂湊出來的算法屌多了。

分享到