【问题标题】:How to improve smoothing in CanvasRenderingContext2D in Firefox?如何提高 Firefox 中 CanvasRenderingContext2D 的平滑度?
【发布时间】:2020-12-22 10:19:48
【问题描述】:

我想在画布上显示一个按比例缩小的图像。这样做时,飞船底部会出现锯齿状边缘,似乎是禁用了抗锯齿。

这是在 Firefox 中生成的图像的放大图:

图像非常清晰,但我们看到锯齿状边缘(尤其是宇宙飞船的底部、挡风玻璃、鼻翼)。

在 Chrome 中:

图像保持清晰(舷窗保持清晰,所有线条)并且我们没有锯齿状边缘。只有云有点模糊。

在禁用平滑的 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, 0, 0, 3840, 2160, 0, 0, canvas.width, 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:将 antialising 的提及替换为 smoothing,因为似乎涉及的不仅仅是 AA。

到目前为止的解决方法(我很想听听您的建议!):

  • 用更多像素渲染画布,然后通过 CSS 缩小它 -> 手动移动质量/性能光标
  • 使用离线工具调整图像大小 -> 非交互式
  • 对图像应用 1px 模糊 -> 不再有锯齿状边缘,但显然是模糊图像

使用模糊技术的屏幕截图:

【问题讨论】:

  • 您在代码中的画布分辨率为 640 x 360,但您提供的图像是 1382 x 750。设置画布 CSS 宽度和高度以匹配画布宽度和高度。例如 canvas{width:640px;height:360px;} 画布宽度和高度设置画布分辨率(它包含的像素数),而 CSS 宽度和高度设置画布显示尺寸(页面上有多大) 为了获得最佳效果,您必须确保显示尺寸匹配决议。
  • 感谢您的回答,但我只是放大了浏览器以显示问题,问题与 640x360 的画布相同。我将编辑我的问题,使其更加明确。
  • 您的评论启发了一个解决方法:渲染一个 1280x720 的画布,然后使用 CSS 将其缩小到 640x360。这种方式 Firefox 进行抗锯齿,但我仍然希望直接通过上下文 API 进行抗锯齿。
  • 那么你无能为力。 FireFox 不支持ctx.imageSmoothingQuality = "high" 质量损失是因为画布是为速度而不是质量而设计的,并且您正在拍摄高分辨率图像并将其压缩到较低分辨率的画布中,如果您通过 Photoshop 缩小原始图像(无论绘图包) 来匹配画布 res 你会得到更好的结果,因为油漆包在减小尺寸方面会做得更好。
  • 这是在回答您的问题吗? stackoverflow.com/questions/17861447/…

标签: firefox html5-canvas antialiasing


【解决方案1】:

高品质羽绒样品。

这个答案提供了一个下采样器,它将在浏览器中获得一致的结果,并允许进行统一和非统一的广泛缩减。

优点

它在质量方面具有显着优势,因为它可以使用 64 位浮点 JS 数字,而不是 GPU 使用的 32 位浮点数。它还减少了 sRGB,而不是 2d API 使用的较低质量的 RGB。

缺点

它的缺点当然是性能。这在对大图像进行下采样时可能会变得不切实际。但是它可以通过 web worker 并行运行,因此不会阻塞主 UI。

仅适用于 50% 或以下的下采样。只需几个小模块即可扩展到任何大小,但该示例选择了速度而不是多功能性。

查看结果的 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, w, 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 , 0);
    const srcData = sCtx.getImageData(0,0,srcW,srcH).data;
    const destData = dCtx.getImageData(0,0,w,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,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, 0, 0, ctx.canvas.width, ctx.canvas.height);
    const downScaleByAPI_B = Object.assign(document.createElement("canvas"), {width: downScaled.width, height: downScaled.height});
    const ctx1 = downScaleByAPI_B.getContext("2d");
    ctx1.drawImage(img, 0, 0, ctx.canvas.width, 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, CC BY-SA 4.0,</a></cite>

更多关于 RGB V sRGB

sRGB 是所有数字媒体设备用来显示内容的色彩空间。 人类看到的亮度对数意味着显示设备的动态范围是 1 到 ~200,000,这需要每个通道 18 位。

显示缓冲区通过将通道值存储为 sRGB 来克服这个问题。亮度范围为 0 - 255。当显示硬件将此值转换为光子时,它首先将 255 值扩展为 2.2 的幂,以提供所需的高动态范围。

问题在于处理显示缓冲区(2D API)会忽略这一点并且不会扩展 sRGB 值。它被视为 RGB,导致颜色混合不正确。

该图像显示了 sRGB 和 RGB(2D API 使用的 RGB)渲染之间的差异。

注意中间和右侧图像上的暗像素。那是RGB渲染的结果。左图使用 sRGB 渲染,不损失亮度。

【讨论】:

  • "2d API 使用的 RGB" 你从哪里得到的?在this 出现之前,实现必须使用用于通过CSS 呈现画布元素的颜色空间which is sRGB
  • @Kaiido 所有数字媒体从 dot 开始就一直在使用 sRGB。我们不是在谈论显示硬件。我们在谈论渲染看看你的屏幕在红线RGB上绘制绿色时看到暗边缘,红色与绿色混合不应该更暗(趋向于深红色或深绿色)。使用 sRGB 进行渲染时,此问题已得到修复,2D API 无法做到这一点!
  • 我也不是在谈论显示部分。这是per specs 他们应该只在drawImage 和渲染到设备时转换颜色。两者之间的一切都必须是 sRGB。
  • @Kaiido 请参阅答案底部图像中的更新。 2D API 显然不使用 sRGB。为什么会这样。因为“Porter Duff Compositing Operators”.drafts.fxtf.org/compositing-1/#porterduffcompositingoperators 所有渲染的内容都是通过复合操作默认的“source-over”添加到画布上的。 Porter Duff 操作是 RGB 而不是 sRGB。直到几十年前的 Porter Duff 被淘汰,转而支持 gamma 校正合成,我们都陷入了 RGB 渲染混乱。
  • 感谢@Blindman67 的详细回答,您清楚地解释了缩小图像并提供有效的javascript 实现的算法。您对 sRGB 的解释也很有启发性和说服力。现在在我的特殊情况下,由于我只想显示图像文件的小预览,我将求助于使用 CSS 或画布缩放,以便由浏览器处理。
猜你喜欢
  • 2011-09-11
  • 2016-10-03
  • 2021-02-07
  • 1970-01-01
  • 2011-08-20
  • 1970-01-01
  • 2015-01-21
  • 1970-01-01
  • 2013-02-17
相关资源
最近更新 更多