HTML5 Canvas Resize (Downscale) Immagine di alta qualità?


149

Uso elementi canvas html5 per ridimensionare le immagini nel mio browser. Si scopre che la qualità è molto bassa. Ho trovato questo: disabilita l'interpolazione quando ridimensiona un <canvas> ma non aiuta ad aumentare la qualità.

Di seguito è riportato il mio codice CSS e JS, nonché l'immagine scaricata con Photoshop e ridimensionata nell'API di tela.

Cosa devo fare per ottenere una qualità ottimale durante il ridimensionamento di un'immagine nel browser?

Nota: desidero ridimensionare un'immagine grande in una piccola, modificare il colore in una tela e inviare il risultato dalla tela al server.

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'immagine ridimensionata con Photoshop:

inserisci qui la descrizione dell'immagine

L'immagine ridimensionata su tela:

inserisci qui la descrizione dell'immagine

Modificare:

Ho cercato di effettuare il downscaling in più di un passaggio, come proposto in:

Ridimensionamento di un'immagine in una tela HTML5 e disegno su tela HTML5 Immagine: come applicare l'antialias

Questa è la funzione che ho usato:

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);


}

Ecco il risultato se uso un ridimensionamento in due passaggi:

inserisci qui la descrizione dell'immagine

Ecco il risultato se uso un ridimensionamento in 3 passaggi:

inserisci qui la descrizione dell'immagine

Ecco il risultato se uso un ridimensionamento in 4 step:

inserisci qui la descrizione dell'immagine

Ecco il risultato se uso un ridimensionamento di 20 step:

inserisci qui la descrizione dell'immagine

Nota: si scopre che da 1 passaggio a 2 passaggi si ha un notevole miglioramento della qualità dell'immagine, ma più passaggi si aggiungono al processo, più l'immagine diventa sfocata.

C'è un modo per risolvere il problema che l'immagine diventa più sfocata più passaggi aggiungi?

Modifica 04-10-2013: ho provato l'algoritmo di GameAlchemist. Ecco il risultato rispetto a Photoshop.

Immagine di PhotoShop:

Immagine di PhotoShop

Algoritmo di GameAlchemist:

Algoritmo di GameAlchemist


2
Si potrebbe provare in modo incrementale il ridimensionamento dell'immagine: stackoverflow.com/questions/18761404/...
Marke

1
possibile duplicato del disegno su tela Html5 Immagine: come applicare l'antialias . Vedi se non funziona. Se le immagini sono grandi e ridotte a dimensioni ridotte, dovrai farlo in pochi passaggi (vedi immagini di esempio nel link)

2
@confile disattivando l'interpolazione la peggiorerà. Vuoi tenerlo abilitato. Guarda il link che ho fornito sopra. Vi mostro come usare i passaggi per ridimensionare le immagini più grandi e mantenere la qualità. E come dice Scott, vuoi dare priorità alla qualità piuttosto che alla velocità.

1
@ Ken-AbdiasSoftware Ti ho provato ad avvicinarti, ma il problema è che peggiorerà il numero di round che utilizzo per il ridimensionamento graduale. Qualche idea su come risolverlo?
Confile

3
Sicuramente le possibilità di replicare la funzionalità di un costoso software di fotoritocco professionale utilizzando HTML5 sono piuttosto scarse? Probabilmente puoi avvicinarti (ish), ma esattamente come funziona in Photoshop immagino che sarebbe impossibile!
Liam,

Risposte:


171

Dal momento che il tuo problema è di ridimensionare l'immagine, non ha senso parlare di interpolazione - che riguarda la creazione di pixel -. Il problema qui è il downsampling.

Per sottocampionare un'immagine, dobbiamo trasformare ogni quadrato di pixel p * p nell'immagine originale in un singolo pixel nell'immagine di destinazione.

Per motivi di prestazioni, i browser effettuano un downsampling molto semplice: per creare un'immagine più piccola, selezioneranno UN SOLO pixel nella sorgente e ne utilizzeranno il valore per la destinazione. che "dimentica" alcuni dettagli e aggiunge rumore.

