HTML5 Canvas Resize (Downscale) Resim Yüksek Kalite?


149

Tarayıcımdaki resimleri yeniden boyutlandırmak için html5 tuval öğelerini kullanıyorum. Kalitenin çok düşük olduğu ortaya çıkıyor. Bunu buldum: Bir <canvas> 'ı Ölçeklerken İnterpolasyonu Devre Dışı Bırakın, ancak kaliteyi artırmaya yardımcı olmaz.

Aşağıda css ve js kodumun yanı sıra Photoshop ile aranan ve canvas API'sinde ölçeklenen görüntü var.

Tarayıcıda bir görüntüyü ölçeklerken en iyi kaliteyi elde etmek için ne yapmam gerekir?

Not: Büyük bir görüntüyü küçük bir görüntüye ölçeklendirmek, bir tuvaldeki rengi değiştirmek ve tuvalden sonucu sunucuya göndermek istiyorum.

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

Görüntü photoshop ile yeniden boyutlandırıldı:

resim açıklamasını buraya girin

Resim tuval üzerine yeniden boyutlandırıldı:

resim açıklamasını buraya girin

Düzenle:

Ben aşağıda önerildiği gibi birden fazla adımda ölçek küçültme yapmaya çalıştı:

HTML5 tuval ve Html5 tuval çizimindeki bir görüntüyü yeniden boyutlandırma

Bu kullandığım işlevdir:

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


}

İşte 2 adım aşağı boyutlandırma kullanırsanız sonuç:

resim açıklamasını buraya girin

İşte 3 adım aşağı boyutlandırma kullanırsanız sonuç:

resim açıklamasını buraya girin

İşte 4 adım aşağı boyutlandırma kullanırsanız sonuç:

resim açıklamasını buraya girin

İşte 20 adım aşağı boyutlandırma kullanırsanız sonuç:

resim açıklamasını buraya girin

Not: 1 adımdan 2 adıma kadar görüntü kalitesinde büyük bir iyileşme olduğu ortaya çıkar, ancak sürece ne kadar çok adım eklerseniz, görüntü o kadar bulanık olur.

Eklediğiniz daha fazla adım, resmin daha bulanık hale gelmesi sorununu çözmenin bir yolu var mı?

Edit 2013-10-04: GameAlchemist algoritmasını denedim. İşte Photoshop ile karşılaştırıldığında sonuç.

PhotoShop Görüntüsü:

PhotoShop Görüntüsü

GameAlchemist Algoritması:

GameAlchemist Algoritması


2
Görüntünüzü aşamalı olarak ölçeklendirmeyi deneyebilirsiniz: stackoverflow.com/questions/18761404/…
markE

1
Html5 canvas drawImage'ın olası kopyası : kenar yumuşatma nasıl uygulanır . Bakalım işe yaramıyor. Görüntüler büyük ve küçük boyuta küçültülmüşse, bunu adım adım yapmanız gerekir (bağlantıdaki örnek resimlere bakın)

2
@confile enterpolasyonu kapatmak onu daha da kötüleştirecektir. Bunu etkin tutmak istiyorsunuz. Yukarıda verdiğim bağlantıya bakın. Orada daha büyük görüntüleri küçültmek ve kaliteyi korumak için nasıl adımlar atılacağını göstereceğim. Scott'ın dediği gibi, kaliteye göre hıza öncelik vermek istiyorsunuz.

1
@ Ken-AbdiasSoftware Sana yaklaşım denedim ama sorun adım adım ölçekleme için kullandığım daha fazla mermi daha kötü olacak olmasıdır. Bunu nasıl düzeltebileceğine dair bir fikrin var mı?
13'te

3
Şüphesiz HTML5 kullanarak pahalı bir profesyonel fotoğraf düzenleme yazılımının işlevselliğini çoğaltma şansı oldukça zayıf mı? Muhtemelen yaklaşabilirsiniz (ish), ama tam olarak Photoshop'ta çalıştığı gibi imkansız olacağını hayal ediyorum!
Liam

