HTML5 Canvas Resize (Downscale) Bild Hohe Qualität?

149

Ich verwende HTML5-Canvas-Elemente, um die Größe von Bildern in meinem Browser zu ändern. Es stellt sich heraus, dass die Qualität sehr gering ist. Ich habe Folgendes gefunden: Deaktivieren Sie die Interpolation beim Skalieren eines <canvas>, aber es hilft nicht, die Qualität zu verbessern.

Unten finden Sie meinen CSS- und JS-Code sowie das mit Photoshop überarbeitete und in der Canvas-API skalierte Bild.

Was muss ich tun, um beim Skalieren eines Bildes im Browser eine optimale Qualität zu erzielen?

Hinweis: Ich möchte ein großes Bild auf ein kleines verkleinern, die Farbe in einer Zeichenfläche ändern und das Ergebnis von der Zeichenfläche an den Server senden.

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

Die Bildgröße wurde mit Photoshop geändert:

Geben Sie hier die Bildbeschreibung ein

Die Bildgröße wurde auf Leinwand geändert:

Geben Sie hier die Bildbeschreibung ein

Bearbeiten:

Ich habe versucht, das Downscaling in mehr als einem Schritt durchzuführen, wie in:

Ändern der Größe eines Bildes in einer HTML5- Zeichenfläche und einer HTML5- Zeichenfläche drawImage: Anwenden von Antialiasing

Dies ist die Funktion, die ich verwendet habe:

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


}

Hier ist das Ergebnis, wenn ich eine 2-Stufen-Größenänderung verwende:

Geben Sie hier die Bildbeschreibung ein

Hier ist das Ergebnis, wenn ich eine 3-Stufen-Größenänderung verwende:

Geben Sie hier die Bildbeschreibung ein

Hier ist das Ergebnis, wenn ich eine 4-Stufen-Größenänderung verwende:

Geben Sie hier die Bildbeschreibung ein

Hier ist das Ergebnis, wenn ich eine Größenänderung von 20 Schritten verwende:

Geben Sie hier die Bildbeschreibung ein

Hinweis: Es stellt sich heraus, dass sich die Bildqualität von 1 Schritt auf 2 Schritte erheblich verbessert. Je mehr Schritte Sie dem Prozess hinzufügen, desto unschärfer wird das Bild.

Gibt es eine Möglichkeit, das Problem zu lösen, dass das Bild umso unschärfer wird, je mehr Schritte Sie hinzufügen?

Edit 2013-10-04: Ich habe den Algorithmus von GameAlchemist ausprobiert. Hier ist das Ergebnis im Vergleich zu Photoshop.

PhotoShop Bild:

PhotoShop-Bild

GameAlchemist-Algorithmus:

GameAlchemist-Algorithmus

konfile
quelle
2
Sie könnten versuchen, Ihr Bild schrittweise zu skalieren: stackoverflow.com/questions/18761404/…
markE
1
Mögliches Duplikat von HTML5-Zeichenfläche drawImage: Anwenden von Antialiasing . Sehen Sie, ob das nicht funktioniert. Wenn Bilder groß und klein sind, müssen Sie dies in Schritten tun (siehe Beispielbilder im Link)
2
@confile Wenn Sie die Interpolation deaktivieren, wird dies am schlimmsten. Sie möchten dies aktiviert lassen. Schauen Sie sich den Link an, den ich oben angegeben habe. Ich zeige dort, wie man mit Schritten größere Bilder verkleinert und die Qualität beibehält. Und wie Scott sagt, möchten Sie Qualität vor Geschwindigkeit priorisieren.
1
@ Ken-AbdiasSoftware Ich habe versucht, dich anzusprechen, aber das Problem ist, dass es umso schlimmer wird, je mehr Runden ich für die schrittweise Skalierung verwende. Irgendeine Idee, wie man das behebt?
Confile
3
Sicherlich sind die Chancen, die Funktionalität einer teuren professionellen Fotobearbeitungssoftware mit HTML5 zu replizieren, ziemlich gering? Sie können sich wahrscheinlich nähern (ish), aber genau so, wie es in Photoshop funktioniert, würde ich mir vorstellen, dass es unmöglich wäre!
Liam