Tuttavia, c'è un'eccezione: dal momento che il downsampling dell'immagine 2X è molto semplice da calcolare (in media 4 pixel per crearne uno) e viene utilizzato per i pixel retina / HiDPI, questo caso viene gestito correttamente -Il browser utilizza 4 pixel per fare uno-.

MA ... se usi più volte un downsampling 2X, dovrai affrontare il problema che i successivi errori di arrotondamento aggiungeranno troppo rumore.
Quel che è peggio, non ridimensionerai sempre con una potenza di due e ridimensionare alla potenza più vicina + un ultimo ridimensionamento è molto rumoroso.

Ciò che cerchi è un downsampling perfetto per i pixel, ovvero: un ricampionamento dell'immagine che terrà conto di tutti i pixel di input, qualunque sia la scala.
Per fare ciò dobbiamo calcolare, per ogni pixel di input, il suo contributo a uno, due o quattro pixel di destinazione a seconda che la proiezione in scala dei pixel di input sia proprio all'interno di un pixel di destinazione, si sovrappone a un bordo X, un bordo Y o entrambi .
(Uno schema sarebbe bello qui, ma non ne ho uno.)

Ecco un esempio di scala della tela rispetto alla scala del mio pixel perfetta su una scala 1/3 di uno zombat.

Nota che l'immagine potrebbe ridimensionarsi nel tuo browser ed è .jpegizzata da SO.
Tuttavia vediamo che c'è molto meno rumore soprattutto nell'erba dietro il vombato e i rami alla sua destra. Il rumore nella pelliccia lo rende più contrastato, ma sembra che abbia i capelli bianchi - a differenza della fonte dell'immagine -.
L'immagine giusta è meno accattivante ma decisamente più bella.

inserisci qui la descrizione dell'immagine

Ecco il codice per eseguire il downscaling perfetto dei pixel:

risultato violino: http://jsfiddle.net/gamealchemist/r6aVp/embedded/result/
fiddle stesso: 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;
}

È piuttosto avido di memoria, poiché è necessario un buffer float per memorizzare i valori intermedi dell'immagine di destinazione (-> se contiamo il canvas dei risultati, utilizziamo 6 volte la memoria dell'immagine di origine in questo algoritmo).
È anche piuttosto costoso, poiché ogni pixel di origine viene utilizzato indipendentemente dalle dimensioni della destinazione e dobbiamo pagare per getImageData / putImageDate, anche piuttosto lento.
Ma in questo caso non c'è modo di essere più veloce dell'elaborazione di ciascun valore sorgente e la situazione non è poi così grave: per la mia immagine 740 * 556 di un wombat, l'elaborazione richiede tra 30 e 40 ms.


Potrebbe essere più veloce se ridimensioni l'immagine prima di metterla nella tela?
Confile

non capisco ... sembra che sia quello che faccio. Il buffer e la tela che creo (resCV) hanno le dimensioni dell'immagine ridimensionata. Penso che l'unico modo per ottenerlo più veloce sarebbe usare il calcolo intero simile a breshensam. Ma 40 ms è lento solo per un videogioco (25 fps), non per un'applicazione di disegno.
GameAlchemist,

vedi qualche possibilità di rendere più veloce il tuo algoritmo mantenendo la qualità?
Confile

1
ho provato a arrotondare il buffer (ultima parte dell'algoritmo) usando 0 | invece di Mat.ceil. È un po 'più veloce Ma comunque c'è un po 'di sovraccarico con get / putImageData e, di nuovo, non possiamo evitare di elaborare ogni pixel.
GameAlchemist,

4
Ok, quindi ho visto il codice: eri molto vicino alla soluzione. Due errori: i tuoi indici sono stati disattivati ​​di uno per tX + 1 (erano + 3, + 4, + 5, + 6 invece di +4, +5, +6, +7) e cambiare riga in rgba è un mul di 4, non 3. Ho appena testato 4 valori casuali per verificare (0.1, 0.15, 0.33, 0.8) sembrava ok. il tuo violino aggiornato è qui: jsfiddle.net/gamealchemist/kpQyE/3
GameAlchemist

51

Ricampionamento rapido della tela con buona qualità: http://jsfiddle.net/9g9Nv/442/

Aggiornamento: versione 2.0 (più veloce, web worker + oggetti trasferibili) - 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);
}

