Image de redimensionnement du canevas HTML5 (échelle inférieure) Haute qualité?

149

J'utilise des éléments de canevas html5 pour redimensionner les images dans mon navigateur. Il s'avère que la qualité est très faible. J'ai trouvé ceci: Désactivez l'interpolation lors de la mise à l'échelle d'un <canvas>, mais cela n'aide pas à augmenter la qualité.

Vous trouverez ci-dessous mon code css et js ainsi que l'image scallée avec Photoshop et mise à l'échelle dans l'API canvas.

Que dois-je faire pour obtenir une qualité optimale lors de la mise à l'échelle d'une image dans le navigateur?

Remarque: je souhaite réduire une grande image à une petite image, modifier la couleur d'une toile et envoyer le résultat de la toile au serveur.

CSS:

canvas, img {
    image-rendering: optimizeQuality;
    image-rendering: -moz-crisp-edges;
    image-rendering: -webkit-optimize-contrast;
    image-rendering: optimize-contrast;
    -ms-interpolation-mode: nearest-neighbor;
}

JS:

var $img = $('<img>');
var $originalCanvas = $('<canvas>');
$img.load(function() {


   var originalContext = $originalCanvas[0].getContext('2d');   
   originalContext.imageSmoothingEnabled = false;
   originalContext.webkitImageSmoothingEnabled = false;
   originalContext.mozImageSmoothingEnabled = false;
   originalContext.drawImage(this, 0, 0, 379, 500);
});

L'image redimensionnée avec Photoshop:

entrez la description de l'image ici

L'image redimensionnée sur toile:

entrez la description de l'image ici

Éditer:

J'ai essayé de faire une réduction d'échelle en plus d'une étape comme proposé dans:

Redimensionner une image dans un canevas HTML5 et un canevas Html5 drawImage: comment appliquer l'anticrénelage

C'est la fonction que j'ai utilisée:

function resizeCanvasImage(img, canvas, maxWidth, maxHeight) {
    var imgWidth = img.width, 
        imgHeight = img.height;

    var ratio = 1, ratio1 = 1, ratio2 = 1;
    ratio1 = maxWidth / imgWidth;
    ratio2 = maxHeight / imgHeight;

    // Use the smallest ratio that the image best fit into the maxWidth x maxHeight box.
    if (ratio1 < ratio2) {
        ratio = ratio1;
    }
    else {
        ratio = ratio2;
    }

    var canvasContext = canvas.getContext("2d");
    var canvasCopy = document.createElement("canvas");
    var copyContext = canvasCopy.getContext("2d");
    var canvasCopy2 = document.createElement("canvas");
    var copyContext2 = canvasCopy2.getContext("2d");
    canvasCopy.width = imgWidth;
    canvasCopy.height = imgHeight;  
    copyContext.drawImage(img, 0, 0);

    // init
    canvasCopy2.width = imgWidth;
    canvasCopy2.height = imgHeight;        
    copyContext2.drawImage(canvasCopy, 0, 0, canvasCopy.width, canvasCopy.height, 0, 0, canvasCopy2.width, canvasCopy2.height);


    var rounds = 2;
    var roundRatio = ratio * rounds;
    for (var i = 1; i <= rounds; i++) {
        console.log("Step: "+i);

        // tmp
        canvasCopy.width = imgWidth * roundRatio / i;
        canvasCopy.height = imgHeight * roundRatio / i;

        copyContext.drawImage(canvasCopy2, 0, 0, canvasCopy2.width, canvasCopy2.height, 0, 0, canvasCopy.width, canvasCopy.height);

        // copy back
        canvasCopy2.width = imgWidth * roundRatio / i;
        canvasCopy2.height = imgHeight * roundRatio / i;
        copyContext2.drawImage(canvasCopy, 0, 0, canvasCopy.width, canvasCopy.height, 0, 0, canvasCopy2.width, canvasCopy2.height);

    } // end for


    // copy back to canvas
    canvas.width = imgWidth * roundRatio / rounds;
    canvas.height = imgHeight * roundRatio / rounds;
    canvasContext.drawImage(canvasCopy2, 0, 0, canvasCopy2.width, canvasCopy2.height, 0, 0, canvas.width, canvas.height);


}

Voici le résultat si j'utilise un dimensionnement en 2 étapes:

entrez la description de l'image ici

Voici le résultat si j'utilise un dimensionnement en 3 étapes:

entrez la description de l'image ici

Voici le résultat si j'utilise un dimensionnement en 4 étapes:

entrez la description de l'image ici

Voici le résultat si j'utilise un dimensionnement en 20 étapes:

entrez la description de l'image ici

Remarque: Il s'avère que d'une étape à deux étapes, il y a une grande amélioration de la qualité de l'image, mais plus vous ajoutez d'étapes au processus, plus l'image devient floue.

Y a-t-il un moyen de résoudre le problème que l'image devient plus floue à mesure que vous ajoutez d'étapes?

Edit 2013-10-04: J'ai essayé l'algorithme de GameAlchemist. Voici le résultat par rapport à Photoshop.

Image de PhotoShop:

Image de PhotoShop

Algorithme de GameAlchemist:

Algorithme de GameAlchemist

