canvas繪制圖像輪廓效果

在2d圖形可視化開(kāi)發(fā)中,經(jīng)常要繪制對(duì)象的選中效果。 一般來(lái)說(shuō),表達(dá)對(duì)象選中可以使用邊框,輪廓或者發(fā)光的效果。 發(fā)光的效果,可以使用canvas的陰影功能,比較容易實(shí)現(xiàn),此處不在贅述。

繪制邊框

繪制邊框是最容易實(shí)現(xiàn)的效果,比如下面的圖片


圖片

要繪制邊框,只需要使用strokeRect的方式即可。效果如下圖所示:


邊框

這個(gè)代碼也很簡(jiǎn)單,如下所示:

     ctx1.strokeStyle = "red";
     ctx1.lineWidth = 2;
     ctx1.drawImage(img, 1, 1,img.width ,img.height)
     ctx1.strokeRect(1,1,img.width,img.height);

繪制輪廓

問(wèn)題是,簡(jiǎn)單粗暴的加一個(gè)邊框,并不能滿(mǎn)足需求。很多時(shí)候,人們需要的是輪廓的效果,也就是圖片的有像素和無(wú)像素的邊緣處。如下圖的效果所示:


輪廓

要實(shí)現(xiàn)上述效果,最容易想到的思路就是通過(guò)像素的計(jì)算來(lái)判斷邊緣,并對(duì)邊緣進(jìn)行特定顏色的像素填充。但是像素的計(jì)算算法并不容易,簡(jiǎn)單的算法又很難達(dá)到預(yù)期的效果,而且由于逐像素操作,效率不高。

考慮到在三維webgl中,計(jì)算輪廓的算法思路是這樣的:

  1. 先繪制三維模型自身,并在繪制的時(shí)候啟動(dòng)模板測(cè)試,把三維圖像保存到模板緩沖中。
  2. 把模型適當(dāng)放大,用純屬繪制模型,并在繪制的時(shí)候啟用模板測(cè)試,和之前的模板緩沖區(qū)中的像素進(jìn)行比較,如果對(duì)應(yīng)的坐標(biāo)處在之前模板緩沖區(qū)中有像素,就不繪制純色。

依據(jù)上述的原理,就可以繪制處三維對(duì)象的輪廓了。下面是一個(gè)示例效果,(參考https://stemkoski.github.io/Three.js/Outline.html

image.png

在2d canvas里面有類(lèi)似的原理可以實(shí)現(xiàn)輪廓效果,就是使用globalCompositeOperation了。 大體思路是這樣的:

  1. 首先繪制放大一些的圖片。
  2. 然后開(kāi)啟globalCompositeOperation = 'source-in', 并用純色填充整個(gè)canvas區(qū)域,由于source-in的效果,純色會(huì)填充放大圖片有像素的區(qū)域。
  3. 使用默認(rèn)的globalCompositeOperation(source-over),用原始尺寸繪制圖片。

繪制放大一些的圖片

通過(guò)drawImage的參數(shù)可以控制繪制圖片的大小,如下所示,drawImage有幾個(gè)形式:

1  void ctx.drawImage(image, dx, dy);
2  void ctx.drawImage(image, dx, dy, dWidth, dHeight);
3  void ctx.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);

其中dx,dy 代表繪制的起始位置,一般繪制的時(shí)候使用第一個(gè)方法,代表繪制的大小就是原本圖片的大小。而使用第二個(gè)方法,我們可以指定繪制的尺寸,我們可以使用第二個(gè)方法繪制放大的圖片,代碼如所示:

ctx.drawImage(img, p - s, p  - s, w + 2 * s, h+ 2 * s);

其中p代表圖片本身的繪制位置,s代表向左,向上的偏移量,同時(shí)圖片的寬和高都增加 2 * s

用純色填充放大圖片的區(qū)域

在上一步繪制的基礎(chǔ)上,開(kāi)啟globalCompositeOperation = 'source-in', 并用純色填充整個(gè)canvas區(qū)域。 代碼如下所示:

 // fill with color
        ctx.globalCompositeOperation = "source-in";
        ctx.fillStyle = "#FF0000";
        ctx.fillRect(0, 0, cw, ch);

最終的效果如下圖所示:


填充色

為什么會(huì)出現(xiàn)這種效果是因?yàn)槭褂昧薵lobalCompositeOperation = 'source-in',具體原理可以參考本人的其他文章。

繪制原始圖片

最后一步就是繪制原始圖片,代碼如下所示:

  ctx.globalCompositeOperation = "source-over";
  ctx.drawImage(img, p, p, w, h);

首先恢復(fù)globalCompositeOperation為默認(rèn)值 "source-over",然后按照原本的大小繪制圖片。

經(jīng)過(guò)以上步驟,最終的效果如下圖所示:


輪廓

可以看出最終獲得了我們要的效果。

只顯示輪廓

如果我們只想得到圖片的輪廓,則可以在最后繪制的時(shí)候,globalCompositeOperation 設(shè)置為“destination-out”,代碼如下:

        ctx.globalCompositeOperation = "destination-out";
        ctx.drawImage(img, p, p, w, h);

效果圖如下:


image.png

輪廓粗細(xì)不一致的問(wèn)題

上面的算法實(shí)現(xiàn),是在圖片的有像素值區(qū)域中心和圖片本身的幾何中心基本一直,如果圖片的有像素值的中心和圖片本身的幾何中心相差比較大,則會(huì)出現(xiàn)輪廓粗細(xì)不一致的情況,比如下面這張圖:

