HTML5 canvas drawImage:如何应用抗锯齿


81

请看下面的例子:

http://jsfiddle.net/MLGr4/47/

var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");

img = new Image();
img.onload = function(){
    canvas.width = 400;
    canvas.height = 150;
    ctx.drawImage(img, 0, 0, img.width, img.height, 0, 0, 400, 150);
}
img.src = "http://openwalls.com/image/1734/colored_lines_on_blue_background_1920x1200.jpg";

如您所见,尽管据说drawImage自动应用了抗锯齿,但该图像并未抗锯齿。我尝试了许多不同的方法,但是似乎没有用。您能告诉我如何获得抗锯齿图像吗?谢谢。

Answers:


174

原因

当您要从大尺寸变为小尺寸时,有些图像很难进行下采样和插值,例如带有曲线的图像。

由于性能原因,浏览器似乎通常对画布元素使用双线性(2x2采样)插值,而不是双三次(4x4采样)插值。

如果步幅太大,则根本没有足够的像素可采样,结果会从中反映出来。

从信号/ DSP的角度来看,您可能会看到低通滤波器的阈值设置得太高,如果信号中有很多高频(细节),则可能导致混叠。

更新2018:

这是一个巧妙的技巧,可用于支持filter2D上下文属性的浏览器。这将对图像进行预模糊处理,本质上与重新采样相同,然后按比例缩小。这允许较大的步骤,但只需要两个步骤和两次绘制。

使用步骤数(原始大小/目标大小/ 2)作为半径进行预模糊处理(您可能需要根据浏览器和奇数/偶数步进行启发式调整,此处仅作简化显示):

const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");

if (typeof ctx.filter === "undefined") {
 alert("Sorry, the browser doesn't support Context2D filters.")
}

const img = new Image;
img.onload = function() {

  // step 1
  const oc = document.createElement('canvas');
  const octx = oc.getContext('2d');
  oc.width = this.width;
  oc.height = this.height;

  // steo 2: pre-filter image using steps as radius
  const steps = (oc.width / canvas.width)>>1;
  octx.filter = `blur(${steps}px)`;
  octx.drawImage(this, 0, 0);

  // step 3, draw scaled
  ctx.drawImage(oc, 0, 0, oc.width, oc.height, 0, 0, canvas.width, canvas.height);

}
img.src = "//i.stack.imgur.com/cYfuM.jpg";
body{ background-color: ivory; }
canvas{border:1px solid red;}
<br/><p>Original was 1600x1200, reduced to 400x300 canvas</p><br/>
<canvas id="canvas" width=400 height=250></canvas>

自2018年10月10日起支持过滤器:

2017年更新:规范中现在定义了一个新属性,用于设置重采样质量:

context.imageSmoothingQuality = "low|medium|high"

目前仅在Chrome中支持。每个级别使用的实际方法由供应商决定,但是可以合理地假设Lanczos为“高级”或质量相当的产品。这意味着可以完全跳过降压操作,或者可以使用较大的步幅进行较少的重绘,具体取决于图像大小和

支持imageSmoothingQuality

浏览器。在此之前..:
传输结束

解决方案是使用降压以获得正确的结果。降压意味着您可以逐步减小尺寸,以使有限的插值范围覆盖足够的像素以进行采样。

双线性插值(这样做的行为实际上类似于双三次)也将带来良好的结果,并且开销很小,因为每个步骤中要采样的像素更少。

理想的步骤是将每一步的分辨率提高一半,直到您设置目标尺寸为止(感谢Joe Mabel提到了这一点!)。

改良的小提琴

像原始问题一样使用直接缩放:

正常缩小的图像

使用降压,如下所示:

缩图图像

在这种情况下,您需要分三步走:

在第1步中,我们使用屏幕外画布将图像缩小为一半:

// step 1 - create off-screen canvas
var oc   = document.createElement('canvas'),
    octx = oc.getContext('2d');

oc.width  = img.width  * 0.5;
oc.height = img.height * 0.5;

octx.drawImage(img, 0, 0, oc.width, oc.height);

第2步重复使用屏幕外的画布,并再次将图像缩小一半:

// step 2
octx.drawImage(oc, 0, 0, oc.width * 0.5, oc.height * 0.5);

然后我们再次在主画布上绘制,再次缩小到一半,但最终大小变为:

// step 3
ctx.drawImage(oc, 0, 0, oc.width * 0.5, oc.height * 0.5,
                  0, 0, canvas.width,   canvas.height);

小费:

您可以使用以下公式来计算所需的步骤总数(它包括设置目标大小的最后一步):

steps = Math.ceil(Math.log(sourceWidth / targetWidth) / Math.log(2))

4
我发现使用一些非常大的初始图像(8000 x 6000及以上),基本上迭代第2步,直到达到所需大小的2倍之内,这很有用。
Joe Mabel