confiler
la source
2
Vous pouvez essayer de redimensionner progressivement votre image: stackoverflow.com/questions/18761404/…
MarkE
1
duplication possible du canevas Html5 drawImage: comment appliquer l'anticrénelage . Voyez si non cela fonctionne. Si les images sont grandes et réduites à une petite taille, vous devrez le faire par étapes (voir les images d'exemple dans le lien)
2
@confile désactiver l'interpolation aggravera les choses. Vous voulez garder cela activé. Regardez le lien que j'ai fourni ci-dessus. J'y montre comment utiliser les étapes pour réduire les images plus grandes et conserver la qualité. Et comme Scott le dit, vous voulez privilégier la qualité à la vitesse.
1
@ Ken-AbdiasSoftware J'ai essayé de vous approcher, mais le problème est que cela va empirer avec le nombre de tours que j'utilise pour la mise à l'échelle par étapes. Une idée de comment résoudre ce problème?
confilez le
3
Les chances de reproduire les fonctionnalités d'un logiciel de retouche photo professionnel coûteux en utilisant HTML5 sont sûrement minces? Vous pouvez probablement vous en approcher (ish), mais exactement comme cela fonctionne dans Photoshop, j'imagine que ce serait impossible!
Liam

Réponses:

171

Puisque votre problème est de réduire l'échelle de votre image, il ne sert à rien de parler d'interpolation - qui consiste à créer des pixels -. Le problème ici est le sous-échantillonnage.

Pour sous-échantillonner une image, nous devons transformer chaque carré de p * p pixels de l'image d'origine en un seul pixel de l'image de destination.

Pour des raisons de performances, les navigateurs effectuent un sous-échantillonnage très simple: pour créer la plus petite image, ils choisiront juste UN pixel dans la source et utiliseront sa valeur pour la destination. qui «oublie» certains détails et ajoute du bruit.

Pourtant, il y a une exception à cela: comme le sous-échantillonnage d'image 2X est très simple à calculer (en moyenne 4 pixels pour en faire un) et est utilisé pour les pixels rétine / HiDPI, ce cas est géré correctement - le navigateur utilise 4 pixels pour faire une-.

MAIS ... si vous utilisez plusieurs fois un sous-échantillonnage 2X, vous serez confronté au problème que les erreurs d'arrondi successives ajouteront trop de bruit.
Pire encore, vous ne redimensionnerez pas toujours par une puissance de deux, et redimensionner à la puissance la plus proche + un dernier redimensionnement est très bruyant.

