圖像描邊是設計軟件中常見的圖像處理功能,在 Photoshop 中,圖像應用的描邊後的效果是這樣:javascript
看起來是一個很簡單的功能,那就讓咱們來看看在 WEB 中有哪些方法可以實現描邊。java
既然 Canvas 沒有內置,那萬能的 SVG 有沒有呢?SVG 裏有許多有趣的濾鏡,其中的 feMorphology
能夠達到將某些元素進行「擴張」或者「腐蝕」的效果。咱們能夠用它實現 文字描邊。那若是將它應用在圖像上呢?git
好吧,看起來效果和咱們的需求相去甚遠,SVG 方案走不下去了。github
咱們先選擇一張簡單的矩形圖像,若是將它進行填充並複製 8 份,把這 8 張分別沿着上、下、左、右、左上、左下、右上、右下八個方向進行偏移,就能完成對矩形圖像的描邊。不過它的描邊結果不「圓潤」,若是複製更多份,好比 360 份,讓圖像往 360 個方向進行偏移不就能作出圓角了嗎?讓咱們看看結果:web
不過這個方案有着很多缺點:算法
雖然這個方案有些粗暴,可是它不涉及任何算法,更像是一個腦經急轉彎,實現成本至關低。針對性能問題,若是能夠遷移到 WebGL 上會有不小的提高(嗯?門檻好像變高了?), pixi.js 的描邊)就是這樣的實現。app
仔細想一想,描邊說到底不就是描出邊緣嗎?若是可以提取出圖像的邊緣,是否是一切問題就迎刃而解了呢?ide
咱們經過使用 Marching squares 算法 可以從圖像中提取出輪廓,獲得輪廓路徑後,以後只須要將路徑繪製出來就好了。爲了達到描邊邊緣圓潤的效果,咱們須要設置 lineJoin
爲 round
.svg
const outlineWidth = 20 const path = getPath(image) ctx.lineJoin = 'round' ctx.lineWidth = outlineWidth * 2 drawPath(ctx, path) ctx.drawImage(image)
再來看看結果,就算是大半徑的描邊也能正常輸出:函數
這個方案好像又快又好,並且也能處理描邊寬度過大的狀況。不過仍是勉強能挑出缺點:
在輪廓提取的方向上還有另外一個思路,咱們可以獲得圖像的邊緣以後,再算出整張圖像裏每一個像素點到最近的邊緣的距離。當描邊寬度等於這個距離時,咱們就填充這個像素點,這樣便實現了描邊。
Distance transform 是一種計算二值圖各像素點到邊緣距離的算法。經過一段簡單的代碼理解一下這個算法:
const getPixelByPosition = (pixels, x, y) => { alpha: 0 } const checkTransparent = pixel => pixel.alpha < 255 // 歐拉距離計算 const euclideanDistance = (x1, y1, x2, y2) => ( sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2)) ) for(let pixel of pixels) { const isTransparent = checkTransparent(pixel) let { x, y } = pixel let min = Infinity // 判斷目標是否位於圖像邊緣 for(let ox = 0; ox < width; ox++) { for(let oy = 0; oy < height; oy++) { const current = getPixelByPosition(pixels, ox, oy) if( // 當前像素透明而且目標像素不透明 isTransparent && !checkTransparent(current) || // 當前像素不透明而且目標像素透明 !isTransparent && checkTransparent(current) ) { min = Math.min(euclideanDistance(x, y, ox, oy), min) } } } }
一句話說明就是逐像素地查找距離邊緣的最短距離。不過這段代碼複雜度過高了,實際場景根本沒法用。咱們能夠選擇現成的優秀算法,不過不管如何優化,複雜度也低不了多少,通過測試,2000 * 2000px 的圖像須要 300ms。因此對於大尺寸圖像,這個方案註定快不起來。儘管如此,當只要計算出距離數據後,以後的渲染和更新都再也不是問題,咱們能夠輕鬆得作到實時更新描邊結果。
另外,這類像素操做若是不通過抗鋸齒的處理每每會產生「毛刺」,實時 CPU 鋸齒計算顯然不是一個好選擇,因而咱們就只剩 WebGL 可用了。那麼在 WebGL 中如何解決這類簡單的「毛刺」呢?在 The Book Of Shaders 中經過 smoothstep
畫出了一個更 「圓」 的圓,咱們也能夠基於此函數來解決這個「毛刺」問題。
這個方案除了初始化距離數據的時間過長之外,幾乎沒有其餘缺點,而且相比其餘方案,咱們能夠經過使用 不一樣的距離函數 來達到不一樣的描邊效果。這個方案有很多現成的應用,例如 tiny-sdf。
至此,描邊的方案分享就結束了。看似簡單的描邊,對於算法菜雞的我仍是有着不低的難度。總結一下以上三個方案,這幾個方案都各有優缺點,從性能、效果和門檻三個維度上來看排名大體是以下(針對 2000 * 2000px 的圖像而言):