image.png

上半部分是透明的,下半部分是非透明的,像素的中心在3/4出,而幾何中心在1/2處。使用上面的算法,該圖片的輪廓如下:


image.png

可以發(fā)現(xiàn)上邊緣的輪廓寬度變成了0。

在比如下圖,


image.png

繪制后上邊緣的輪廓比其他邊緣的細(xì)。


image.png

怎么處理這種情況呢?可以在繪制放大圖片的時(shí)候,不直接使用縮放,而是在上下左右,上左,上右,下左,下右?guī)讉€(gè)方向進(jìn)行偏移繪制,多次繪制,代碼如下:

  var dArr = [-1, -1, 0, -1, 1, -1, -1, 0, 1, 0, -1, 1, 0, 1, 1, 1], // offset array
 // draw images at offsets from the array scaled by s
 for (var i = 0; i < dArr.length; i += 2) {
     ctx.drawImage(img, p + dArr[i] * s, p + dArr[i + 1] * s, w, h);
  }

再看上面圖片的輪廓效果,如下所示:


新的輪廓效果

半透明的情況

我在其他文章中說(shuō)過(guò),globalCompositeOperation為"source-in"的時(shí)候,source圖形的透明度,會(huì)影響到目標(biāo)繪制圖形的透明度。所以會(huì)導(dǎo)致輪廓的像素值會(huì)乘以透明度。比如,我們?cè)诶L制放大圖的時(shí)候,設(shè)置globalAlpha = 0.5進(jìn)行模擬。
最后的繪制效果如下:


image.png

可以看到輪廓的顏色變淺了,解決辦法就是多繪制幾次放大圖。比如:

ctx.globalAlpha = 0.5;
ctx.drawImage(img, p - s, p  - s, w + 2 * s, h+ 2 * s);
ctx.drawImage(img, p - s, p  - s, w + 2 * s, h+ 2 * s);

而上面通過(guò)偏移的方式繪制的時(shí)候,本身都繪制了好多遍,所以不存在這個(gè)問(wèn)題。如下:

  ctx.globalAlpha = 0.5;
  for (var i = 0; i < dArr.length; i += 2) {
     ctx.drawImage(img, p + dArr[i] * s, p + dArr[i + 1] * s, w, h);
  }

如下圖所示:


image.png

當(dāng)然,在透明度很低的情況下,使用繪制很多遍的方式,不是很好的解決方案。

使用算法(marching-squares-algorithm)

上面的方法對(duì)于有些圖片效果就很不好,比如這張圖片:


image.png

由于其有很多中空的效果,所以其最終效果如下圖所示:


image.png

但是想要的只是外部的輪廓,而不需要中空部分也繪制上輪廓效果。此時(shí)需要使用其他的算法。 直接使用marching squares algorithm 可以獲取圖片的邊緣。這一塊的算法具體實(shí)現(xiàn)本文不再講解,后續(xù)有機(jī)會(huì)單獨(dú)一篇文章進(jìn)行講解。 此處直接使用開(kāi)源的實(shí)現(xiàn)。比如可以使用 https://github.com/sakri/MarchingSquaresJS,代碼如下:

 function drawOuttline2(){
        var canvas = document.createElement('canvas');
        var ctx = canvas.getContext('2d');
        var w = img.width;
        var h = img.height;
        canvas.width = w;
        canvas.height = h;
        ctx.drawImage(img, 0, 0, w, h);
        var pathPoints = MarchingSquares.getBlobOutlinePoints(canvas);
        var points = [];
       
        for(var i = 0;i < pathPoints.length;i += 2){
          points.push({
            x:pathPoints[i],
            y:pathPoints[i + 1],
          })
        }


        // ctx.clearRect(0, 0, w, h);
        ctx.beginPath();
        ctx.lineWidth = 2;
        ctx.strokeStyle = '#00CCFF';
        ctx.moveTo(points[0].x, points[0].y);
        for (var i = 1; i < points.length; i += 1) {
          var point = points[i];
          ctx.lineTo(point.x,point.y);
        }
        ctx.closePath();
        ctx.stroke();
        
        ctx1.drawImage(canvas,0,0);
      }

首先使用調(diào)用MarchingSquaresJS的方法獲取img圖像的輪廓點(diǎn)的集合,然后把所有的點(diǎn)連接起來(lái)。形成輪廓圖,最終效果如下:


鐵塔輪廓

不過(guò)可以看出,MarchingSquares 算法獲得的輪廓效果鋸齒相對(duì)較多的。有光這塊算法的優(yōu)化,本文不講解。

總結(jié)

對(duì)于沒(méi)有中空效果的圖片,我們一般不采用MarchingSquares算法,而采用前面的一種方式來(lái)實(shí)現(xiàn),效率高,而且效果相對(duì)更好。 而對(duì)于有中空,就會(huì)使用MarchingSquares算法,效果相對(duì)差,效率也相對(duì)低一些,實(shí)際應(yīng)用中,可以通過(guò)緩存來(lái)降低性能的損耗。

本文的起源來(lái)資源一個(gè)2.5D項(xiàng)目,上一張項(xiàng)目圖吧:


image.png

參考文檔

https://www.emanueleferonato.com/2013/03/01/using-marching-squares-algorithm-to-trace-the-contour-of-an-image/
https://github.com/sakri/MarchingSquaresJS
https://github.com/OSUblake/msqr
http://users.polytech.unice.fr/~lingrand/MarchingCubes/algo.html#squar

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容