Yanıtlar:


171

Sorununuz görüntünüzü küçültmek olduğundan, enterpolasyon hakkında konuşmanın anlamı yoktur - ki bu piksel oluşturmakla ilgilidir. Buradaki sorun altörneklemedir.

Bir görüntüyü altörneklemek için, orijinal görüntüdeki p * p piksellerinin her karesini hedef görüntüdeki tek bir piksele dönüştürmeliyiz.

Performans nedenlerinden dolayı Tarayıcılar çok basit bir altörnekleme yapar: daha küçük resmi oluşturmak için, kaynakta yalnızca bir piksel seçer ve hedef için değerini kullanırlar. bazı detayları 'unutur' ve gürültü ekler.

Ancak bunun bir istisnası vardır: 2X görüntü altörneklemesinin hesaplanması çok basit olduğundan (bir tane yapmak için ortalama 4 piksel) ve retina / HiDPI pikselleri için kullanıldığında, bu durum düzgün bir şekilde işlenir - Tarayıcı yapmak için 4 piksel kullanır bir-.

AMA ... 2 kat aşağı örnekleme birkaç kez kullanırsanız, ardışık yuvarlama hatalarının çok fazla gürültü ekleyeceği sorunuyla karşılaşırsınız.
Daha da kötüsü, her zaman iki güçle yeniden boyutlandırmazsınız ve en yakın güce yeniden boyutlandırma + son yeniden boyutlandırma çok gürültülüdür.

Aradığınız piksel mükemmel bir altörneklemedir, yani: tüm girdi piksellerini - ölçek ne olursa olsun - dikkate alan görüntünün yeniden örneklenmesi.
Bunu yapmak için, her bir giriş pikseli için, giriş piksellerinin ölçekli projeksiyonunun hedef piksellerin içinde olmasına, bir X kenarının, Y kenarının veya her ikisinin birden örtüşmesine bağlı olarak bir, iki veya dört hedef piksele katkısını hesaplamamız gerekir. .
(Burada bir plan iyi olurdu, ama bir planım yok.)

İşte bir zombatın 1/3 ölçeğinde tuval ölçeğine karşı piksel mükemmel ölçeğime bir örnek.

Resmin Tarayıcınızda ölçeklenebileceğini ve SO tarafından .jpegized edildiğini unutmayın.
Yine de, özellikle wombatın arkasındaki çimenlerde ve sağdaki dallarda çok daha az gürültü olduğunu görüyoruz. Kürkteki gürültü daha zıttır, ancak beyaz saçları var - görünüşe göre kaynak resim-.
Doğru görüntü daha az çekici ama kesinlikle daha güzel.

resim açıklamasını buraya girin

İşte mükemmel piksel küçültme yapmak için kod:

keman sonucu: http://jsfiddle.net/gamealchemist/r6aVp/embedded/result/
kemanın kendisi: 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;
}

Öyle oldukça bir şamandıra tampon hedef görüntünün orta değerlerini saklamak için gerekli olduğundan, bellek açgözlü (-> biz sonuç tuval sayarsak, biz 6 kez bu algoritmada kaynak resmin bellek kullanın).
Ayrıca, her kaynak piksel, hedef boyutu ne olursa olsun kullanıldığından oldukça pahalıdır ve getImageData / putImageDate için de ödeme yapmak zorundayız, ayrıca oldukça yavaş.
Ancak bu durumda her kaynak değerini işlemekten daha hızlı olmanın bir yolu yoktur ve durum o kadar da kötü değildir: Bir wombatın 740 * 556 görüntüsünde, işleme 30 ila 40 ms sürer.


Resmi tuvale koymadan önce ölçeklendirmek daha hızlı olabilir mi?
yapılandırma dosyasındaki

Anlamadım ... Görünüşe göre yaptığım şey bu. Tampon ve oluşturduğum kanvas (resCV) ölçeklendirilmiş görüntünün boyutuna sahip. Daha hızlı almanın tek yolunun breshensam benzeri tamsayı hesaplaması kullanmak olduğunu düşünüyorum. Ancak 40ms sadece bir video oyunu (25 fps) için yavaştır, çizim uygulaması için değil.
GameAlchemist

