原因
当您要从大尺寸变为小尺寸时,有些图像很难进行下采样和插值,例如带有曲线的图像。
由于性能原因,浏览器似乎通常对画布元素使用双线性(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() {
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月10日起支持过滤器:
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提到了这一点!)。
改良的小提琴
像原始问题一样使用直接缩放:
使用降压,如下所示:
在这种情况下,您需要分三步走:
在第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);
第2步重复使用屏幕外的画布,并再次将图像缩小一半:
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))