canvas繪製圖像輪廓效果 繪製邊框 繪製輪廓 使用算法(marching-squares-algorithm) 總結 參考文檔

在2d圖形可視化開發中,經常要繪製對象的選中效果。 一般來說,表達對象選中可以使用邊框,輪廓或者發光的效果。 發光的效果,可以使用canvas的陰影功能,比較容易實現,此處不在贅述。

繪製邊框

繪製邊框是最容易實現的效果,比如下面的圖片


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


這個代碼也很簡單,如下所示:

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

繪製輪廓

問題是,簡單粗暴的加一個邊框,並不能滿足需求。很多時候,人們需要的是輪廓的效果,也就是圖片的有像素和無像素的邊緣處。如下圖的效果所示:


要實現上述效果,最容易想到的思路就是通過像素的計算來判斷邊緣,並對邊緣進行特定顏色的像素填充。但是像素的計算算法並不容易,簡單的算法又很難達到預期的效果,而且由於逐像素操作,效率不高。

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

  1. 先繪製三維模型自身,並在繪製的時候啓動模板測試,把三維圖像保存到模板緩衝中。
  2. 把模型適當放大,用純屬繪製模型,並在繪製的時候啓用模板測試,和之前的模板緩衝區中的像素進行比較,如果對應的座標處在之前模板緩衝區中有像素,就不繪製純色。

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

在2d canvas裏面有類似的原理可以實現輪廓效果,就是使用globalCompositeOperation了。 大體思路是這樣的:

  1. 首先繪製放大一些的圖片。
  2. 然後開啓globalCompositeOperation = 'source-in', 並用純色填充整個canvas區域,由於source-in的效果,純色會填充放大圖片有像素的區域。
  3. 使用默認的globalCompositeOperation(source-over),用原始尺寸繪製圖片。

繪製放大一些的圖片

通過drawImage的參數可以控制繪製圖片的大小,如下所示,drawImage有幾個形式:

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 代表繪製的起始位置,一般繪製的時候使用第一個方法,代表繪製的大小就是原本圖片的大小。而使用第二個方法,我們可以指定繪製的尺寸,我們可以使用第二個方法繪製放大的圖片,代碼如所示:

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

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

用純色填充放大圖片的區域

在上一步繪製的基礎上,開啓globalCompositeOperation = 'source-in', 並用純色填充整個canvas區域。 代碼如下所示:

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

最終的效果如下圖所示:


爲什麼會出現這種效果是因爲使用了globalCompositeOperation = 'source-in',具體原理可以參考本人的其他文章。

繪製原始圖片

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

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

首先恢復globalCompositeOperation爲默認值 "source-over",然後按照原本的大小繪製圖片。

經過以上步驟,最終的效果如下圖所示:


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

只顯示輪廓

如果我們只想得到圖片的輪廓,則可以在最後繪製的時候,globalCompositeOperation 設置爲“destination-out”,代碼如下:

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

效果圖如下:


輪廓粗細不一致的問題

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

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


可以發現上邊緣的輪廓寬度變成了0。

在比如下圖,


繪製後上邊緣的輪廓比其他邊緣的細。


怎麼處理這種情況呢?可以在繪製放大圖片的時候,不直接使用縮放,而是在上下左右,上左,上右,下左,下右幾個方向進行偏移繪製,多次繪製,代碼如下:

  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);
  }

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


半透明的情況

我在其他文章中說過,globalCompositeOperation爲"source-in"的時候,source圖形的透明度,會影響到目標繪製圖形的透明度。所以會導致輪廓的像素值會乘以透明度。比如,我們在繪製放大圖的時候,設置globalAlpha = 0.5進行模擬。
最後的繪製效果如下:


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

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);

而上面通過偏移的方式繪製的時候,本身都繪製了好多遍,所以不存在這個問題。如下:

  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);
  }

如下圖所示:


當然,在透明度很低的情況下,使用繪製很多遍的方式,不是很好的解決方案。

使用算法(marching-squares-algorithm)

上面的方法對於有些圖片效果就很不好,比如這張圖片:


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


但是想要的只是外部的輪廓,而不需要中空部分也繪製上輪廓效果。此時需要使用其他的算法。 直接使用marching squares algorithm 可以獲取圖片的邊緣。這一塊的算法具體實現本文不再講解,後續有機會單獨一篇文章進行講解。 此處直接使用開源的實現。比如可以使用 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);
      }

首先使用調用MarchingSquaresJS的方法獲取img圖像的輪廓點的集合,然後把所有的點連接起來。形成輪廓圖,最終效果如下:


不過可以看出,MarchingSquares 算法獲得的輪廓效果鋸齒相對較多的。有光這塊算法的優化,本文不講解。

總結

對於沒有中空效果的圖片,我們一般不採用MarchingSquares算法,而採用前面的一種方式來實現,效率高,而且效果相對更好。 而對於有中空,就會使用MarchingSquares算法,效果相對差,效率也相對低一些,實際應用中,可以通過緩存來降低性能的損耗。

本文的起源來資源一個2.5D項目,上一張項目圖吧:


參考文檔

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章