Ce que vous recherchez, c'est un sous-échantillonnage au pixel parfait, c'est-à-dire: un ré-échantillonnage de l'image qui prendra en compte tous les pixels d'entrée - quelle que soit l'échelle -.
Pour ce faire, nous devons calculer, pour chaque pixel d'entrée, sa contribution à un, deux ou quatre pixels de destination selon que la projection mise à l'échelle des pixels d'entrée se trouve juste à l'intérieur d'un pixel de destination, chevauche une bordure X, une bordure Y ou les deux. .
(Un schéma serait bien ici, mais je n'en ai pas.)

Voici un exemple de l'échelle de la toile par rapport à mon échelle parfaite en pixels à l'échelle 1/3 d'un zombat.

Notez que l'image peut être mise à l'échelle dans votre navigateur et qu'elle est .jpegized par SO.
Pourtant, on voit qu'il y a beaucoup moins de bruit surtout dans l'herbe derrière le wombat, et les branches à sa droite. Le bruit dans la fourrure la rend plus contrastée, mais on dirait qu'il a des cheveux blancs - contrairement à l'image source -.
La bonne image est moins accrocheuse mais définitivement plus jolie.

entrez la description de l'image ici

Voici le code pour effectuer la réduction d'échelle parfaite des pixels:

résultat du violon: http://jsfiddle.net/gamealchemist/r6aVp/embedded/result/
violon lui-même: http://jsfiddle.net/gamealchemist/r6aVp/

// scales the image by (float) scale < 1
// returns a canvas containing the scaled image.
function downScaleImage(img, scale) {
    var imgCV = document.createElement('canvas');
    imgCV.width = img.width;
    imgCV.height = img.height;
    var imgCtx = imgCV.getContext('2d');
    imgCtx.drawImage(img, 0, 0);
    return downScaleCanvas(imgCV, scale);
}

// scales the canvas by (float) scale < 1
// returns a new canvas containing the scaled image.
function downScaleCanvas(cv, scale) {
    if (!(scale < 1) || !(scale > 0)) throw ('scale must be a positive number <1 ');
    var sqScale = scale * scale; // square scale = area of source pixel within target
    var sw = cv.width; // source image width
    var sh = cv.height; // source image height
    var tw = Math.floor(sw * scale); // target image width
    var th = Math.floor(sh * scale); // target image height
    var sx = 0, sy = 0, sIndex = 0; // source x,y, index within source array
    var tx = 0, ty = 0, yIndex = 0, tIndex = 0; // target x,y, x,y index within target array
    var tX = 0, tY = 0; // rounded tx, ty
    var w = 0, nw = 0, wx = 0, nwx = 0, wy = 0, nwy = 0; // weight / next weight x / y
    // weight is weight of current source point within target.
    // next weight is weight of current source point within next target's point.
    var crossX = false; // does scaled px cross its current px right border ?
    var crossY = false; // does scaled px cross its current px bottom border ?
    var sBuffer = cv.getContext('2d').
    getImageData(0, 0, sw, sh).data; // source buffer 8 bit rgba
    var tBuffer = new Float32Array(3 * tw * th); // target buffer Float32 rgb
    var sR = 0, sG = 0,  sB = 0; // source's current point r,g,b
    /* untested !
    var sA = 0;  //source alpha  */    

    for (sy = 0; sy < sh; sy++) {
        ty = sy * scale; // y src position within target
        tY = 0 | ty;     // rounded : target pixel's y
        yIndex = 3 * tY * tw;  // line index within target array
        crossY = (tY != (0 | ty + scale)); 
        if (crossY) { // if pixel is crossing botton target pixel
            wy = (tY + 1 - ty); // weight of point within target pixel
            nwy = (ty + scale - tY - 1); // ... within y+1 target pixel
        }
        for (sx = 0; sx < sw; sx++, sIndex += 4) {
            tx = sx * scale; // x src position within target
            tX = 0 |  tx;    // rounded : target pixel's x
            tIndex = yIndex + tX * 3; // target pixel index within target array
            crossX = (tX != (0 | tx + scale));
            if (crossX) { // if pixel is crossing target pixel's right
                wx = (tX + 1 - tx); // weight of point within target pixel
                nwx = (tx + scale - tX - 1); // ... within x+1 target pixel
            }
            sR = sBuffer[sIndex    ];   // retrieving r,g,b for curr src px.
            sG = sBuffer[sIndex + 1];
            sB = sBuffer[sIndex + 2];

            /* !! untested : handling alpha !!
               sA = sBuffer[sIndex + 3];
               if (!sA) continue;
               if (sA != 0xFF) {
                   sR = (sR * sA) >> 8;  // or use /256 instead ??
                   sG = (sG * sA) >> 8;
                   sB = (sB * sA) >> 8;
               }
            */
            if (!crossX && !crossY) { // pixel does not cross
                // just add components weighted by squared scale.
                tBuffer[tIndex    ] += sR * sqScale;
                tBuffer[tIndex + 1] += sG * sqScale;
                tBuffer[tIndex + 2] += sB * sqScale;
            } else if (crossX && !crossY) { // cross on X only
                w = wx * scale;
                // add weighted component for current px
                tBuffer[tIndex    ] += sR * w;
                tBuffer[tIndex + 1] += sG * w;
                tBuffer[tIndex + 2] += sB * w;
                // add weighted component for next (tX+1) px                
                nw = nwx * scale
                tBuffer[tIndex + 3] += sR * nw;
                tBuffer[tIndex + 4] += sG * nw;
                tBuffer[tIndex + 5] += sB * nw;
            } else if (crossY && !crossX) { // cross on Y only
                w = wy * scale;
                // add weighted component for current px
                tBuffer[tIndex    ] += sR * w;
                tBuffer[tIndex + 1] += sG * w;
                tBuffer[tIndex + 2] += sB * w;
                // add weighted component for next (tY+1) px                
                nw = nwy * scale
                tBuffer[tIndex + 3 * tw    ] += sR * nw;
                tBuffer[tIndex + 3 * tw + 1] += sG * nw;
                tBuffer[tIndex + 3 * tw + 2] += sB * nw;
            } else { // crosses both x and y : four target points involved
                // add weighted component for current px
                w = wx * wy;
                tBuffer[tIndex    ] += sR * w;
                tBuffer[tIndex + 1] += sG * w;
                tBuffer[tIndex + 2] += sB * w;
                // for tX + 1; tY px
                nw = nwx * wy;
                tBuffer[tIndex + 3] += sR * nw;
                tBuffer[tIndex + 4] += sG * nw;
                tBuffer[tIndex + 5] += sB * nw;
                // for tX ; tY + 1 px
                nw = wx * nwy;
                tBuffer[tIndex + 3 * tw    ] += sR * nw;
                tBuffer[tIndex + 3 * tw + 1] += sG * nw;
                tBuffer[tIndex + 3 * tw + 2] += sB * nw;
                // for tX + 1 ; tY +1 px
                nw = nwx * nwy;
                tBuffer[tIndex + 3 * tw + 3] += sR * nw;
                tBuffer[tIndex + 3 * tw + 4] += sG * nw;
                tBuffer[tIndex + 3 * tw + 5] += sB * nw;
            }
        } // end for sx 
    } // end for sy

    // create result canvas
    var resCV = document.createElement('canvas');
    resCV.width = tw;
    resCV.height = th;
    var resCtx = resCV.getContext('2d');
    var imgRes = resCtx.getImageData(0, 0, tw, th);
    var tByteBuffer = imgRes.data;
    // convert float32 array into a UInt8Clamped Array
    var pxIndex = 0; //  
    for (sIndex = 0, tIndex = 0; pxIndex < tw * th; sIndex += 3, tIndex += 4, pxIndex++) {
        tByteBuffer[tIndex] = Math.ceil(tBuffer[sIndex]);
        tByteBuffer[tIndex + 1] = Math.ceil(tBuffer[sIndex + 1]);
        tByteBuffer[tIndex + 2] = Math.ceil(tBuffer[sIndex + 2]);
        tByteBuffer[tIndex + 3] = 255;
    }
    // writing result to canvas.
    resCtx.putImageData(imgRes, 0, 0);
    return resCV;
}

C'est assez gourmand en mémoire, puisqu'une mémoire tampon flottante est nécessaire pour stocker les valeurs intermédiaires de l'image de destination (-> si on compte le canevas résultat, on utilise 6 fois la mémoire de l'image source dans cet algorithme).
C'est aussi assez cher, puisque chaque pixel source est utilisé quelle que soit la taille de la destination, et nous devons payer pour le getImageData / putImageDate, assez lent également.
Mais il n'y a aucun moyen d'être plus rapide que de traiter chaque valeur source dans ce cas, et la situation n'est pas si mauvaise: pour mon image 740 * 556 d'un wombat, le traitement prend entre 30 et 40 ms.