kaliteyi korurken algoritmanızı daha hızlı hale getirme şansı görüyor musunuz?
13'ü

1
0 kullanarak arabellek (algoritmanın son kısmı) yuvarlak denedim | Mat.ceil yerine. Biraz daha hızlı. Ama yine de get / putImageData ile oldukça fazla yük var ve yine her pikseli işlemekten kaçınamıyoruz.
GameAlchemist

4
Tamam, bu yüzden kodu izledim: çözümden çok yakınsınız. İki hata: dizinleriniz tX + 1 için birer birer devre dışı bırakıldı (+4, +5, +6, +7 yerine + 3, + 4, + 5, + 6 idi) ve rgba'daki satır değiştirme bir mul 4 ile, 3 değil. Sadece 4 rastgele değeri test ettim (0.1, 0.15, 0.33, 0.8). güncellenmiş kemanınız burada: jsfiddle.net/gamealchemist/kpQyE/3
GameAlchemist

51

İyi kalitede hızlı tuval resample: http://jsfiddle.net/9g9Nv/442/

Güncelleme: sürüm 2.0 (daha hızlı, web çalışanları + aktarılabilir nesneler) - 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);
}

Ben en iyi kalite ihtiyacım
yapılandırma dosyasındaki

18
düzeltildi, "iyi" yi "en iyi" olarak değiştirdim, şimdi sorun yok mu? : D. Öte yandan, mümkün olan en iyi yeniden örneklemeyi istiyorsanız - imagemagick kullanın.
ViliusL

@confile imgur.com jsfiddle'da kullanmak güvenliydi, ancak yöneticiler yanlış bir şey mi yaptı? İyi kalite görmüyorsunuz, çünkü tarayıcınız CORS'a ölümcül hata veriyor. (uzak sitelerden resim kullanılamaz)
ViliusL

Tamam, saydam alanlara sahip başka bir PNG resmi kullanabilirsiniz. Bu konuda bir fikrin var mı?
yapılandırma dosyasındaki

4
@ haklısın, bazı durumlarda saydam görüntülerin keskin alanlarda sorunları vardı. Testimle bu davaları kaçırdım. Sabit yeniden boyutlandırma aynı zamanda kemandaki
ViliusL

28

Öneri 1 - Proses boru hattını uzatın

Başvurduğunuz bağlantılarda açıkladığım şekilde aşağı inmeyi kullanabilirsiniz, ancak bunları yanlış bir şekilde kullanıyorsunuz.

Görüntüleri 1: 2'nin üzerindeki oranlara ölçeklendirmek için yavaşlama gerekmez (genellikle, ancak bunlarla sınırlı değildir). Bir yapmanız gereken nerede olduğunu şiddetli iki (ve nadiren de diğer) (ince çizgiler olarak yüksek frekanslar gerçekleştiği özellikle) görüntünün içeriğine bağlı olarak adımlar onu bölmek gerekir aşağı ölçekleme.

Bir görüntüyü her indirdiğinizde ayrıntıları ve bilgileri kaybedersiniz. Ortaya çıkan görüntünün orijinal kadar net olmasını bekleyemezsiniz.

Daha sonra görüntüleri birçok adımda ölçeklendirirseniz, toplamda çok fazla bilgi kaybedersiniz ve sonuç zaten fark ettiğiniz gibi zayıf olacaktır.

Ekstra bir adımla veya en üst iki adımda deneyin.

konvolusyonları

Photoshop'un, görüntü yeniden örneklendikten sonra keskinleştirme gibi bir kıvrım uyguladığını fark etmesi durumunda. Sadece iki kübik enterpolasyon değil, bu yüzden Photoshop'u tamamen taklit etmek için Photoshop'un yaptığı adımları da eklememiz gerekiyor (varsayılan kurulumla).

