2

I want to display a scaled down image in a canvas. When doing so, jagged edges appear on the bottom of the spaceship, it seems that antialiasing is disabled.

Here is a zoom of the image produced in Firefox:
enter image description here
The image is very sharp but we see jagged edges (especially the bottom of the spaceship, the windshield, the nose wing).

And in Chrome:
enter image description here
The image stays sharp (the portholes stay sharp, all lines) and we have no jagged edges. Only the clouds got blurred a little.

And in Chrome with smoothing disabled:
enter image description here

I tried setting the property imageSmoothingEnabled to true, but it has no effect in Firefox, my example:

<!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>

How can I apply antialiasing?

Edit: Antialising is applied when viewing the site in Chrome, but not in Firefox.

Edit 2: Compare the images more precisely. Actually it seems that Firefox applies some image enhancement, but does not disable it when setting imageSmoothingEnabled to false

Edit 3: Replace mentions of antialising to smoothing because it seems that there is more than just AA involved.

Workarounds so far (I am eager to hear your proposals!):

  • render the canvas with more pixels, then shrink it via CSS -> shifts the quality / performance cursor manually
  • use an offline tool to resize the image -> not interactive
  • apply 1px blur to the image -> no more jagged edges, but obviously a blurry image

Screenshot with the blur technique:
enter image description here