GameAlchemist
la source
Cela pourrait-il être plus rapide si vous mettez l'image à l'échelle avant de la mettre dans le canevas?
confilez
je ne comprends pas ... il semble que c'est ce que je fais. Le tampon ainsi que le canevas que je crée (resCV) ont la taille de l'image mise à l'échelle. Je pense que le seul moyen de l'obtenir plus rapidement serait d'utiliser le calcul d'entiers de type breshensam. Mais 40 ms n'est lent que pour un jeu vidéo (25 fps), pas pour une application de tirage au sort.
GameAlchemist
voyez-vous une chance de rendre votre algorithme plus rapide tout en gardant la qualité?
confilez
1
J'ai essayé d'arrondir le tampon (dernière partie de l'algorithme) en utilisant 0 | au lieu de Mat.ceil. C'est un peu plus rapide. Mais de toute façon, il y a pas mal de surcharge avec get / putImageData et encore une fois, nous ne pouvons pas éviter de traiter chaque pixel.
GameAlchemist
4
Ok, alors j'ai regardé le code: vous étiez très proche de la solution. Deux erreurs: vos index étaient décalés d'un pour tX + 1 (ils étaient + 3, + 4, + 5, + 6 au lieu de +4, +5, +6, +7), et changer de ligne dans rgba est un mul par 4, pas 3. Je viens de tester 4 valeurs aléatoires pour vérifier (0,1, 0,15, 0,33, 0,8), cela me semblait correct. votre violon mis à jour est ici: jsfiddle.net/gamealchemist/kpQyE/3
GameAlchemist
51

Rééchantillonnage rapide de la toile avec une bonne qualité: http://jsfiddle.net/9g9Nv/442/

Mise à jour: version 2.0 (plus rapide, web workers + objets transférables) - https://github.com/viliusle/Hermite-resize

/**
 * Hermite resize - fast image resize/resample using Hermite filter. 1 cpu version!
 * 
 * @param {HtmlElement} canvas
 * @param {int} width
 * @param {int} height
 * @param {boolean} resize_canvas if true, canvas will be resized. Optional.
 */
function resample_single(canvas, width, height, resize_canvas) {
    var width_source = canvas.width;
    var height_source = canvas.height;
    width = Math.round(width);
    height = Math.round(height);

    var ratio_w = width_source / width;
    var ratio_h = height_source / height;
    var ratio_w_half = Math.ceil(ratio_w / 2);
    var ratio_h_half = Math.ceil(ratio_h / 2);

    var ctx = canvas.getContext("2d");
    var img = ctx.getImageData(0, 0, width_source, height_source);
    var img2 = ctx.createImageData(width, height);
    var data = img.data;
    var data2 = img2.data;

    for (var j = 0; j < height; j++) {
        for (var i = 0; i < width; i++) {
            var x2 = (i + j * width) * 4;
            var weight = 0;
            var weights = 0;
            var weights_alpha = 0;
            var gx_r = 0;
            var gx_g = 0;
            var gx_b = 0;
            var gx_a = 0;
            var center_y = (j + 0.5) * ratio_h;
            var yy_start = Math.floor(j * ratio_h);
            var yy_stop = Math.ceil((j + 1) * ratio_h);
            for (var yy = yy_start; yy < yy_stop; yy++) {
                var dy = Math.abs(center_y - (yy + 0.5)) / ratio_h_half;
                var center_x = (i + 0.5) * ratio_w;
                var w0 = dy * dy; //pre-calc part of w
                var xx_start = Math.floor(i * ratio_w);
                var xx_stop = Math.ceil((i + 1) * ratio_w);
                for (var xx = xx_start; xx < xx_stop; xx++) {
                    var dx = Math.abs(center_x - (xx + 0.5)) / ratio_w_half;
                    var w = Math.sqrt(w0 + dx * dx);
                    if (w >= 1) {
                        //pixel too far
                        continue;
                    }
                    //hermite filter
                    weight = 2 * w * w * w - 3 * w * w + 1;
                    var pos_x = 4 * (xx + yy * width_source);
                    //alpha
                    gx_a += weight * data[pos_x + 3];
                    weights_alpha += weight;
                    //colors
                    if (data[pos_x + 3] < 255)
                        weight = weight * data[pos_x + 3] / 250;
                    gx_r += weight * data[pos_x];
                    gx_g += weight * data[pos_x + 1];
                    gx_b += weight * data[pos_x + 2];
                    weights += weight;
                }
            }
            data2[x2] = gx_r / weights;
            data2[x2 + 1] = gx_g / weights;
            data2[x2 + 2] = gx_b / weights;
            data2[x2 + 3] = gx_a / weights_alpha;
        }
    }
    //clear and resize canvas
    if (resize_canvas === true) {
        canvas.width = width;
        canvas.height = height;
    } else {
        ctx.clearRect(0, 0, width_source, height_source);
    }

    //draw
    ctx.putImageData(img2, 0, 0);
}
ViliusL
la source
J'ai besoin de la meilleure qualité
confilez
18
corrigé, j'ai changé «bon» en «meilleur», est-ce que ça va maintenant? :RÉ. D'un autre côté, si vous voulez le meilleur rééchantillonnage possible, utilisez imagemagick.
ViliusL
@confile imgur.com était sûr à utiliser dans jsfiddle, mais les administrateurs ont fait quelque chose de mal? Vous ne voyez pas une bonne qualité, car votre navigateur donne une erreur fatale CORS. (impossible d'utiliser l'image de sites distants)
ViliusL
ok, vous pouvez utiliser n'importe quelle autre image PNG avec des zones transparentes. Une idée à ce sujet?
confilez le
4
@confile vous aviez raison, dans certains cas, les images transparentes avaient des problèmes dans les zones nettes. J'ai raté ces cas avec mon test. Le redimensionnement fixe a également corrigé la prise en charge des images distantes sur le violon: jsfiddle.net/9g9Nv/49
ViliusL
28

Suggestion 1 - Prolonger la canalisation du processus

Vous pouvez utiliser step-down comme je le décris dans les liens auxquels vous faites référence, mais vous semblez les utiliser de manière incorrecte.

Il n'est pas nécessaire de redimensionner les images à des ratios supérieurs à 1: 2 (généralement, mais sans s'y limiter). C'est là que vous devez effectuer une réduction drastique de l'échelle, vous devez le diviser en deux (et rarement, plus) étapes en fonction du contenu de l'image (en particulier lorsque des hautes fréquences telles que des lignes fines se produisent).