Ho bisogno della migliore qualità
confile

18
risolto, ho cambiato da "buono" a "migliore", va bene adesso? : D. D'altra parte, se si desidera ricampionare al meglio, utilizzare imagemagick.
ViliusL

@confile imgur.com era sicuro da usare in jsfiddle, ma gli amministratori hanno fatto qualcosa di sbagliato? Non vedi una buona qualità, perché il tuo browser dà un errore fatale CORS. (non è possibile utilizzare l'immagine da siti remoti)
ViliusL

ok puoi usare qualsiasi altra immagine PNG con aree trasparenti. Qualche idea su questo?
confile

4
@confile avevi ragione, in alcuni casi le immagini trasparenti avevano problemi in aree nitide. Ho perso questi casi con il mio test. Risolto il problema con il
ViliusL

28

Suggerimento 1: estendere la linea di processo

Puoi usare il step-down come descrivo nei link a cui ti riferisci ma sembra che li usi in modo sbagliato.

Non è necessario il passaggio verso il basso per ridimensionare le immagini in rapporto superiore a 1: 2 (in genere, ma non limitato a). È dove devi eseguire un drastico ridimensionamento che devi dividere in due (e raramente, più) passaggi a seconda del contenuto dell'immagine (in particolare dove si verificano alte frequenze come le linee sottili).

Ogni volta che esegui il down-campionamento di un'immagine perderai dettagli e informazioni. Non puoi aspettarti che l'immagine risultante sia chiara come l'originale.

Se poi ridimensionate le immagini in molti passaggi, perderete molte informazioni in totale e il risultato sarà scarso come già notato.

Prova con un solo passo in più, o al massimo due.

circonvoluzioni

In caso di Photoshop, si noti che applica una convoluzione dopo il ricampionamento dell'immagine, come ad esempio la nitidezza. Non è solo l'interpolazione bi-cubica che ha luogo, quindi per emulare completamente Photoshop dobbiamo anche aggiungere i passaggi che Photoshop sta eseguendo (con la configurazione predefinita).

Per questo esempio userò la mia risposta originale a cui fai riferimento nel tuo post, ma ho aggiunto una netta contusione ad esso per migliorare la qualità come processo post (vedi la demo in fondo).

Ecco il codice per aggiungere il filtro di nitidezza (si basa su un filtro di convoluzione generico - ho inserito la matrice di peso per la nitidezza al suo interno e un fattore di miscelazione per regolare la pronuncia dell'effetto):

Uso:

sharpen(context, width, height, mixFactor);

Il mixFactorvalore è compreso tra [0,0, 1,0] e consente di minimizzare l'effetto nitidezza - regola empirica: minore è la dimensione, minore è l'effetto necessario.

Funzione (basata su questo frammento ):

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);
}

Il risultato dell'utilizzo di questa combinazione sarà:

DEMO ONLINE QUI

Il risultato riduce e acuisce la convoluzione

A seconda della quantità di nitidezza che si desidera aggiungere alla fusione, è possibile ottenere il risultato da "sfocato" predefinito a molto nitido:

Variazioni di nitidezza

Suggerimento 2 - implementazione dell'algoritmo di basso livello

Se vuoi ottenere il miglior risultato dal punto di vista della qualità, devi passare a un livello basso e considerare di implementare, ad esempio, questo nuovissimo algoritmo per farlo.

Vedi Downsampling immagine dipendente dall'interpolazione (2011) di IEEE.
Ecco un link al documento completo (PDF) .

Non ci sono implementazioni di questo algoritmo in JavaScript AFAIK in questo momento, quindi sei pronto per una mano piena se vuoi lanciarti in questo compito.

L'essenza è (estratti dall'articolo):

Astratto