Bu örnekte, gönderinizde bahsettiğiniz orijinal cevabımı kullanacağım, ancak bir post süreci olarak kaliteyi artırmak için buna keskin bir kıvrım ekledim (bkz. Altta demo).

İşte keskinleştirme filtresi eklemek için kod (genel bir evrişim filtresine dayanır - İçinde keskinleştirmek için ağırlık matrisini ve efektin telaffuzunu ayarlamak için bir karışım faktörünü koydum):

Kullanımı:

sharpen(context, width, height, mixFactor);

Bu mixFactor, [0.0, 1.0] arasındaki bir değerdir ve keskinleştirme efektini küçümsemenize izin verir - başparmak kuralı: daha az boyut, daha az efekt gerekir.

İşlev ( bu snippet'e dayanarak ):

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

Bu kombinasyonu kullanmanın sonucu:

ONLINE DEMO BURADA

Sonuç örnek ve kıvrımları keskinleştirir

Karışıma ne kadar keskinleştirme eklemek istediğinize bağlı olarak, varsayılan "bulanık" durumdan çok keskin sonuçlar elde edebilirsiniz:

Keskinleştirme çeşitleri

Öneri 2 - düşük seviye algoritma uygulaması

En iyi sonucu kalite açısından elde etmek istiyorsanız, düşük seviyeye gitmeniz ve bunu yapmak için örneğin bu yepyeni algoritmayı uygulamayı düşünmeniz gerekir.

Bkz. IEEE'den İnterpolasyona Bağlı Görüntü Altörnekleme (2011).
İşte makalenin tam bağlantısı (PDF) .

Şu anda JavaScript AFAIK içinde bu algoritmanın herhangi bir uygulaması yok, bu yüzden kendinizi bu göreve atmak istiyorsanız bir tam eliniz var.

Özü (kağıttan alıntılar):

Öz

Bu yazıda düşük bit oranlı görüntü kodlaması için enterpolasyona yönelik bir uyarlamalı aşağı örnekleme algoritması önerilmiştir. Bir görüntü verildiğinde, önerilen algoritma, giriş görüntüsü ile aynı çözünürlüğe sahip yüksek kaliteli bir görüntünün enterpole edilebileceği düşük çözünürlüklü bir görüntü elde edebilir. İnterpolasyon işleminden bağımsız olan geleneksel aşağı örnekleme algoritmalarından farklı olarak, önerilen aşağı örnekleme algoritması, aşağı örneklemeyi enterpolasyon işlemine bağlar. Sonuç olarak, önerilen aşağı örnekleme algoritması giriş görüntüsünün orijinal bilgisini en büyük ölçüde koruyabilir. Aşağı örneklenen görüntü daha sonra JPEG'e beslenir. Daha sonra, sıkıştırılmış düşük çözünürlüklü görüntüye toplam varyasyon (TV) tabanlı bir son işlem uygulanır. Sonuçta,Deneysel sonuçlar, önerilen algoritma ile altörneklenmiş görüntünün kullanılmasıyla, çok daha yüksek kalitede enterpolasyonlu bir görüntünün elde edilebileceğini doğrulamaktadır. Ayrıca, önerilen algoritma düşük bit oranlı görüntü kodlaması için JPEG'den daha üstün performans elde edebilir.

Kağıttan anlık görüntü

(tüm ayrıntılar, formüller vb. için sağlanan bağlantıya bakın)


Bu da harika bir çözüm. Teşekkür ederim!
2014'te

Bu harika bir çözüm. Saydam alanları olan png dosyalarında denedim. İşte sonuç: jsfiddle.net/confile/5CD4N Çalışmasını sağlamak için ne yapacağınız hakkında bir fikriniz var mı?
2014'te

1
bu GENIUS! ama lütfen tam olarak ne yaptığını açıklayabilir misin? lol .. ben tamamen ins ve çıkışları bilmek istiyorum ... belki öğrenmek için kaynaklar?
carinlynchin

1
@Carine, zayıf bir yorum alanı için biraz fazla olabilir :), ancak ölçeklendirildiğinde bu grubu temsil eden yenisinin ortalamasını almak için bir piksel grubu örneklenir. Bu aslında genel olarak biraz bulanıklık getiren düşük geçişli bir filtredir. Keskinlik kaybını telafi etmek için sadece bir keskinleştirme kıvrımı uygulayın. Keskinleştirme çok belirgin olabileceğinden, bunun yerine görüntüyle karıştırabiliriz, böylece bileme seviyesini kontrol edebiliriz. Umarım bu biraz fikir verir.

