原因
当您想从大尺寸变为小尺寸时,有些图像很难下采样和插值,例如带有曲线的图像。
出于(可能的)性能原因,浏览器似乎通常对画布元素使用双线性(2x2 采样)插值而不是双三次(4x4 采样)。
如果步长太大,则根本没有足够的像素来采样,这会反映在结果中。
从信号/DSP 的角度来看,您可以将此视为低通滤波器的阈值设置得太高,如果信号中有许多高频(细节),可能会导致混叠。
解决方案
2018 年更新:
这是一个可以用于支持filter
2D 上下文属性的浏览器的巧妙技巧。这预先模糊了本质上与重新采样相同的图像,然后按比例缩小。这允许大的步骤,但只需要两个步骤和两次绘制。
使用步数(原始大小/目标大小/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>
支持过滤器为 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 年更新:现在在规范中定义了一个用于设置重采样质量的新属性:
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 提到这一点!)。
修改过的小提琴
在原始问题中使用直接缩放:
使用降压如下图:
在这种情况下,您需要分 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);
第 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))