Antworten:

171

Da Ihr Problem darin besteht, Ihr Bild zu verkleinern, macht es keinen Sinn, über Interpolation zu sprechen, bei der es um das Erstellen von Pixeln geht. Das Problem hier ist das Downsampling.

Um ein Bild herunterzusampeln, müssen wir jedes Quadrat von p * p-Pixeln im Originalbild in ein einzelnes Pixel im Zielbild verwandeln.

Aus Leistungsgründen führen Browser ein sehr einfaches Downsampling durch: Um das kleinere Bild zu erstellen, wählen sie nur EIN Pixel in der Quelle aus und verwenden dessen Wert für das Ziel. Das "vergisst" einige Details und fügt Rauschen hinzu.

Es gibt jedoch eine Ausnahme: Da das 2X-Bild-Downsampling sehr einfach zu berechnen ist (durchschnittlich 4 Pixel, um eines zu erstellen) und für Retina- / HiDPI-Pixel verwendet wird, wird dieser Fall ordnungsgemäß behandelt. Der Browser verwendet 4 Pixel für die Erstellung einer-.

ABER ... wenn Sie mehrmals ein 2X-Downsampling verwenden, werden Sie mit dem Problem konfrontiert, dass die aufeinanderfolgenden Rundungsfehler zu viel Rauschen verursachen.
Was noch schlimmer ist, Sie werden die Größe nicht immer um eine Zweierpotenz ändern, und die Größenänderung auf die nächste Potenz + eine letzte Größenänderung ist sehr laut.

Was Sie suchen, ist ein pixelgenaues Downsampling, dh ein erneutes Abtasten des Bildes, bei dem alle Eingabepixel berücksichtigt werden - unabhängig von der Skalierung.
Dazu müssen wir für jedes Eingabepixel seinen Beitrag zu einem, zwei oder vier Zielpixeln berechnen, je nachdem, ob sich die skalierte Projektion der Eingabepixel direkt innerhalb eines Zielpixels befindet, einen X-Rand, einen Y-Rand oder beides überlappt .
(Ein Schema wäre hier schön, aber ich habe keines.)

Hier ist ein Beispiel für die Leinwandskala im Vergleich zu meiner pixelgenauen Skala im Maßstab 1/3 eines Zombats.

Beachten Sie, dass das Bild möglicherweise in Ihrem Browser skaliert wird und von SO .jpegisiert wird.
Wir sehen jedoch, dass es besonders im Gras hinter dem Wombat und in den Zweigen rechts viel weniger Lärm gibt. Das Geräusch im Fell macht es kontrastreicher, aber es sieht so aus, als hätte er weiße Haare - im Gegensatz zum Quellbild -.
Das rechte Bild ist weniger eingängig, aber definitiv schöner.

Geben Sie hier die Bildbeschreibung ein

Hier ist der Code für das pixelgenaue Downscaling:

Geigenergebnis: http://jsfiddle.net/gamealchemist/r6aVp/embedded/result/
Geige selbst: 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;
}

Es ist ziemlich speichergierig, da ein Float-Puffer erforderlich ist, um die Zwischenwerte des Zielbilds zu speichern (-> wenn wir den Ergebnisbereich zählen, verwenden wir in diesem Algorithmus das 6-fache des Speichers des Quellbilds).
Es ist auch ziemlich teuer, da jedes Quellpixel unabhängig von der Zielgröße verwendet wird und wir für getImageData / putImageDate bezahlen müssen, auch ziemlich langsam.
In diesem Fall gibt es jedoch keine Möglichkeit, schneller zu sein, als jeden Quellwert zu verarbeiten, und die Situation ist nicht so schlimm: Für mein 740 * 556-Bild eines Wombats dauert die Verarbeitung zwischen 30 und 40 ms.