21

Yalnızca tuvali kullanmak istiyorsanız, en iyi sonuç birden fazla aşağı yönde olacaktır. Ama bu henüz yeterli değil. Daha iyi kalite için saf js uygulamasına ihtiyacınız var. Değişken kalite / hıza sahip pica - yüksek hızlı küçültücü piyasaya sürdük . Kısacası, ~ 0.1s'de 1280 * 1024px ve 1s'de 5000 * 3000px görüntüyü en yüksek kalitede yeniden boyutlandırır (3 loblu lanczos filtresi). Pica, görüntülerinizle, kalite düzeylerinizle oynayabileceğiniz ve hatta mobil cihazlarda deneyebileceğiniz demoya sahiptir .

Pica'nın henüz keskin olmayan maskesi yok, ancak çok yakında eklenecek. Bu, yeniden boyutlandırma için yüksek hızlı evrişim filtresi uygulamaktan çok daha kolaydır.


16

Resimleri yeniden boyutlandırmak için tuvali neden kullanmalıyım? Modern tarayıcıların hepsi bisubik enterpolasyon kullanır - Photoshop tarafından kullanılanla aynı işlemdir (doğru yapıyorsanız) ve bunu tuval işleminden daha hızlı yaparlar. İstediğiniz görüntü boyutunu belirtin (orantılı olarak yeniden boyutlandırmak için yalnızca bir boyut, yükseklik veya genişlik kullanın).

Bu, IE'nin sonraki sürümleri de dahil olmak üzere çoğu tarayıcı tarafından desteklenir. Önceki sürümler tarayıcıya özgü CSS gerektirebilir .

Görüntüyü yeniden boyutlandırmak için basit bir işlev (jQuery kullanarak) şöyle olur:

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

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

Ardından, görüntüyü bir veya her iki boyutta yeniden boyutlandırmak için döndürülen değeri kullanın.

Açıkçası yapabileceğiniz farklı ayrıntılandırmalar var, ama bu işi hallediyor.

Aşağıdaki kodu bu sayfanın konsoluna yapıştırın ve gravatarlara ne olduğunu izleyin:

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
Ayrıca, yalnızca bir boyut belirtirseniz, (modern) tarayıcının görüntünün doğal en boy oranını otomatik olarak koruyacağını unutmayın.
André Dion

38
Belki yeniden boyutlandırılan görüntüyü bir sunucuya göndermesi gerekir.
Sergiu Paraschiv

2
@Sergiu: Gerekli değil, ancak çok küçük bir görüntüden çok büyük bir görüntüye gidiyorsanız, bir sunucudan bile harika sonuçlar elde edemeyeceğinizi unutmayın.
Robusto

2
@Robusto Daha sonra resmi tuvale koyup daha sonra sunucuya göndermem gerekiyor. Büyük bir görüntüyü küçük bir görüntüye ölçeklendirmek, bir tuvaldeki rengi değiştirmek ve sonucu sunucuya göndermek istiyorum. Ne yapmam gerektiğini düşünüyorsun?
yapılandırma dosyasındaki

9
@Robusto Sorun bu. İstemcide küçük bir görüntü göstermek kolaydır. img.width nad img.height çok önemsiz. Sunucuda tekrar değil, yalnızca bir kez ölçeklendirmek istiyorum.
yapılandırma dosyasındaki

8

Resmin kendisini gerçekten yeniden boyutlandırması gereken insanlar için doğru cevap değil, sadece dosya boyutunu küçültmek .

