原因
有些图片在从大尺寸缩小到小尺寸时,例如曲线图像,很难进行下采样和插值。
浏览器通常使用二次线性(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() {
const oc = document.createElement('canvas');
const octx = oc.getContext('2d');
oc.width = this.width;
oc.height = this.height;
const steps = (oc.width / canvas.width)>>1;
octx.filter = `blur(${steps}px)`;
octx.drawImage(this, 0, 0);
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:
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:
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提出这一点!)。
修改后的演示
如原始问题中所示,使用直接缩放:
如下所示,使用逐步缩小:
在这种情况下,您需要逐步缩小3步:
第1步,我们使用离屏画布将图像减半:
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);
第二步重复使用屏幕外画布并将图像缩小一半:
octx.drawImage(oc, 0, 0, oc.width * 0.5, oc.height * 0.5);
我们再次绘制到主画布上,尺寸再次缩小,但这已经是最终大小:
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))