How to scale images on a html5 canvas with better interpolation?
Asked Answered
J

1

43

First of all: what am I trying to do?

I have an application to view images. It uses the canvas element to render the image. You can zoom in, you can zoom out, and you can drag it around. This part works perfectly right now.

But let's say I have an image with a lot of text. It has a resolution of 1200x1700, and my canvas has 1200x900. Initially, when zoomed out, this leads to a rendered resolution of ~560x800.

My actual drawing looks like this:

drawImage(src, srcOffsetX, srcOffsetY, sourceViewWidth, sourceViewHeight,
destOffsetX, destOffsetY, destWidth, destHeight);

Small text on this image looks really, really bad, especially when compared to other image viewers (e.g. IrfanView), or even the html < img > element.

I figured out that the browsers interpolation algorithm is the cause of this problem. Comparing different browsers showed that Chrome renders scaled images the best, but still not good enough.

Well I searched in every corner of the Interwebs for 4-5 hours straight and did not find what I need. I found the "imageSmoothingEnabled" option, "image-rendering" CSS styles which you can not use on canvas, rendering at float positions and many JavaScript implementations of interpolation algorithms (those are far to slow for my purpose).

You may ask why I am telling you all of this: to save you the time to give me answers I already know

So: is there any good and fast way to have better interpolation? My current idea is to create an image object, resize this (because img has good interpolation when scaled!) and render it then. Unfortunately, applying img.width seems only to affect the displayed width...

Update: Thanks to Simon, I could solve my problem. Here is the dynamic scaling algorithm I used. Notice that it keeps the aspect ratio, the height parameter is only for avoiding more float computing. It only scales down right now.

scale(destWidth, destHeight){
        var start = new Date().getTime();
        var scalingSteps = 0;
        var ctx = this._sourceImageCanvasContext;
        var curWidth = this._sourceImageWidth;
        var curHeight = this._sourceImageHeight;

        var lastWidth = this._sourceImageWidth;
        var lastHeight = this._sourceImageHeight;

        var end = false;
        var scale=0.75;
        while(end==false){
            scalingSteps +=1;
            curWidth *= scale;
            curHeight *= scale;
            if(curWidth < destWidth){
                curWidth = destWidth;
                curHeight = destHeight;
                end=true;
            }
            ctx.drawImage(this._sourceImageCanvas, 0, 0, Math.round(lastWidth), Math.round(lastHeight), 0, 0, Math.round(curWidth), Math.round(curHeight));
            lastWidth = curWidth;
            lastHeight = curHeight;
        }
        var endTime =new Date().getTime();
        console.log("execution time: "+ ( endTime - start) + "ms. scale per frame: "+scale+ " scaling step count: "+scalingSteps);
    }
Jat answered 12/9, 2013 at 10:11 Comment(6)
By "better interpolation" you mean an interpolation dedicated to a better readability of text when scaled down ?Singer
better interpolation in general. the poor quality of text is just the most noticeable thing you see... and text readability is, sadly, very important for this project. but if you know a way to only make text look better, please tell me then!Jat
possible duplicate of Html5 canvas drawImage: how to apply antialiasingAgnusago
strange. i did not find that page, despite searching for so long..Jat
You can try embedding the text as HTML in a foreignObject inside SVG which you can then render to the canvas. I usually get much better text rendering that way, then form the canvas methods directly.Multifarious
ctx.imageSmoothingEnabled = false; seems relevant here (more for upscaling)Underpin
E
60

You need to "step down" several times. Instead of scaling from a very large image to a very small, you need to re-scale it to intermediary sizes.

Consider an image you want to draw at 1/6 scale. You could do this:

var w = 1280;
var h = 853;

ctx.drawImage(img, 0, 0, w/6, h/6);   

Or you could draw it to an in-memory canvas at 1/2 scale, then 1/2 scale again, then 1/2 scale again. The result is a 1/6 scale image, but we use three steps:

var can2 = document.createElement('canvas');
can2.width = w/2;
can2.height = w/2;
var ctx2 = can2.getContext('2d');

ctx2.drawImage(img, 0, 0, w/2, h/2);
ctx2.drawImage(can2, 0, 0, w/2, h/2, 0, 0, w/4, h/4);
ctx2.drawImage(can2, 0, 0, w/4, h/4, 0, 0, w/6, h/6);

Then you can draw that back to your original context:

ctx.drawImage(can2, 0, 0, w/6, h/6, 0, 200, w/6, h/6);

You can see the difference live, here:

var can = document.getElementById('canvas1');
var ctx = can.getContext('2d');

var img = new Image();
var w = 1280;
var h = 853;
img.onload = function() {
    // step it down only once to 1/6 size:
    ctx.drawImage(img, 0, 0, w/6, h/6);   
    
    // Step it down several times
    var can2 = document.createElement('canvas');
    can2.width = w/2;
    can2.height = w/2;
    var ctx2 = can2.getContext('2d');
    
    // Draw it at 1/2 size 3 times (step down three times)
    
    ctx2.drawImage(img, 0, 0, w/2, h/2);
    ctx2.drawImage(can2, 0, 0, w/2, h/2, 0, 0, w/4, h/4);
    ctx2.drawImage(can2, 0, 0, w/4, h/4, 0, 0, w/6, h/6);
    ctx.drawImage(can2, 0, 0, w/6, h/6, 0, 200, w/6, h/6);
}



img.src = 'http://upload.wikimedia.org/wikipedia/commons/thumb/a/a4/Equus_quagga_%28Namutoni%2C_2012%29.jpg/1280px-Equus_quagga_%28Namutoni%2C_2012%29.jpg'
canvas {
    border: 1px solid gray;
}
<canvas id="canvas1" width="400" height="400"></canvas>

View same snippet on jsfiddle.

Earle answered 12/9, 2013 at 13:41 Comment(10)
Thanks a lot for this idea! The second image looks really much better! I will try that now and look how performant it is...Jat
wow i am really impressed. rescaling in 15 steps needs only 2ms! (ok, i have a i7 running here, but still....) thank youJat
Yeah drawImage() is one of the fastest operations you can do on canvas, so don't feel bad about 8x step downs on image scalesEarle
Nice idea. This is the principle of mip-mapping : Having several resolution of one image allows to get rid of the high frequency part of the image, which is noise at lower resolutions. It allows also to draw faster, since less source pixels are involved. Notice that the size of all scaled down images scaled down by 2X2 down to 1pxX1px is less than the size of the original. (1+1/4+1/16+...)Ruscio
How do you mean that, less size? Size in cm²?Jat
I heard that scaling the image down, in a way that every step reduces the amount of pixels by a half, improves quality, because algorithms can work better then. That would mean i use a scale of sqrt(0.5) in my above algorithm. Anyone knows if that is true? I don't see a big difference compared to e.g. 0.75, but i don't want to rely only on my tired eyes!:)Jat
Square roots are slow. If it looks good enough for you I wouldn't worry about it otherwise.Earle
Could this be done better using a loop? Thus you could make a function where by you supply an argument of target scale, and total steps. Also, what is the recommended number of step intervals to maintain good quality?Audrey
Any idea on how to do this to any scale number? For instance, I want the result to be 0.2035048049745619 of the original, I have a picture with 1769px of width and need to be 360px (on runtime)Impenetrable
1/2 * 1/2 * 1/2 = 1/8, not 1/6. You can stop at 1/6 if you want but if writing a universal algorithm you wouldn't bother with such a size.Xenogamy

© 2022 - 2024 — McMap. All rights reserved.