GameAlchemist
quelle
Könnte es schneller sein, wenn Sie das Bild skalieren, bevor Sie es in die Leinwand legen?
Confile
Ich verstehe es nicht ... es scheint, dass es das ist, was ich tue. Der Puffer sowie die von mir erstellte Zeichenfläche (resCV) haben die Größe des skalierten Bildes. Ich denke, der einzige Weg, es schneller zu machen, wäre die Verwendung einer breshensam-ähnlichen Ganzzahlberechnung. 40 ms sind jedoch nur für ein Videospiel (25 fps) langsam, nicht für eine Zeichenanwendung.
GameAlchemist
Sehen Sie eine Chance, Ihren Algorithmus schneller zu machen und gleichzeitig die Qualität zu erhalten?
Confile
1
Ich habe versucht, den Puffer (letzter Teil des Algorithmus) mit 0 | zu runden anstelle von Mat.ceil. Es ist etwas schneller. Trotzdem ist der Aufwand für get / putImageData ziemlich hoch, und wir können es auch hier nicht vermeiden, jedes Pixel zu verarbeiten.
GameAlchemist
4
Ok, also habe ich mir den Code angesehen: Sie waren der Lösung sehr nahe. Zwei Fehler: Ihre Indizes waren für tX + 1 um eins verschoben (sie waren + 3, + 4, + 5, + 6 anstelle von +4, +5, +6, +7), und das Ändern der Linie in rgba ist ein Mul Ich habe gerade 4 Zufallswerte getestet, um zu überprüfen (0,1, 0,15, 0,33, 0,8), dass es in Ordnung zu sein schien. Ihre aktualisierte Geige ist hier: jsfiddle.net/gamealchemist/kpQyE/3
GameAlchemist
51

Schnelles Canvas-Resample mit guter Qualität: http://jsfiddle.net/9g9Nv/442/

Update: Version 2.0 (schneller, Web Worker + übertragbare Objekte) - 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
quelle
Ich brauche die beste Qualität
Confile
18
behoben, ich habe "gut" in "am besten" geändert, ist das jetzt in Ordnung? : D. Wenn Sie jedoch das bestmögliche Resample wünschen, verwenden Sie imagemagick.
ViliusL
@confile imgur.com war sicher in jsfiddle zu verwenden, aber Admins haben etwas falsch gemacht? Sie sehen keine gute Qualität, da Ihr Browser einen schwerwiegenden CORS-Fehler ausgibt. (kann kein Bild von entfernten Standorten verwenden)
ViliusL
Okay, Sie können jedes andere PNG-Bild mit transparenten Bereichen verwenden. Irgendeine Idee dazu?
Confile
4
@confile Sie hatten Recht, in einigen Fällen hatten transparente Bilder Probleme in scharfen Bereichen. Ich habe diese Fälle mit meinem Test verpasst. Feste Größenänderung auch Remote-Image-Unterstützung für Geige behoben
ViliusL
28

Vorschlag 1 - Verlängerung der Prozessleitung

Sie können Step-Down verwenden, wie ich in den Links beschreibe, auf die Sie verweisen, aber Sie scheinen sie falsch zu verwenden.

Ein Schritt nach unten ist nicht erforderlich, um Bilder auf Verhältnisse über 1: 2 zu skalieren (normalerweise, aber nicht beschränkt auf). Hier müssen Sie eine drastische Verkleinerung vornehmen, die Sie je nach Bildinhalt in zwei (und selten mehr) Schritte aufteilen müssen (insbesondere, wenn hohe Frequenzen wie dünne Linien auftreten).

Jedes Mal, wenn Sie ein Bild herunterproben, verlieren Sie Details und Informationen. Sie können nicht erwarten, dass das resultierende Bild so klar wie das Original ist.

Wenn Sie dann die Bilder in vielen Schritten verkleinern, verlieren Sie insgesamt viele Informationen und das Ergebnis ist schlecht, wie Sie bereits bemerkt haben.

Versuchen Sie es mit nur einem zusätzlichen Schritt oder oben mit zwei.

Faltungen

Im Falle von Photoshop beachten Sie, dass nach dem erneuten Abtasten des Bildes eine Faltung angewendet wird, z. B. Schärfen. Es findet nicht nur eine bi-kubische Interpolation statt. Um Photoshop vollständig zu emulieren, müssen wir auch die Schritte hinzufügen, die Photoshop ausführt (mit der Standardeinstellung).

