关于canvas:canvas绘制图像轮廓效果

37次阅读

共计 4287 个字符,预计需要花费 11 分钟才能阅读完成。

在 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

如果对可视化感兴趣, 关注公号“ITMan 彪叔”能够及时收到更多有价值的文章。也能够加微信 541002349 进行交换。

正文完
 0