Louis Coulet
  • 3,663
  • 1
  • 21
  • 39
  • You have the canvas resolution in code as 640 by 360 but the image you have provided is 1382 by 750. Set the canvas CSS width and height to match the canvas width and height. eg `canvas{width:640px;height:360px;}` Canvas width and height set the canvas resolution (number of pixels it contains) while CSS width and height set the canvas display size (how big it is on the page) For the best result you must ensure that the display size matches the resolution. – Blindman67 Dec 22 '20 at 14:20
  • Thank you for your answer, but I just zoomed my browser to show the issue, the problem is the same with canvas at 640x360. I will edit my question to be more explicit. – Louis Coulet Dec 22 '20 at 14:28
  • Your comment inspires a workaround: render a 1280x720 canvas, then scale it down to 640x360 with CSS. This way Firefox does antialiasing, but I would still like to have antialiasing via the context API directly. – Louis Coulet Dec 22 '20 at 14:35
  • Then there is not much you can do. FireFox does not support `ctx.imageSmoothingQuality = "high"` The quality loss is because the canvas is designed for speed rather than quality and you are taking a high res image and squashing it into a lower res canvas, If you reduce the original image via photoshop (whatever drawing package) to match the canvas res you will get better results as paint packages will do a far better job at reducing the size. – Blindman67 Dec 22 '20 at 14:50
  • Thank you again, I made more screenshots to describe the issue more accurately. I understand the compromise between quality and speed, but Chrome's output looks way nicer, and imageSmoothingEnabled has no effect in Firefox. Indeed using an external tool would work, but I want resize the image in my app! – Louis Coulet Dec 22 '20 at 15:35
  • I image `.imageSmoothingEnabled` only works correctly in Firefox when you up-scale an image. The artifacts you're seeing seem to be a fundamental part of Firefox's canvas scaling algorithm. Using something like `ctx.save(); ctx.scale(1/6, 1/6); ctx.drawImage(img, 0, 0, 3840, 2160, 0, 0, 3840, 2160); ctx.restore();` produces the same effect. – Ouroborus Dec 24 '20 at 15:32
  • 1
    Is this answering your question? https://stackoverflow.com/questions/17861447/html5-canvas-drawimage-how-to-apply-antialiasing – Pavol Velky Dec 24 '20 at 19:02
  • Or this? https://stackoverflow.com/questions/18922880/html5-canvas-resize-downscale-image-high-quality – Kaiido Dec 26 '20 at 03:12
  • Does this answer your question? [Html5 canvas drawImage: how to apply antialiasing](https://stackoverflow.com/questions/17861447/html5-canvas-drawimage-how-to-apply-antialiasing) – Kaiido Jan 05 '21 at 09:27
  • Thank you for all your suggestions! https://stackoverflow.com/questions/17861447 does not answer my question: it applies a blur filter, it removes jagged edges but the result is blurry (though it is subtle) – Louis Coulet Jan 05 '21 at 14:35

1 Answers1

4

High quality down sample.

This answer presents a down sampler that will have consistent results across browsers and allows for a wide range of reductions both uniform and non uniform.

Pros

It has a significant advantage in terms of quality as it can use 64bit floating point JS numbers rather than the 32bit float used by the GPU. It also does the reduction in sRGB rather than the lower quality RGB used by the 2d API.

Cons

Its drawback is of course performance. This could make it impractical when down sampling large images. However it can be run in parallel via web workers thus not block the main UI.

Only for down sampling at or below 50%. It will only take a few minor mods to scale to any size, but the example opted for speed over versatility.

The quality gain for 99% of people viewing the result will barely be noticeable.

Area samples

The method samples the source pixels under the new destination pixel calculating the color based on overlapping pixel areas.

The following illustration will help in understanding how it work.

enter image description here

  • Left side shows smaller high res source pixels (blue) overlapped by new low res destination pixel (red).
  • The right illiterates which parts of the source pixels contribute to the destination pixels color. The % values are the % the destination pixel overlaps each source pixel.

Overview of process.

First we create 3 values to hold the new R,G,B color to zero (black)

We perform the following for each pixel under the destination pixel.

  • Calculate the overlap area between the destination and source pixel.
  • Divide the source pixels overlap by the destination pixels area to get a fractional contribution the source pixel has to the destination pixels color
  • Convert the source pixel RGB to sRGB, normalize and multiply by the fractional contribution calculated in previous step, then add the result to the stored R,G,B values.

When all pixel under the new pixel have been processed, the new colors R,G,B values are converted back to RGB and added to the image data.

When done the pixel data is added to a canvas which is returned ready for use

Example

The example down-scales the image by approx ~ 1/4

When done the example displays the scaled image and the images scaled via the 2D API.

You can click on the top image to swap between the two methods and compare results.

/* 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>

More on RGB V sRGB

sRGB is the color space that all digital media devices use to display content. Humans see brightness logarithmic meaning that the dynamic range of a display device is 1 to ~200,000 which would require 18bits per channel.

Display buffers overcome this by storing the channel values as sRGB. The brightness in the range 0 - 255. When the display hardware converts this value to photons it first expands 255 values by raising it to the power of 2.2 as to provide the high dynamic range needed.

The problem is that processing the display buffer (2D API) ignores this and does not expand the sRGB values. It is treated as RGB resulting in incorrect mixing of color.

The image shows the difference between sRGB and RGB (RGB as used by the 2D API) rendering.

Note the dark pixels on the center and right image. That is the result of RGB rendering. The left image is rendered using sRGB and does not lose brightness.

enter image description here

Blindman67
  • 51,134
  • 11
  • 73
  • 136
  • "RGB used by the 2d API" where do you get that from? Until [this](https://github.com/WICG/canvas-color-space/blob/master/CanvasColorSpaceProposal.md) comes to life, implementations have to use the color space used to render the canvas element by CSS, [which is sRGB](https://www.w3.org/TR/css-color-4/#untagged). – Kaiido Dec 26 '20 at 02:58
  • @Kaiido All digital media has been using sRGB since day dot. We are not talking about display hardware. We are talking about rendering Look at your screen see the dark edges when drawing green on red lines RGB, Red mixed with green should not be darker (tending to dark red or dark green). This is fixed when rendering using sRGB tending to yellow orange which the 2D API CAN NOT DO!.. I will add example to answer. – Blindman67 Dec 26 '20 at 04:35
  • I'm not talking about the display part either. It's [per specs](https://html.spec.whatwg.org/multipage/canvas.html#colour-spaces-and-colour-correction) they should convert colors only at drawImage and when rendering to device. Everything in between must be sRGB. – Kaiido Dec 26 '20 at 04:46
  • 1
    @Kaiido See update in answer bottom image. 2D API clearly does not use sRGB. Why is this so. Because "Porter Duff Compositing Operators".https://drafts.fxtf.org/compositing-1/#porterduffcompositingoperators All rendered content is added to the canvas via the composite operations default "source-over". Porter Duff operations are RGB not sRGB. Until the decades old Porter Duff is dropped in favor of gamma corrected compositing we are stuck in a RGB rendering mess. – Blindman67 Dec 26 '20 at 04:59
  • Thank you @Blindman67 for this detailed answer, you explain clearly your algorithm to scale down the image and give a working javascript implementation. Your explanations on sRGB are also very instructive and convincing. Now in my particuliar case, as I only want to display small previews of image files, I will resort to using CSS or canvas scaling so that it is handled by the browser. – Louis Coulet Dec 27 '20 at 16:31
  • As for the cause of the aliased smoothing in Firefox, you say that it is bacause the scaling algorithm is specified by the W3C and prevents using sRGB images!? If so, it looks like Chrome has decided not to respect it! – Louis Coulet Dec 27 '20 at 16:35
  • 1
    @LouisCoulet No the use if sRGB (lack there of) is but a part of the problem. The issue is that the algorithms used by 2D API are not designed to do such high reductions as they are optimized for performance – Blindman67 Dec 28 '20 at 01:54