In diesem Beispiel verwende ich meine ursprüngliche Antwort, auf die Sie in Ihrem Beitrag verweisen. Ich habe jedoch eine schärfere Faltung hinzugefügt, um die Qualität als Nachbearbeitungsprozess zu verbessern (siehe Demo unten).

Hier ist der Code zum Hinzufügen eines Schärfefilters (er basiert auf einem generischen Faltungsfilter - ich habe die Gewichtsmatrix zum Schärfen sowie einen Mischungsfaktor zum Anpassen der Aussprache des Effekts eingefügt):

Verwendung:

sharpen(context, width, height, mixFactor);

Der mixFactorWert liegt zwischen [0.0, 1.0] und ermöglicht es Ihnen, den Schärfeeffekt herunterzuspielen - Faustregel: Je weniger Größe, desto weniger Effekt wird benötigt.

Funktion (basierend auf diesem Snippet ):

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

Das Ergebnis der Verwendung dieser Kombination ist:

ONLINE DEMO HIER

Ergebnis Downsample und schärfen die Faltung

Abhängig davon, wie viel des Schärfens Sie der Mischung hinzufügen möchten, können Sie das Ergebnis von Standard "verschwommen" bis sehr scharf erhalten:

Variationen des Schärfens

Vorschlag 2 - Implementierung eines Algorithmus auf niedriger Ebene

Wenn Sie qualitativ das beste Ergebnis erzielen möchten, müssen Sie auf niedriger Ebene arbeiten und beispielsweise diesen brandneuen Algorithmus implementieren, um dies zu tun.

Siehe Interpolationsabhängiges Bild-Downsampling (2011) von IEEE.
Hier ist ein Link zum vollständigen Artikel (PDF) .

Derzeit gibt es keine Implementierungen dieses Algorithmus in JavaScript AFAIK. Sie müssen also eine Hand voll haben, wenn Sie sich dieser Aufgabe stellen möchten.

Das Wesentliche ist (Auszüge aus dem Papier):

Abstrakt

In diesem Artikel wird ein interpolationsorientierter adaptiver Downsampling-Algorithmus für die Bildcodierung mit niedriger Bitrate vorgeschlagen. Bei einem gegebenen Bild kann der vorgeschlagene Algorithmus ein Bild mit niedriger Auflösung erhalten, aus dem ein Bild hoher Qualität mit derselben Auflösung wie das Eingabebild interpoliert werden kann. Anders als bei den herkömmlichen Down-Sampling-Algorithmen, die vom Interpolationsprozess unabhängig sind, hängt der vorgeschlagene Down-Sampling-Algorithmus das Down-Sampling vom Interpolationsprozess ab. Folglich kann der vorgeschlagene Downsampling-Algorithmus die Originalinformationen des Eingabebildes weitgehend beibehalten. Das heruntergetastete Bild wird dann in JPEG eingespeist. Eine auf Total Variation (TV) basierende Nachbearbeitung wird dann auf das dekomprimierte Bild mit niedriger Auflösung angewendet. Letzten Endes,Experimentelle Ergebnisse bestätigen, dass unter Verwendung des heruntergetasteten Bildes durch den vorgeschlagenen Algorithmus ein interpoliertes Bild mit viel höherer Qualität erzielt werden kann. Außerdem kann der vorgeschlagene Algorithmus eine überlegene Leistung als JPEG für die Bildcodierung mit niedriger Bitrate erzielen.

Schnappschuss vom Papier

(Alle Details, Formeln usw. finden Sie unter dem angegebenen Link.)