Chaque fois que vous sous-échantillonnez une image, vous perdez des détails et des informations. Vous ne pouvez pas vous attendre à ce que l'image résultante soit aussi claire que l'original.

Si vous réduisez ensuite les images en plusieurs étapes, vous perdrez beaucoup d'informations au total et le résultat sera médiocre comme vous l'avez déjà remarqué.

Essayez avec une seule étape supplémentaire, ou au sommet de deux.

Convolutions

Dans le cas de Photoshop, notez qu'il applique une convolution après que l'image a été rééchantillonnée, telle que la netteté. Ce n'est pas seulement une interpolation bi-cubique qui a lieu, donc pour émuler pleinement Photoshop, nous devons également ajouter les étapes que Photoshop effectue (avec la configuration par défaut).

Pour cet exemple, j'utiliserai ma réponse originale à laquelle vous vous référez dans votre article, mais j'y ai ajouté une convolution affinée pour améliorer la qualité en tant que post-processus (voir la démo en bas).

Voici le code pour ajouter un filtre de netteté (il est basé sur un filtre de convolution générique - j'ai mis la matrice de poids pour l'accentuation à l'intérieur ainsi qu'un facteur de mixage pour ajuster la prononciation de l'effet):

Usage:

sharpen(context, width, height, mixFactor);

La mixFactorvaleur est comprise entre [0.0, 1.0] et vous permet de minimiser l'effet de netteté - règle empirique: moins il y a de taille, moins l'effet est nécessaire.

Fonction (basée sur cet extrait ):

function sharpen(ctx, w, h, mix) {

    var weights =  [0, -1, 0,  -1, 5, -1,  0, -1, 0],
        katet = Math.round(Math.sqrt(weights.length)),
        half = (katet * 0.5) |0,
        dstData = ctx.createImageData(w, h),
        dstBuff = dstData.data,
        srcBuff = ctx.getImageData(0, 0, w, h).data,
        y = h;
        
    while(y--) {

        x = w;

        while(x--) {

            var sy = y,
                sx = x,
                dstOff = (y * w + x) * 4,
                r = 0, g = 0, b = 0, a = 0;

            for (var cy = 0; cy < katet; cy++) {
                for (var cx = 0; cx < katet; cx++) {

                    var scy = sy + cy - half;
                    var scx = sx + cx - half;

                    if (scy >= 0 && scy < h && scx >= 0 && scx < w) {

                        var srcOff = (scy * w + scx) * 4;
                        var wt = weights[cy * katet + cx];

                        r += srcBuff[srcOff] * wt;
                        g += srcBuff[srcOff + 1] * wt;
                        b += srcBuff[srcOff + 2] * wt;
                        a += srcBuff[srcOff + 3] * wt;
                    }
                }
            }

            dstBuff[dstOff] = r * mix + srcBuff[dstOff] * (1 - mix);
            dstBuff[dstOff + 1] = g * mix + srcBuff[dstOff + 1] * (1 - mix);
            dstBuff[dstOff + 2] = b * mix + srcBuff[dstOff + 2] * (1 - mix)
            dstBuff[dstOff + 3] = srcBuff[dstOff + 3];
        }
    }

    ctx.putImageData(dstData, 0, 0);
}

Le résultat de l'utilisation de cette combinaison sera:

DÉMO EN LIGNE ICI

Résultat sous-échantillonner et accentuer la convolution

Selon la quantité de netteté que vous souhaitez ajouter au mélange, vous pouvez obtenir un résultat de "flou" par défaut à très net:

Variations de netteté

Suggestion 2 - Implémentation d'algorithme de bas niveau

Si vous voulez obtenir le meilleur résultat en termes de qualité, vous devrez aller de bas niveau et envisager d'implémenter par exemple ce tout nouvel algorithme pour ce faire.

Voir Sous-échantillonnage d'image dépendant de l'interpolation (2011) de l'IEEE.
Voici un lien vers l'article dans son intégralité (PDF) .

Il n'y a pas d'implémentation de cet algorithme dans JavaScript AFAIK ou pour le moment, vous êtes donc prêt à vous lancer dans cette tâche.

L'essence est (extraits de l'article):

Abstrait

Un algorithme de sous-échantillonnage adaptatif orienté interpolation est proposé pour le codage d'images à faible débit binaire dans cet article. Étant donné une image, l'algorithme proposé est capable d'obtenir une image à basse résolution, à partir de laquelle une image de haute qualité avec la même résolution que l'image d'entrée peut être interpolée. Différent des algorithmes de sous-échantillonnage traditionnels, qui sont indépendants du processus d'interpolation, l'algorithme de sous-échantillonnage proposé articule le sous-échantillonnage au processus d'interpolation. Par conséquent, l'algorithme de sous-échantillonnage proposé est capable de conserver les informations d'origine de l'image d'entrée dans la plus grande mesure. L'image sous-échantillonnée est ensuite introduite en JPEG. Un post-traitement basé sur la variation totale (TV) est ensuite appliqué à l'image basse résolution décompressée. En fin de compte,Les résultats expérimentaux vérifient qu'en utilisant l'image sous-échantillonnée par l'algorithme proposé, une image interpolée avec une qualité beaucoup plus élevée peut être obtenue. En outre, l'algorithme proposé est capable d'atteindre des performances supérieures à JPEG pour le codage d'image à faible débit binaire.

