问题描述
我想在画布中显示缩小的图像。这样做时,飞船底部出现锯齿状边缘,似乎禁用了抗锯齿。
这是在 Firefox 中生成的图像的放大图:
图像非常清晰,但我们看到了锯齿状边缘(尤其是飞船底部、挡风玻璃、前翼)。
在 Chrome 中:
图像保持清晰(舷窗保持清晰,所有线条)并且我们没有锯齿状边缘。只是云朵有些模糊。
我尝试将属性 imageSmoothingEnabled 设置为 true,但它在 Firefox 中不起作用,我的示例:
<!DOCTYPE html>
<html>
<head>
<Meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
</head>
<body>
<!-- <canvas id="canvas1" width="1280" height="720" style="width: 640px; height: 360px;"></canvas> -->
<canvas id="canvas1" width="640" height="360" style="width: 640px; height: 360px;"></canvas>
<script>
const canvas = document.getElementById("canvas1")
const ctx = canvas.getContext("2d")
console.log("canvas size",canvas.width,canvas.height);
const img = new Image()
img.onload = () => {
const smooth = true;
ctx.mozImageSmoothingEnabled = smooth;
ctx.webkitimageSmoothingEnabled = smooth;
ctx.msImageSmoothingEnabled = smooth;
ctx.imageSmoothingEnabled = smooth;
// ctx.filter = 'blur(1px)';
ctx.drawImage(img,3840,2160,canvas.height);
}
img.src = "https://upload.wikimedia.org/wikipedia/commons/f/f8/BFR_at_stage_separation_2-2018.jpg";
</script>
</body>
</html>
如何应用抗锯齿?
编辑:在 Chrome 中查看网站时应用抗锯齿,但在 Firefox 中不应用。
编辑 2:更精确地比较图像。实际上Firefox似乎应用了一些图像增强功能,但是将imageSmoothingEnabled设置为false时并没有禁用它
编辑 3:将提到的 抗锯齿 替换为 平滑,因为似乎涉及的不仅仅是 AA。
目前的解决方法(我很想听听您的建议!):
- 用更多像素渲染画布,然后通过 CSS 缩小它 -> 手动移动质量/性能光标
- 使用离线工具调整图像大小 -> 非交互式
- 对图像应用 1px 模糊 -> 没有锯齿状边缘,但图像显然很模糊
解决方法
高质量下样。
这个答案提供了一个下采样器,它将在浏览器中产生一致的结果,并允许统一和非统一的广泛减少。
优点
它在质量方面具有显着优势,因为它可以使用 64 位浮点 JS 数字,而不是 GPU 使用的 32 位浮点数。它还减少了 sRGB,而不是 2d API 使用的低质量 RGB。
缺点
它的缺点当然是性能。这可能使其在对大图像进行下采样时变得不切实际。但是,它可以通过 Web Worker 并行运行,因此不会阻塞主 UI。
仅适用于 50% 或以下的下采样。只需几个小的 mod 即可扩展到任何大小,但该示例选择了速度而不是多功能性。
查看结果的 99% 的人的质量提升几乎不会引起注意。
区域样本
该方法对新目标像素下的源像素进行采样,根据重叠像素区域计算颜色。
下图将有助于理解它的工作原理。
- 左侧显示较小的高分辨率源像素(蓝色)与新的低分辨率目标像素(红色)重叠。
- 右边不知道源像素的哪些部分对目标像素颜色有贡献。 % 值是目标像素与每个源像素重叠的百分比。
流程概述。
首先我们创建 3 个值来将新的 R、G、B 颜色保持为零(黑色)
我们对目标像素下的每个像素执行以下操作。
- 计算目标像素和源像素之间的重叠区域。
- 将源像素与目标像素区域重叠,以获得源像素对目标像素颜色的部分贡献
- 将源像素 RGB 转换为 sRGB,归一化并乘以上一步计算的分数贡献,然后将结果与存储的 R、G、B 值相加。
当新像素下的所有像素都处理完毕后,新的颜色 R、G、B 值将转换回 RGB 并添加到图像数据中。
完成后,像素数据被添加到画布中,返回准备使用
示例
该示例将图像缩小了约 1/4
完成后,示例显示缩放后的图像和通过 2D API 缩放的图像。
您可以单击顶部图像在两种方法之间切换并比较结果。
/* Image source By SharonPapierdreams - Own work,CC BY-SA 4.0,https://commons.wikimedia.org/w/index.php?curid=97564904 */
// reduceImage(img,w,h)
// img is image to down sample. w,h is down sampled image size.
// returns down sampled image as a canvas.
function reduceImage(img,h) {
var x,y = 0,sx,sy,ssx,ssy,r,g,b,a;
const RGB2sRGB = 2.2; // this is an approximation of sRGB
const sRGB2RGB = 1 / RGB2sRGB;
const sRGBMax = 255 ** RGB2sRGB;
const srcW = img.naturalWidth;
const srcH = img.naturalHeight;
const srcCan = Object.assign(document.createElement("canvas"),{width: srcW,height: srcH});
const sCtx = srcCan.getContext("2d");
const destCan = Object.assign(document.createElement("canvas"),{width: w,height: h});
const dCtx = destCan.getContext("2d");
sCtx.drawImage(img,0);
const srcData = sCtx.getImageData(0,srcW,srcH).data;
const destData = dCtx.getImageData(0,h);
// Warning if yStep or xStep span less than 2 pixels then there may be
// banding artifacts in the image
const xStep = srcW / w,yStep = srcH / h;
if (xStep < 2 || yStep < 2) {console.warn("Downsample too low. Should be at least 50%");}
const area = xStep * yStep
const sD = srcData,dD = destData.data;
while (y < h) {
sy = y * yStep;
x = 0;
while (x < w) {
sx = x * xStep;
const ssyB = sy + yStep;
const ssxR = sx + xStep;
r = g = b = a = 0;
ssy = sy | 0;
while (ssy < ssyB) {
const yy1 = ssy + 1;
const yArea = yy1 > ssyB ? ssyB - ssy : ssy < sy ? 1 - (sy - ssy) : 1;
ssx = sx | 0;
while (ssx < ssxR) {
const xx1 = ssx + 1;
const xArea = xx1 > ssxR ? ssxR - ssx : ssx < sx ? 1 - (sx - ssx) : 1;
const srcContribution = (yArea * xArea) / area;
const idx = (ssy * srcW + ssx) * 4;
r += ((sD[idx ] ** RGB2sRGB) / sRGBMax) * srcContribution;
g += ((sD[idx+1] ** RGB2sRGB) / sRGBMax) * srcContribution;
b += ((sD[idx+2] ** RGB2sRGB) / sRGBMax) * srcContribution;
a += (sD[idx+3] / 255) * srcContribution;
ssx += 1;
}
ssy += 1;
}
const idx = (y * w + x) * 4;
dD[idx] = (r * sRGBMax) ** sRGB2RGB;
dD[idx+1] = (g * sRGBMax) ** sRGB2RGB;
dD[idx+2] = (b * sRGBMax) ** sRGB2RGB;
dD[idx+3] = a * 255;
x += 1;
}
y += 1;
}
dCtx.putImageData(destData,0);
return destCan;
}
const scaleBy = 1/3.964;
const img = new Image;
img.crossOrigin = "Anonymous";
img.src = "https://upload.wikimedia.org/wikipedia/commons/7/71/800_Houston_St_Manhattan_KS_3.jpg";
img.addEventListener("load",() => {
const downScaled = reduceImage(img,img.naturalWidth * scaleBy | 0,img.naturalHeight * scaleBy | 0);
const downScaleByAPI = Object.assign(document.createElement("canvas"),{width: downScaled.width,height: downScaled.height});
const ctx = downScaleByAPI.getContext("2d");
ctx.drawImage(img,ctx.canvas.width,ctx.canvas.height);
const downScaleByAPI_B = Object.assign(document.createElement("canvas"),height: downScaled.height});
const ctx1 = downScaleByAPI_B.getContext("2d");
ctx1.drawImage(img,ctx.canvas.height);
img1.appendChild(downScaled);
img2.appendChild(downScaleByAPI_B);
info2.textContent = "Original image " + img.naturalWidth + " by " + img.naturalHeight + "px Downsampled to " + ctx.canvas.width + " by " + ctx.canvas.height+ "px"
var a = 0;
img1.addEventListener("click",() => {
if (a) {
info.textContent = "High quality JS downsampler";
img1.removeChild(downScaleByAPI);
img1.appendChild(downScaled);
} else {
info.textContent = "Standard 2D API downsampler";
img1.removeChild(downScaled);
img1.appendChild(downScaleByAPI);
}
a = (a + 1) % 2;
})
},{once: true})
body { font-family: arial }
<br>Click first image to switch between JS rendered and 2D API rendered versions<br><br>
<span id="info2"></span><br><br>
<div id="img1"> <span id="info">High quality JS downsampler </span><br></div>
<div id="img2"> Down sampled using 2D API<br></div>
Image source <cite><a href="https://commons.wikimedia.org/w/index.php?curid=97564904">By SharonPapierdreams - Own work,</a></cite>
有关 RGB V sRGB 的更多信息
sRGB 是所有数字媒体设备用来显示内容的色彩空间。 人类看到亮度对数意味着显示设备的动态范围是 1 到 ~200,000,这需要每通道 18 位。
显示缓冲区通过将通道值存储为 sRGB 来克服这个问题。 0 - 255 范围内的亮度。当显示硬件将此值转换为光子时,它首先通过将其提高到 2.2 的幂来扩展 255 个值,以提供所需的高动态范围。
问题在于处理显示缓冲区 (2D API) 会忽略这一点,并且不会扩展 sRGB 值。它被视为 RGB,导致不正确的颜色混合。
该图像显示了 sRGB 和 RGB(2D API 使用的 RGB)渲染之间的差异。
注意中心和右侧图像上的暗像素。那就是RGB渲染的结果。左图使用sRGB渲染,不失亮度。