In questo documento viene proposto un algoritmo di down-sampling adattivo orientato all'interpolazione per la codifica di immagini a bassa velocità di bit. Data un'immagine, l'algoritmo proposto è in grado di ottenere un'immagine a bassa risoluzione, da cui è possibile interpolare un'immagine di alta qualità con la stessa risoluzione dell'immagine di input. Diversamente dai tradizionali algoritmi di down-sampling, che sono indipendenti dal processo di interpolazione, l'algoritmo di down-sampling proposto incardina il down-sampling al processo di interpolazione. Di conseguenza, l'algoritmo di down-sampling proposto è in grado di mantenere le informazioni originali dell'immagine di input nella massima misura. L'immagine sotto campionata viene quindi inserita in JPEG. Una post elaborazione basata su una variazione totale (TV) viene quindi applicata all'immagine decompressa a bassa risoluzione. In definitiva,I risultati sperimentali confermano che utilizzando l'immagine sottocampionata dall'algoritmo proposto, è possibile ottenere un'immagine interpolata con una qualità molto più elevata. Inoltre, l'algoritmo proposto è in grado di ottenere prestazioni superiori rispetto a JPEG per la codifica delle immagini a bassa velocità di trasmissione.

Istantanea dalla carta

(vedi link fornito per tutti i dettagli, le formule ecc.)


Anche questa è un'ottima soluzione. Grazie!
Confile il

Questa è un'ottima soluzione L'ho provato su file png con aree trasparenti. Ecco il risultato: jsfiddle.net/confile/5CD4N Hai idea di cosa fare per farlo funzionare?
Confile il

1
questo è GENIUS! ma per favore puoi spiegare cosa stai facendo esattamente? lol .. voglio assolutamente conoscere i dettagli ... forse risorse da imparare?
carinlynchin,

1
@Carine che può essere un po 'troppo per un campo di commento scadente :) ma, ridimensionando, ricampiona un gruppo di pixel per fare una media di uno nuovo che rappresenta quel gruppo. Questo è in effetti un filtro passa-basso che introduce un po 'di sfocatura complessiva. Per compensare la perdita di nitidezza è sufficiente applicare una convoluzione di nitidezza. Poiché la nitidezza può essere molto pronunciata, possiamo invece mescolarla con l'immagine in modo da poter controllare il livello di nitidezza. Spero che ci dia qualche idea.

21

Se si desidera utilizzare solo la tela, il risultato migliore sarà con più passaggi verso il basso. Ma non è ancora una buona enougth. Per una migliore qualità è necessaria un'implementazione js pura. Abbiamo appena rilasciato pica : downscaler ad alta velocità con qualità / velocità variabili. In breve, ridimensiona 1280 * 1024 px in ~ 0,1 secondi e 5000 * 3000 px in 1 secondo, con la massima qualità (filtro lanczos a 3 lobi). Pica ha una demo , dove puoi giocare con le tue immagini, i livelli di qualità e persino provarlo su dispositivi mobili.

Pica non ha ancora la maschera di contrasto, ma verrà aggiunta molto presto. È molto più semplice dell'implementazione del filtro di convoluzione ad alta velocità per il ridimensionamento.


16

Perché usare la tela per ridimensionare le immagini? I browser moderni usano tutti l'interpolazione bicubica - lo stesso processo usato da Photoshop (se lo stai facendo bene) - e lo fanno più velocemente del processo canvas. Basta specificare la dimensione dell'immagine desiderata (utilizzare solo una dimensione, altezza o larghezza, per ridimensionare proporzionalmente).

Questo è supportato dalla maggior parte dei browser, comprese le versioni successive di IE. Le versioni precedenti potrebbero richiedere CSS specifici del browser .

Una semplice funzione (usando jQuery) per ridimensionare un'immagine sarebbe così:

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

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

Quindi basta usare il valore restituito per ridimensionare l'immagine in una o entrambe le dimensioni.

Ovviamente ci sono diversi perfezionamenti che potresti fare, ma questo fa il lavoro.

Incolla il seguente codice nella console di questa pagina e guarda cosa succede ai gravatari:

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";
});

2
Si noti inoltre che se si specifica solo una dimensione, il browser (moderno) manterrà automaticamente le proporzioni naturali dell'immagine.
André Dion,

38
Forse ha bisogno di inviare l'immagine ridimensionata a un server.
Sergiu Paraschiv,

2
@Sergiu: Non necessario, ma nota che se passi da un'immagine molto piccola a un'immagine molto grande non otterrai grandi risultati nemmeno da un server.
Robusto,