Instantané sur papier

(voir le lien fourni pour tous les détails, formules, etc.)

Communauté
la source
C'est aussi une excellente solution. Je vous remercie!
confilez le
C'est une excellente solution. Je l'ai essayé sur des fichiers png avec des zones transparentes. Voici le résultat: jsfiddle.net/confile/5CD4N Avez-vous une idée de ce qu'il faut faire pour que cela fonctionne?
confilez le
1
c'est GENIUS! mais pouvez-vous expliquer ce que vous faites exactement? lol .. je veux totalement connaître les tenants et les aboutissants ... peut-être des ressources pour apprendre?
carinlynchin
1
@Carine qui peut être un peu trop pour un champ de commentaire médiocre :) mais, la réduction rééchantillonne un groupe de pixels pour en moyenne un nouveau représentant ce groupe. Il s'agit en fait d'un filtre passe-bas qui introduit un peu de flou dans l'ensemble. Pour compenser la perte de netteté, appliquez simplement une convolution de netteté. Comme la netteté peut être très prononcée, nous pouvons la mélanger avec l'image à la place afin de pouvoir contrôler le niveau de netteté. J'espère que cela donne un aperçu.
21

Si vous souhaitez utiliser uniquement le canevas, le meilleur résultat sera avec plusieurs descentes. Mais ce n'est pas encore assez bon. Pour une meilleure qualité, vous avez besoin d'une implémentation pure js. Nous venons de sortir pica - un downscaler à grande vitesse avec une qualité / vitesse variable. En bref, il redimensionne 1280 * 1024px en ~ 0,1s, et l'image 5000 * 3000px en 1s, avec la plus haute qualité (filtre lanczos à 3 lobes). Pica a une démo , où vous pouvez jouer avec vos images, vos niveaux de qualité et même l'essayer sur des appareils mobiles.

Pica n'a pas encore de masque flou, mais cela sera ajouté très bientôt. C'est beaucoup plus facile que d'implémenter un filtre de convolution haute vitesse pour le redimensionnement.

Vitaly
la source
16

Pourquoi utiliser le canevas pour redimensionner les images? Les navigateurs modernes utilisent tous une interpolation bicubique - le même processus utilisé par Photoshop (si vous le faites correctement) - et ils le font plus rapidement que le processus de canevas. Spécifiez simplement la taille d'image souhaitée (utilisez une seule dimension, hauteur ou largeur, pour redimensionner proportionnellement).

Ceci est pris en charge par la plupart des navigateurs, y compris les versions ultérieures d'IE. Les versions antérieures peuvent nécessiter un CSS spécifique au navigateur .

Une fonction simple (en utilisant jQuery) pour redimensionner une image serait comme ceci:

function resizeImage(img, percentage) {
    var coeff = percentage/100,
        width = $(img).width(),
        height = $(img).height();

    return {"width": width*coeff, "height": height*coeff}           
}

Ensuite, utilisez simplement la valeur renvoyée pour redimensionner l'image dans une ou les deux dimensions.

De toute évidence, vous pouvez apporter différentes améliorations, mais cela fait le travail.

Collez le code suivant dans la console de cette page et regardez ce qui arrive aux gravatars:

function resizeImage(img, percentage) {
    var coeff = percentage/100,
        width = $(img).width(),
        height = $(img).height();

    return {"width": width*coeff, "height": height*coeff}           
}

$('.user-gravatar32 img').each(function(){
  var newDimensions = resizeImage( this, 150);
  this.style.width = newDimensions.width + "px";
  this.style.height = newDimensions.height + "px";
});
Robusto
la source
2
Notez également que si vous ne spécifiez qu'une seule dimension, le navigateur (moderne) conservera automatiquement le rapport hauteur / largeur naturel de l'image.
André Dion
38
Peut-être qu'il a besoin d'envoyer l'image redimensionnée à un serveur.
Sergiu Paraschiv
2
@Sergiu: Pas nécessaire, mais notez que si vous passez d'une très petite image à une très grande, vous n'obtiendrez pas d'excellents résultats même à partir d'un serveur.
Robusto
2
@Robusto Je dois ensuite mettre l'image dans le canevas et l'envoyer au serveur plus tard. Je veux réduire une grande image à une petite, modifier la couleur dans une toile et envoyer le résultat au serveur. Que penses-tu que je devrais faire?
confilez
9
@Robusto C'est le problème. Afficher une petite image sur le client est facile. img.width et img.height est si trivial. Je veux le réduire une seule fois et pas à nouveau sur le serveur.
confilez
8

Ce n'est pas la bonne réponse pour les personnes qui ont vraiment besoin de redimensionner l'image elle-même, mais simplement de réduire la taille du fichier .

J'ai eu un problème avec les images «directement depuis l'appareil photo», que mes clients téléchargeaient souvent au format JPEG «non compressé».

Ce n'est pas si connu que le canevas prend en charge (dans la plupart des navigateurs 2017) pour changer la qualité du JPEG

data=canvas.toDataURL('image/jpeg', .85) # [1..0] default 0.92

Avec cette astuce, je pourrais réduire 4k x 3k photos avec> 10 Mo à 1 ou 2 Mo, bien sûr que cela dépend de vos besoins.

regarde ici

demi-bit
la source
4

Voici un service angulaire réutilisable pour le redimensionnement d'image / toile de haute qualité: https://gist.github.com/fisch0920/37bac5e741eaec60e983

