Html5画布drawImage:如何应用抗锯齿

87
请查看以下示例: 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会自动应用反锯齿技术,但是这张图片并没有反锯齿。我试过很多不同的方法,但好像都没有用。请问您能告诉我如何获得反锯齿图片吗? 谢谢。

7个回答

191

原因

有些图片在从大尺寸缩小到小尺寸时,例如曲线图像,很难进行下采样和插值。

浏览器通常使用二次线性(2x2采样)插值而不是双三次(4x4采样)插值来处理canvas元素,这很可能是出于性能方面的考虑。

如果步长太大,则没有足够的像素可以采样,这会反映在结果中。

从信号/数字信号处理的角度来看,您可以将其视为低通滤波器的阈值设置过高,如果信号中有许多高频率(细节),则可能会导致混叠。

解决方案

更新2018:

对于支持2D上下文中filter属性的浏览器,可以使用以下巧妙的技巧。这会预先模糊图像,本质上相当于重新采样,然后进行缩小。这允许大步长但只需要两个步骤和两个绘制。

使用步数(原始大小/目标大小/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;

  // step 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月份起,支持按ogf过滤。

CanvasRenderingContext2D.filter                                                   
api.CanvasRenderingContext2D.filter                                               
On Standard Track, Experimental                                                   
https://developer.mozilla.org/docs/Web/API/CanvasRenderingContext2D/filter        
                                                                                  
DESKTOP >        |Chrome    |Edge      |Firefox   |IE        |Opera     |Safari   
:----------------|:--------:|:--------:|:--------:|:--------:|:--------:|:--------
filter !         |    52    |    ?     |    49    |    -     |    -     |    -    
                                                                                  
MOBILE >         |Chrome/A  |Edge/mob  |Firefox/A |Opera/A   |Safari/iOS|Webview/A
:----------------|:--------:|:--------:|:--------:|:--------:|:--------:|:--------
filter !         |    52    |    ?     |    49    |    -     |    -     |    52   
                                                                                  
! = Experimental                                                                  
                                                                                  
Data from MDN - "npm i -g mdncomp" (c) epistemex

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

context.imageSmoothingQuality = "low|medium|high"

目前只有Chrome支持该功能。每个级别使用的实际方法由供应商决定,但可以合理地假设对于“高”级别,采用Lanczos或类似质量的算法。这意味着根据图像大小和需要,可以跳过缩小步骤,或者使用更大的步长进行较少的重绘。

imageSmoothingQuality的支持:

CanvasRenderingContext2D.imageSmoothingQuality
api.CanvasRenderingContext2D.imageSmoothingQuality
On Standard Track, Experimental
https://developer.mozilla.org/docs/Web/API/CanvasRenderingContext2D/imageSmoothingQuality

DESKTOP >              |Chrome    |Edge      |Firefox   |IE        |Opera     |Safari
:----------------------|:--------:|:--------:|:--------:|:--------:|:--------:|:--------:
imageSmoothingQuality !|    54    |    ?     |    -     |    ?     |    41    |    Y

MOBILE >               |Chrome/A  |Edge/mob  |Firefox/A |Opera/A   |Safari/iOS|Webview/A
:----------------------|:--------:|:--------:|:--------:|:--------:|:--------:|:--------:
imageSmoothingQuality !|    54    |    ?     |    -     |    41    |    Y     |    54

! = Experimental

Data from MDN - "npm i -g mdncomp" (c) epistemex

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

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

这将允许使用双线性插值获得良好的结果(实际上当这样做时,它的行为非常类似于双三次插值),而开销很小,因为每个步骤中需要采样的像素更少。

理想的步长是每次将分辨率减半,直到设置目标大小为止(感谢Joe Mabel提出这一点!)。

修改后的演示

如原始问题中所示,使用直接缩放:

NORMAL DOWN-SCALED IMAGE

如下所示,使用逐步缩小:

DOWN-STEPPED IMAGE

在这种情况下,您需要逐步缩小3步:

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

第二步重复使用屏幕外画布并将图像缩小一半:
// 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甚至更大),我发现逐步迭代第二步骤对于达到预期大小的两倍以内非常有帮助。 - Joe Mabel
有没有一种快速调整图片大小的方法,已知宽度和高度相同,例如500x500,但我想将其调整为300x300?这个过程是否比矩形图像简单? - Core_dumped
1
我对第二步和第三步之间的区别感到困惑...有人能解释一下吗? - carinlynchin
1
@Carine 这有点复杂,但是画布尝试尽可能快地保存png文件。Png文件在内部支持5种不同的过滤器类型,这些过滤器可以改善压缩(gzip),但是为了找到最佳组合,所有这些过滤器都必须逐行测试图像。对于大型图像来说,这将是耗时的,并且可能会阻止浏览器,因此大多数浏览器只使用filter 0,并希望获得一些压缩。您可以手动执行此过程,但显然需要更多的工作量。或者通过tinypng.com等服务API运行它。 - user1693593
1
@Kaiido 这并没有被遗忘,“copy”非常慢。如果你需要透明度,使用clearRect()并将主画布或备选画布作为目标会更快。 - user1693593
显示剩余11条评论

13

我强烈推荐使用pica来处理此类任务。它的质量比多次缩小更优,同时速度也很快。 这里有一个演示


5
作为对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] );

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
参数“quality”的值范围是什么? - serup
在零和一之间[0,1] - Iván Rodríguez

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

working demo


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

1

适配高分辨率显示屏
您可能会发现在高分辨率显示屏上,画布上的物体看起来模糊。虽然有很多解决方案,但一个简单的第一步是同时使用画布的属性、样式和上下文的缩放功能来缩放画布大小。

// Get the DPR and size of the canvas
const dpr = window.devicePixelRatio;
const rect = canvas.getBoundingClientRect();

// Set the "actual" size of the canvas
canvas.width = rect.width * dpr;
canvas.height = rect.height * dpr;

// Scale the context to ensure correct drawing operations
ctx.scale(dpr, dpr);

// Set the "drawn" size of the canvas
canvas.style.width = `${rect.width}px`;
canvas.style.height = `${rect.height}px`;

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接