Gemeinschaft
quelle
Dies ist auch eine großartige Lösung. Danke dir!
Confile
Dies ist eine großartige Lösung. Ich habe es mit PNG-Dateien mit transparenten Bereichen versucht. Hier ist das Ergebnis: jsfiddle.net/confile/5CD4N Haben Sie eine Idee, was zu tun ist, damit es funktioniert?
Confile
1
das ist GENIE! Aber können Sie bitte erklären, was genau Sie tun? lol .. ich möchte unbedingt die Vor- und Nachteile kennen ... vielleicht Ressourcen zum Lernen?
Carinlynchin
1
@Carine, das kann ein bisschen viel für ein schlechtes Kommentarfeld sein :), aber durch Verkleinern wird eine Gruppe von Pixeln neu abgetastet, um eine neue zu mitteln, die diese Gruppe darstellt. Dies ist in der Tat ein Tiefpassfilter, der insgesamt eine gewisse Unschärfe hervorruft. Um den Schärfeverlust auszugleichen, wenden Sie einfach eine Schärfungsfaltung an. Da das Schärfen sehr ausgeprägt sein kann, können wir es stattdessen mit dem Bild mischen, um den Grad des Schärfens zu steuern. Hoffe das gibt einen Einblick.
21

Wenn Sie nur Leinwand verwenden möchten, erzielen Sie das beste Ergebnis mit mehreren Abwärtsschritten. Aber das ist noch nicht gut genug. Für eine bessere Qualität benötigen Sie eine reine js-Implementierung. Wir haben gerade pica - Hochgeschwindigkeits-Downscaler mit variabler Qualität / Geschwindigkeit veröffentlicht. Kurz gesagt, es ändert die Größe von 1280 * 1024px in ~ 0,1s und 5000 * 3000px in 1s mit höchster Qualität (Lanczos-Filter mit 3 Lappen). Pica hat eine Demo , in der Sie mit Ihren Bildern und Qualitätsstufen spielen und sie sogar auf Mobilgeräten ausprobieren können.

Pica hat noch keine unscharfe Maske, aber die wird sehr bald hinzugefügt. Das ist viel einfacher als die Implementierung eines Hochgeschwindigkeits-Faltungsfilters zur Größenänderung.

Vitaly
quelle
16

Warum die Leinwand verwenden, um die Größe von Bildern zu ändern? Moderne Browser verwenden alle die bikubische Interpolation - den gleichen Prozess wie Photoshop (wenn Sie es richtig machen) - und das schneller als der Canvas-Prozess. Geben Sie einfach die gewünschte Bildgröße an (verwenden Sie nur eine Dimension, Höhe oder Breite, um die Größe proportional zu ändern).

Dies wird von den meisten Browsern unterstützt, einschließlich späterer Versionen des IE. Frühere Versionen erfordern möglicherweise browserspezifisches CSS .

Eine einfache Funktion (mit jQuery) zum Ändern der Bildgröße lautet wie folgt:

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

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

Verwenden Sie dann einfach den zurückgegebenen Wert, um die Größe des Bilds in einer oder beiden Dimensionen zu ändern.

Natürlich gibt es verschiedene Verfeinerungen, die Sie vornehmen könnten, aber dies erledigt den Job.

Fügen Sie den folgenden Code in die Konsole dieser Seite ein und beobachten Sie, was mit den Gravataren passiert:

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
quelle
2
Beachten Sie außerdem, dass der (moderne) Browser automatisch das natürliche Seitenverhältnis des Bildes beibehält, wenn Sie nur eine Dimension angeben.
André Dion
38
Möglicherweise muss er das Bild in der Größe an einen Server senden.
Sergiu Paraschiv
2
@Sergiu: Nicht erforderlich, aber beachten Sie, dass Sie selbst von einem Server keine großartigen Ergebnisse erzielen, wenn Sie von einem sehr kleinen zu einem sehr großen Bild wechseln.
Robusto
2
@Robusto Ich muss das Bild danach in die Leinwand legen und später an den Server senden. Ich möchte ein großes Bild auf ein kleines verkleinern, die Farbe in einer Leinwand ändern und das Ergebnis an den Server senden. Was denkst du sollte ich tun?
Confile
9
@ Robusto Das ist das Problem. Das Anzeigen eines kleinen Bildes auf dem Client ist einfach. img.width nad img.height ist so trivial. Ich möchte es nur einmal und nicht noch einmal auf dem Server verkleinern.
Confile
8

Nicht die richtige Antwort für Leute, die wirklich die Größe des Bildes selbst ändern müssen, sondern nur die Dateigröße verkleinern möchten .