Le service prend en charge la convolution lanczos et la réduction d'échelle par étapes. L'approche par convolution est de meilleure qualité au prix d'être plus lente, tandis que l'approche de réduction d'échelle par étapes produit des résultats raisonnablement anticrénelés et est nettement plus rapide.

Exemple d'utilisation:

angular.module('demo').controller('ExampleCtrl', function (imageService) {
  // EXAMPLE USAGE
  // NOTE: it's bad practice to access the DOM inside a controller, 
  // but this is just to show the example usage.

  // resize by lanczos-sinc filter
  imageService.resize($('#myimg')[0], 256, 256)
    .then(function (resizedImage) {
      // do something with resized image
    })

  // resize by stepping down image size in increments of 2x
  imageService.resizeStep($('#myimg')[0], 256, 256)
    .then(function (resizedImage) {
      // do something with resized image
    })
})
fisch2
la source
4

Il s'agit du filtre de redimensionnement Hermite amélioré qui utilise 1 travailleur pour que la fenêtre ne se fige pas.

https://github.com/calvintwr/blitz-hermite-resize

const blitz = Blitz.create()

/* Promise */
blitz({
    source: DOM Image/DOM Canvas/jQuery/DataURL/File,
    width: 400,
    height: 600
}).then(output => {
    // handle output
})catch(error => {
    // handle error
})

/* Await */
let resized = await blizt({...})

/* Old school callback */
const blitz = Blitz.create('callback')
blitz({...}, function(output) {
    // run your callback.
})
Calvintwr
la source
3

J'ai trouvé une solution qui n'a pas besoin d'accéder directement aux données de pixels et de les parcourir pour effectuer le sous-échantillonnage. Selon la taille de l'image, cela peut être très gourmand en ressources et il serait préférable d'utiliser les algorithmes internes du navigateur.

La fonction drawImage () utilise une méthode de rééchantillonnage du plus proche voisin par interpolation linéaire. Cela fonctionne bien lorsque vous ne redimensionnez pas plus de la moitié de la taille d'origine .

Si vous effectuez une boucle pour ne redimensionner que la moitié au maximum à la fois, les résultats seraient assez bons et beaucoup plus rapides que l'accès aux données de pixels.

Cette fonction sous-échantillonne de moitié à la fois jusqu'à atteindre la taille souhaitée:

  function resize_image( src, dst, type, quality ) {
     var tmp = new Image(),
         canvas, context, cW, cH;

     type = type || 'image/jpeg';
     quality = quality || 0.92;

     cW = src.naturalWidth;
     cH = src.naturalHeight;

     tmp.src = src.src;
     tmp.onload = function() {

        canvas = document.createElement( 'canvas' );

        cW /= 2;
        cH /= 2;

        if ( cW < src.width ) cW = src.width;
        if ( cH < src.height ) cH = src.height;

        canvas.width = cW;
        canvas.height = cH;
        context = canvas.getContext( '2d' );
        context.drawImage( tmp, 0, 0, cW, cH );

        dst.src = canvas.toDataURL( type, quality );

        if ( cW <= src.width || cH <= src.height )
           return;

        tmp.src = dst.src;
     }

  }
  // The images sent as parameters can be in the DOM or be image objects
  resize_image( $( '#original' )[0], $( '#smaller' )[0] );
Jesús Carrera
la source
Pourriez-vous s'il vous plaît poster un jsfiddle et quelques images résultantes?
confile
Dans le lien en bas, vous pouvez trouver les images résultantes en utilisant cette technique
Jesús Carrera
1

Peut-être que vous pouvez essayer ceci, que j'utilise toujours dans mon projet. De cette façon, vous pouvez non seulement obtenir une image de haute qualité, mais tout autre élément sur votre toile.

/* 
 * @parame canvas => canvas object
 * @parame rate => the pixel quality
 */
function setCanvasSize(canvas, rate) {
    const scaleRate = rate;
    canvas.width = window.innerWidth * scaleRate;
    canvas.height = window.innerHeight * scaleRate;
    canvas.style.width = window.innerWidth + 'px';
    canvas.style.height = window.innerHeight + 'px';
    canvas.getContext('2d').scale(scaleRate, scaleRate);
}
RandomYang
la source
0

au lieu de .85 , si nous ajoutons 1.0 . Vous obtiendrez une réponse exacte.

data=canvas.toDataURL('image/jpeg', 1.0);

Vous pouvez obtenir une image claire et lumineuse. Vérifiez s'il vous plaît

Phénix
la source
0

J'essaie vraiment d'éviter de parcourir les données d'image, en particulier sur des images plus grandes. Ainsi, j'ai trouvé un moyen assez simple de réduire décemment la taille de l'image sans aucune restriction ni limitation en utilisant quelques étapes supplémentaires. Cette routine descend au demi-pas le plus bas possible avant la taille cible souhaitée. Ensuite, il le met à l'échelle jusqu'à deux fois la taille cible, puis à nouveau de moitié. Cela semble drôle au début, mais les résultats sont incroyablement bons et y parviennent rapidement.