Müşterilerimin sık sık "sıkıştırılmamış" JPEG'e yüklediği "doğrudan kameradan" resimlerle ilgili bir sorun yaşadım.

Çok iyi bilinmeyen, tuvalin JPEG kalitesini değiştirmek için desteklediği (çoğu tarayıcıda 2017)

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

Bu hile ile 4k x 3k resimlerini> 10Mb'den 1 veya 2Mb'ye düşürebilirim, eminim ihtiyaçlarınıza bağlıdır.

buraya bak


4

İşte yüksek kaliteli görüntü / tuval boyutlandırma için yeniden kullanılabilir bir açısal hizmet: https://gist.github.com/fisch0920/37bac5e741eaec60e983

Hizmet, lanczos evrişimini ve adım adım ölçek küçültmeyi destekler. Evrişim yaklaşımı daha yavaş olma maliyetiyle daha yüksek kalitedir, buna karşın kademeli ölçek küçültme yaklaşımı makul ölçüde yumuşatılmış sonuçlar üretir ve önemli ölçüde daha hızlıdır.

Örnek kullanım:

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

Bu, pencerenin donmaması için 1 çalışanı kullanan geliştirilmiş Hermite yeniden boyutlandırma filtresidir.

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

Doğrudan piksel verilerine erişmesi ve altörneklemeyi gerçekleştirmek için döngü yapması gerekmeyen bir çözüm buldum. Görüntünün boyutuna bağlı olarak bu çok kaynak yoğun olabilir ve tarayıcının dahili algoritmalarını kullanmak daha iyi olur.

DrawImage () işlevi, bir doğrusal interpolasyon kullanarak, en yakın komşu yöntemi yeniden örnekleme. Orijinal boyutun yarısından fazlasını küçültmediğinizde bu işe yarar .

Bir kerede en fazla bir buçuk boyutu yeniden boyutlandırmak istiyorsanız, sonuçlar oldukça iyi ve piksel verilerine erişmekten çok daha hızlı olur.

Bu işlev, istenen boyuta ulaşana kadar bir seferde yarıya indirilir:

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

Lütfen bir jsfiddle ve sonuçta ortaya çıkan bazı resimler yayınlayabilir misiniz?
yapılandırma dosyasındaki

Alttaki bağlantıda bu tekniği kullanarak elde edilen görüntüleri bulabilirsiniz
Jesús Carrera

1

Belki adam bunu deneyebilirsiniz, ki bu benim projemde her zaman kullanıyorum.Bu şekilde sadece yüksek kaliteli görüntü elde etmekle kalmaz, aynı zamanda tuvalinizdeki herhangi bir öğeyi elde edebilirsiniz.

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

yerine .85 , biz eklerseniz 1.0 . Kesin cevap alacaksınız.

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

Net ve parlak görüntü elde edebilirsiniz. lütfen kontrol edin


0

Görüntü verilerinin, özellikle de büyük resimlerde, kaçınmaya çalışıyorum. Böylece, birkaç ekstra adım kullanarak herhangi bir kısıtlama veya sınırlama olmaksızın görüntü boyutunu düzgün bir şekilde küçültmek için oldukça basit bir yol buldum. Bu rutin, istenen hedef boyuttan önce mümkün olan en düşük yarım basamağa iner. Daha sonra hedef boyutun iki katına ve ardından tekrar yarıya kadar ölçeklendirir. İlk başta komik geliyor, ama sonuçlar şaşırtıcı derecede iyi ve hızlı bir şekilde oraya gidiyor.

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 : JS ve HTML Canvas Demo kemancı ile görüntüleri yeniden boyutlandırma.

Kodun nasıl çalıştığını ve nedenini anlamanıza yardımcı olacak bu yeniden boyutlandırmayı yapmak için 3 farklı yöntem bulabilirsiniz.

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

Kodunuzda kullanmak isteyebileceğiniz hem demo hem de TypeScript yönteminin tam kodu GitHub projesinde bulunabilir.

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

Bu son kod:

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
        }
    };
}}
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.