Ich hatte ein Problem mit Bildern "direkt von der Kamera", die meine Kunden häufig in "unkomprimiertem" JPEG hochgeladen haben.

Nicht so bekannt ist, dass die Leinwand (in den meisten Browsern 2017) unterstützt, die Qualität von JPEG zu ändern

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

Mit diesem Trick könnte ich 4k x 3k Bilder mit> 10Mb auf 1 oder 2Mb reduzieren, sicher, dass es von Ihren Bedürfnissen abhängt.

Schau hier

Halbbit
quelle
4

Hier ist ein wiederverwendbarer Angular-Dienst für die Größenänderung von Bildern / Leinwand in hoher Qualität: https://gist.github.com/fisch0920/37bac5e741eaec60e983

Der Service unterstützt die Faltung von Lanczos und das schrittweise Downscaling. Der Faltungsansatz bietet eine höhere Qualität auf Kosten der Langsamkeit, während der schrittweise Downscaling-Ansatz einigermaßen antialiasierte Ergebnisse liefert und erheblich schneller ist.

Anwendungsbeispiel:

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

Dies ist der verbesserte Hermite-Größenänderungsfilter, der 1 Arbeiter verwendet, damit das Fenster nicht einfriert.

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

Ich habe eine Lösung gefunden, die nicht direkt auf die Pixeldaten zugreifen und diese durchlaufen muss, um das Downsampling durchzuführen. Abhängig von der Größe des Bildes kann dies sehr ressourcenintensiv sein, und es ist besser, die internen Algorithmen des Browsers zu verwenden.

Die Funktion drawImage () verwendet eine Resampling-Methode mit linearer Interpolation und nächstem Nachbarn. Das funktioniert gut, wenn Sie nicht mehr als die Hälfte der Originalgröße verkleinern .

Wenn Sie eine Schleife ausführen, um jeweils nur die Hälfte der Größe zu ändern, sind die Ergebnisse recht gut und viel schneller als der Zugriff auf Pixeldaten.

Diese Funktion wird jeweils auf die Hälfte heruntergesampelt, bis die gewünschte Größe erreicht ist:

  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
quelle
Könnten Sie bitte eine jsfiddle und einige resultierende Bilder posten?
Confile
In dem Link unten finden Sie resultierende Bilder mit dieser Technik
Jesús Carrera
1

Vielleicht können Sie dies versuchen, was ich immer in meinem Projekt verwende. Auf diese Weise erhalten Sie nicht nur qualitativ hochwertige Bilder, sondern auch jedes andere Element auf Ihrer Leinwand.

/* 
 * @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
quelle
0

anstelle von .85 , wenn wir 1.0 hinzufügen . Sie erhalten eine genaue Antwort.

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

Sie können ein klares und helles Bild erhalten. Bitte prüfen

Phönix
quelle
0

Ich versuche wirklich zu vermeiden, Bilddaten zu durchlaufen, insbesondere bei größeren Bildern. Daher habe ich eine ziemlich einfache Möglichkeit gefunden, die Bildgröße mit ein paar zusätzlichen Schritten ohne Einschränkungen oder Einschränkungen angemessen zu reduzieren. Diese Routine geht auf den niedrigstmöglichen halben Schritt vor der gewünschten Zielgröße zurück. Dann skaliert es auf die doppelte Zielgröße und dann wieder auf die Hälfte. Klingt zunächst lustig, aber die Ergebnisse sind erstaunlich gut und gehen schnell dahin.

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
quelle
-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>
Formenbau
quelle
-1

DEMO : Ändern der Größe von Bildern mit JS und HTML Canvas Demo Fiddler.

Möglicherweise finden Sie drei verschiedene Methoden, um die Größe zu ändern. Diese helfen Ihnen zu verstehen, wie der Code funktioniert und warum.

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

Der vollständige Code der Demo und der TypeScript-Methode, die Sie möglicherweise in Ihrem Code verwenden möchten, finden Sie im GitHub-Projekt.

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

Dies ist der endgültige Code:

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
        }
    };
}}
Augen c
quelle