function resizeCanvas(canvas, newWidth, newHeight) {
  let ctx = canvas.getContext('2d');
  let buffer = document.createElement('canvas');
  buffer.width = ctx.canvas.width;
  buffer.height = ctx.canvas.height;
  let ctxBuf = buffer.getContext('2d');
  

  let scaleX = newWidth / ctx.canvas.width;
  let scaleY = newHeight / ctx.canvas.height;

  let scaler = Math.min(scaleX, scaleY);
  //see if target scale is less than half...
  if (scaler < 0.5) {
    //while loop in case target scale is less than quarter...
    while (scaler < 0.5) {
      ctxBuf.canvas.width = ctxBuf.canvas.width * 0.5;
      ctxBuf.canvas.height = ctxBuf.canvas.height * 0.5;
      ctxBuf.scale(0.5, 0.5);
      ctxBuf.drawImage(canvas, 0, 0);
      ctxBuf.setTransform(1, 0, 0, 1, 0, 0);
      ctx.canvas.width = ctxBuf.canvas.width;
      ctx.canvas.height = ctxBuf.canvas.height;
      ctx.drawImage(buffer, 0, 0);

      scaleX = newWidth / ctxBuf.canvas.width;
      scaleY = newHeight / ctxBuf.canvas.height;
      scaler = Math.min(scaleX, scaleY);
    }
    //only if the scaler is now larger than half, double target scale trick...
    if (scaler > 0.5) {
      scaleX *= 2.0;
      scaleY *= 2.0;
      ctxBuf.canvas.width = ctxBuf.canvas.width * scaleX;
      ctxBuf.canvas.height = ctxBuf.canvas.height * scaleY;
      ctxBuf.scale(scaleX, scaleY);
      ctxBuf.drawImage(canvas, 0, 0);
      ctxBuf.setTransform(1, 0, 0, 1, 0, 0);
      scaleX = 0.5;
      scaleY = 0.5;
    }
  } else
    ctxBuf.drawImage(canvas, 0, 0);

  //wrapping things up...
  ctx.canvas.width = newWidth;
  ctx.canvas.height = newHeight;
  ctx.scale(scaleX, scaleY);
  ctx.drawImage(buffer, 0, 0);
  ctx.setTransform(1, 0, 0, 1, 0, 0);
}
Timur Baysal
la source
-1

context.scale(xScale, yScale)

<canvas id="c"></canvas>
<hr/>
<img id="i" />

<script>
var i = document.getElementById('i');

i.onload = function(){
    var width = this.naturalWidth,
        height = this.naturalHeight,
        canvas = document.getElementById('c'),
        ctx = canvas.getContext('2d');

    canvas.width = Math.floor(width / 2);
    canvas.height = Math.floor(height / 2);

    ctx.scale(0.5, 0.5);
    ctx.drawImage(this, 0, 0);
    ctx.rect(0,0,500,500);
    ctx.stroke();

    // restore original 1x1 scale
    ctx.scale(2, 2);
    ctx.rect(0,0,500,500);
    ctx.stroke();
};

i.src = 'https://static.md/b70a511140758c63f07b618da5137b5d.png';
</script>
moule
la source
-1

DEMO : Redimensionner des images avec JS et HTML Canvas Demo Fiddler.

Vous pouvez trouver 3 méthodes différentes pour effectuer ce redimensionnement, qui vous aideront à comprendre comment le code fonctionne et pourquoi.

https://jsfiddle.net/1b68eLdr/93089/

Le code complet de la démo et de la méthode TypeScript que vous pouvez utiliser dans votre code se trouve dans le projet GitHub.

https://github.com/eyalc4/ts-image-resizer

Voici le code final:

export class ImageTools {
base64ResizedImage: string = null;

constructor() {
}

ResizeImage(base64image: string, width: number = 1080, height: number = 1080) {
    let img = new Image();
    img.src = base64image;

    img.onload = () => {

        // Check if the image require resize at all
        if(img.height <= height && img.width <= width) {
            this.base64ResizedImage = base64image;

            // TODO: Call method to do something with the resize image
        }
        else {
            // Make sure the width and height preserve the original aspect ratio and adjust if needed
            if(img.height > img.width) {
                width = Math.floor(height * (img.width / img.height));
            }
            else {
                height = Math.floor(width * (img.height / img.width));
            }

            let resizingCanvas: HTMLCanvasElement = document.createElement('canvas');
            let resizingCanvasContext = resizingCanvas.getContext("2d");

            // Start with original image size
            resizingCanvas.width = img.width;
            resizingCanvas.height = img.height;


            // Draw the original image on the (temp) resizing canvas
            resizingCanvasContext.drawImage(img, 0, 0, resizingCanvas.width, resizingCanvas.height);

            let curImageDimensions = {
                width: Math.floor(img.width),
                height: Math.floor(img.height)
            };

            let halfImageDimensions = {
                width: null,
                height: null
            };

            // Quickly reduce the size by 50% each time in few iterations until the size is less then
            // 2x time the target size - the motivation for it, is to reduce the aliasing that would have been
            // created with direct reduction of very big image to small image
            while (curImageDimensions.width * 0.5 > width) {
                // Reduce the resizing canvas by half and refresh the image
                halfImageDimensions.width = Math.floor(curImageDimensions.width * 0.5);
                halfImageDimensions.height = Math.floor(curImageDimensions.height * 0.5);

                resizingCanvasContext.drawImage(resizingCanvas, 0, 0, curImageDimensions.width, curImageDimensions.height,
                    0, 0, halfImageDimensions.width, halfImageDimensions.height);

                curImageDimensions.width = halfImageDimensions.width;
                curImageDimensions.height = halfImageDimensions.height;
            }

            // Now do final resize for the resizingCanvas to meet the dimension requirments
            // directly to the output canvas, that will output the final image
            let outputCanvas: HTMLCanvasElement = document.createElement('canvas');
            let outputCanvasContext = outputCanvas.getContext("2d");

            outputCanvas.width = width;
            outputCanvas.height = height;

            outputCanvasContext.drawImage(resizingCanvas, 0, 0, curImageDimensions.width, curImageDimensions.height,
                0, 0, width, height);

            // output the canvas pixels as an image. params: format, quality
            this.base64ResizedImage = outputCanvas.toDataURL('image/jpeg', 0.85);

            // TODO: Call method to do something with the resize image
        }
    };
}}
Eyal c
la source