奇迹般有效!谢谢!
弗拉德·谢佩列夫2014年

1
我对第二步和第三步之间的区别感到困惑...有人可以解释吗?
carinlynchin

1
@Carine有点复杂,但是canvas试图尽快保存png。png文件内部支持5种不同的滤镜类型,可以通过压缩(gzip)进行改进,但是为了找到最佳的组合,必须对每行图像测试所有这些滤镜。对于大图像而言,这将很耗时,并且可能会阻塞浏览器,因此大多数浏览器仅使用过滤器0并将其推出以希望获得一定的压缩率。您可以手动执行此过程,但是显然要多做一些工作。或者通过服务API(例如tinypng.com的API)运行它。

1
@Kaiido并没有忘记它,并且“复制”非常慢。如果您需要透明性,则使用clearRect()并使用main或alt更快。以画布为目标。

12

我强烈建议将pica用于此类任务。其质量优于多次精简,并且速度相当快。这是一个演示


4
    var getBase64Image = function(img, quality) {
    var canvas = document.createElement("canvas");
    canvas.width = img.width;
    canvas.height = img.height;
    var ctx = canvas.getContext("2d");

    //----- origin draw ---
    ctx.drawImage(img, 0, 0, img.width, img.height);

    //------ reduced draw ---
    var canvas2 = document.createElement("canvas");
    canvas2.width = img.width * quality;
    canvas2.height = img.height * quality;
    var ctx2 = canvas2.getContext("2d");
    ctx2.drawImage(canvas, 0, 0, img.width * quality, img.height * quality);

    // -- back from reduced draw ---
    ctx.drawImage(canvas2, 0, 0, img.width, img.height);

    var dataURL = canvas.toDataURL("image/png");
    return dataURL;
    // return dataURL.replace(/^data:image\/(png|jpg);base64,/, "");
}

1
参数“质量”的值范围是多少?
serup

在零和一之间[0,1]
伊万·罗德里格斯(

4

除了Ken的答案外,这里还有另一种解决方案,可以将采样量减半(因此使用浏览器的算法看起来效果不错):

  function resize_image( src, dst, type, quality ) {
     var tmp = new Image(),
         canvas, context, cW, cH;

     type = type || 'image/jpeg';
     quality = quality || 0.92;

     cW = src.naturalWidth;
     cH = src.naturalHeight;

     tmp.src = src.src;
     tmp.onload = function() {

        canvas = document.createElement( 'canvas' );

        cW /= 2;
        cH /= 2;

        if ( cW < src.width ) cW = src.width;
        if ( cH < src.height ) cH = src.height;

        canvas.width = cW;
        canvas.height = cH;
        context = canvas.getContext( '2d' );
        context.drawImage( tmp, 0, 0, cW, cH );

        dst.src = canvas.toDataURL( type, quality );

        if ( cW <= src.width || cH <= src.height )
           return;

        tmp.src = dst.src;
     }

  }
  // The images sent as parameters can be in the DOM or be image objects
  resize_image( $( '#original' )[0], $( '#smaller' )[0] );

3

如果其他人仍在寻找答案,则可以使用另一种方法来使用背景图像而不是drawImage()。这样您将不会损失任何图像质量。

JS:

    var canvas=document.getElementById("canvas");
    var ctx=canvas.getContext("2d");
   var url = "http://openwalls.com/image/17342/colored_lines_on_blue_background_1920x1200.jpg";

    img=new Image();
    img.onload=function(){

        canvas.style.backgroundImage = "url(\'" + url + "\')"

    }
    img.src="http://openwalls.com/image/17342/colored_lines_on_blue_background_1920x1200.jpg";

工作演示


2

我创建了一个可重复使用的Angular服务,为有兴趣的任何人处理图像的高质量调整:https : //gist.github.com/fisch0920/37bac5e741eaec60e983

该服务包括Ken的逐步降级方法以及此处找到的lanczos卷积方法的修改版本。

我提供了这两种解决方案,因为它们都有各自的优缺点。lanczos卷积方法具有较高的质量,但代价是速度较慢,而逐步缩小的方法可产生合理的抗锯齿结果,并且速度明显更快。

用法示例:

angular.module('demo').controller('ExampleCtrl', function (imageService) {
  // EXAMPLE USAGE
  // NOTE: it's bad practice to access the DOM inside a controller, 
  // but this is just to show the example usage.

  // resize by lanczos-sinc filter
  imageService.resize($('#myimg')[0], 256, 256)
    .then(function (resizedImage) {
      // do something with resized image
    })

  // resize by stepping down image size in increments of 2x
  imageService.resizeStep($('#myimg')[0], 256, 256)
    .then(function (resizedImage) {
      // do something with resized image
    })
})
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.