2
@Robusto Ho bisogno di mettere l'immagine sulla tela in seguito e inviarla al server in seguito. Voglio ridimensionare un'immagine grande a una piccola, modificare il colore in una tela e inviare il risultato al server. Cosa pensi che dovrei fare?
Confile

9
@Robusto Questo è il problema. Mostrare una piccola immagine sul client è facile. img.width nad img.height è così banale. Voglio ridimensionarlo solo una volta e non di nuovo sul server.
Confile

8

Non è la risposta giusta per le persone che hanno davvero bisogno di ridimensionare l'immagine stessa, ma solo per ridurre le dimensioni del file .

Ho avuto un problema con le immagini "direttamente dalla fotocamera", che i miei clienti caricavano spesso in JPEG "non compressi".

Non è così noto che la tela supporta (nella maggior parte dei browser 2017) la modifica della qualità di JPEG

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

Con questo trucco ho potuto ridurre le foto 4k x 3k con> 10 Mb a 1 o 2 Mb, certo che dipende dalle tue esigenze.

guarda qui


4

Ecco un servizio angolare riutilizzabile per il ridimensionamento dell'immagine / tela di alta qualità: https://gist.github.com/fisch0920/37bac5e741eaec60e983

Il servizio supporta la convoluzione di Lanczos e il downscaling graduale. L'approccio di convoluzione è di qualità superiore a costo di essere più lento, mentre l'approccio di downscaling graduale produce risultati ragionevolmente antializzati ed è significativamente più veloce.

Esempio di utilizzo:

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
    })
})

4

Questo è il filtro ridimensionamento Hermite migliorato che utilizza 1 lavoratore in modo che la finestra non si blocchi.

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.
})

3

Ho trovato una soluzione che non ha bisogno di accedere direttamente ai dati dei pixel e di eseguirne il ciclo per eseguire il downsampling. A seconda della dimensione dell'immagine, ciò può richiedere molte risorse e sarebbe meglio usare gli algoritmi interni del browser.

La funzione drawImage () sta usando un metodo di ricampionamento a interpolazione lineare, il più vicino vicino. Questo funziona bene quando non si sta ridimensionando in calo di oltre la metà delle dimensioni originali .

Se esegui il loop per ridimensionare solo la metà alla volta, i risultati sarebbero abbastanza buoni e molto più veloci dell'accesso ai dati pixel.

Questa funzione riduce a metà alla volta fino a raggiungere la dimensione desiderata:

  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] );

Potresti per favore pubblicare un jsfiddle e alcune immagini risultanti?
Confile

Nel link in basso puoi trovare le immagini risultanti usando questa tecnica
Jesús Carrera,

1

Forse amico, puoi provare questo, che uso sempre nel mio progetto, in questo modo non solo puoi ottenere immagini di alta qualità, ma qualsiasi altro elemento sulla tua tela.

/* 
 * @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);
}

0

anziché .85 , se aggiungiamo 1.0 . Riceverai una risposta esatta.

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

Puoi ottenere un'immagine chiara e luminosa. si prega di controllare


0

Cerco davvero di evitare di scorrere i dati delle immagini, specialmente su immagini più grandi. Così ho trovato un modo piuttosto semplice per ridurre decentemente la dimensione dell'immagine senza alcuna restrizione o limitazione usando alcuni passaggi extra. Questa routine scende al mezzo passo più basso possibile prima della dimensione target desiderata. Quindi lo ridimensiona fino a due volte la dimensione target e quindi di nuovo la metà. All'inizio sembra divertente, ma i risultati sono sorprendentemente buoni e ci vanno rapidamente.

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);
}

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

-1

DEMO : ridimensionamento delle immagini con Fiddler Demo Canvas JS e HTML.

Potresti trovare 3 diversi metodi per eseguire questo ridimensionamento, che ti aiuteranno a capire come funziona il codice e perché.

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

Il codice completo sia della demo, sia del metodo TypeScript che potresti voler usare nel tuo codice, è disponibile nel progetto GitHub.

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

Questo è il codice finale:

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
        }
    };
}}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.