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

请看下面的例子:

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 会自动应用抗锯齿,但图像并未进行抗锯齿处理.我尝试了许多不同的方法,但似乎不起作用.你能告诉我如何获得抗锯齿图像吗?谢谢.

As you see, the image is not anti-aliased although it is said that drawImage applies anti aliasing automatically. I tried many different ways but it doesn't seem to work. Could you please tell me how I can get anti-aliased image? Thanks.

推荐答案

原因

当您想从大尺寸变为小尺寸时,有些图像很难下采样和插值,例如这张带有曲线的图像.

Cause

Some images are just very hard to down-sample and interpolate such as this one with curves when you want to go from a large size to a small.

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

Browsers appear to typically use bi-linear (2x2 sampling) interpolation with the canvas element rather than bi-cubic (4x4 sampling) for (likely) performance reasons.

如果步长太大,则根本没有足够的像素进行采样,这会反映在结果中.

If the step is too huge then there are simply not enough pixels to sample from which is reflected in the result.

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

From a signal/DSP perspective you could see this as a low-pass filter's threshold value set too high, which may result in aliasing if there are many high frequencies (details) in the signal.

2018 年更新:

这是一个巧妙的技巧,可用于支持 2D 上下文中的 filter 属性的浏览器.这预先模糊了本质上与重新采样相同的图像,然后按比例缩小.这允许较大的步骤,但只需要两个步骤和两次绘制.

Here's a neat trick you can use for browsers which supports the filter property on the 2D context. This pre-blurs the image which is in essence the same as a resampling, then scales down. This allows for large steps but only needs two steps and two draws.

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

Pre-blur using number of steps (original size / destination size / 2) as radius (you may need to adjust this heuristically based on browser and odd/even steps - here only shown simplified):

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>

支持过滤器为 ogf Oct/2018:

Support for filter as ogf Oct/2018:

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 年更新:现在定义了一个新属性设置重采样质量的规范:

Update 2017: There is now a new property defined in the specs for setting resampling quality:

context.imageSmoothingQuality = "low|medium|high"

目前仅在 Chrome 中支持.每个级别使用的实际方法由供应商决定,但假设 Lanczos 为高"或质量相当的东西是合理的.这意味着可以完全跳过降步,或者可以使用更大的步长而减少重绘,具体取决于图像大小和

It's currently only supported in Chrome. The actual methods used per level is left to the vendor to decide, but it's reasonable to assume Lanczos for "high" or something equivalent in quality. This means step-down may be skipped altogether, or larger steps can be used with fewer redraws, depending on the image size and

支持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

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

browser. Until then..:
End of transmission

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

The solution is to use step-down to get a proper result. Step-down means you reduce the size in steps to allow the limited interpolation range to cover enough pixels for sampling.

这也可以通过双线性插值获得良好的结果(这样做时它实际上的行为很像双三次)并且开销最小,因为在每个步骤中要采样的像素更少.

This will allow good results also with bi-linear interpolation (it actually behaves much like bi-cubic when doing this) and the overhead is minimal as there are less pixels to sample in each step.

理想的步骤是在每个步骤中将分辨率减半,直到您设置目标尺寸(感谢 Joe Mabel 提到这一点!).

The ideal step is to go to half the resolution in each step until you would set the target size (thanks to Joe Mabel for mentioning this!).

改良小提琴

在原始问题中使用直接缩放:

如下图使用降压:

在这种情况下,您需要分 3 步下台:

In this case you will need to step down in 3 steps:

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

In step 1 we reduce the image to half by using an off-screen canvas:

// 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 reuses the off-screen canvas and draws the image reduced to half again:

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

我们再次在主画布上绘制,再次将缩小到一半,但最终尺寸:

And we draw once more to main canvas, again reduced to half but to the final size:

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

提示:

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

Tip:

You can calculate total number of steps needed, using this formula (it includes the final step to set target size):

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

相关文章