HTML5 Canvas Resize (Downscale) Kualitas Gambar Tinggi?

149

Saya menggunakan elemen kanvas html5 untuk mengubah ukuran gambar di browser saya. Ternyata kualitasnya sangat rendah. Saya menemukan ini: Nonaktifkan Interpolasi saat Melakukan Penskalaan <kanvas> tetapi tidak membantu meningkatkan kualitas.

Di bawah ini adalah kode css dan js saya serta gambar yang dipindai dengan Photoshop dan diskalakan di kanvas API.

Apa yang harus saya lakukan untuk mendapatkan kualitas optimal saat menskala gambar di browser?

Catatan: Saya ingin memperkecil gambar besar menjadi kecil, memodifikasi warna di kanvas dan mengirim hasilnya dari kanvas ke 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);
});

Ukuran gambar dengan photoshop:

masukkan deskripsi gambar di sini

Ukuran gambar di kanvas:

masukkan deskripsi gambar di sini

Edit:

Saya mencoba membuat downscaling dalam lebih dari satu langkah seperti yang diusulkan dalam:

Mengubah ukuran gambar di kanvas HTML5 dan kanvas gambar Html5 drawImage: cara menerapkan antialiasing

Ini adalah fungsi yang saya gunakan:

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


}

Inilah hasilnya jika saya menggunakan ukuran 2 langkah turun:

masukkan deskripsi gambar di sini

Inilah hasilnya jika saya menggunakan ukuran 3 langkah turun:

masukkan deskripsi gambar di sini

Ini hasilnya jika saya menggunakan ukuran 4 langkah:

masukkan deskripsi gambar di sini

Inilah hasilnya jika saya menggunakan ukuran 20 langkah turun:

masukkan deskripsi gambar di sini

Catatan: Ternyata dari 1 langkah ke 2 langkah ada peningkatan besar dalam kualitas gambar tetapi semakin banyak langkah yang Anda tambahkan ke proses semakin fuzzy gambar menjadi.

Apakah ada cara untuk menyelesaikan masalah sehingga gambar menjadi lebih kabur, semakin banyak langkah yang Anda tambahkan?

Sunting 2013-10-04: Saya mencoba algoritma dari GameAlchemist. Ini hasilnya dibandingkan dengan Photoshop.

Gambar PhotoShop:

Gambar PhotoShop

Algoritma GameAlchemist:

Algoritma GameAlchemist

confile
sumber
2
Anda dapat mencoba mengubah gambar secara bertahap: stackoverflow.com/questions/18761404/…
tandai
1
kemungkinan duplikat dari drawImage kanvas Html5: bagaimana menerapkan antialiasing . Lihat apakah tidak berhasil. Jika gambar besar dan diperkecil menjadi ukuran kecil Anda harus melakukannya dalam langkah-langkah (lihat contoh gambar di tautan)
2
@ confile mematikan interpolasi akan membuatnya lebih buruk. Anda ingin itu diaktifkan. Lihatlah tautan yang saya berikan di atas. Saya tunjukkan di sana bagaimana menggunakan langkah-langkah untuk memperkecil gambar yang lebih besar dan menjaga kualitas. Dan seperti kata Scott, Anda ingin memprioritaskan kualitas daripada kecepatan.
1
@ Ken-AbdiasSoftware Saya mencoba Anda mendekati tetapi masalahnya adalah bahwa hal itu akan semakin buruk semakin banyak putaran yang saya gunakan untuk penskalaan langkah-bijaksana. Adakah cara untuk memperbaikinya?
confile
3
Tentunya peluang mereplikasi fungsi perangkat lunak pengedit foto profesional yang mahal menggunakan HTML5 cukup tipis? Anda mungkin bisa mendekati (ish), tetapi persis seperti itu bekerja di Photoshop saya bayangkan tidak mungkin!
Liam

Jawaban:

171

Karena masalah Anda adalah menurunkan skala gambar Anda, tidak ada gunanya berbicara tentang interpolasi - yang tentang membuat pixel-. Masalahnya di sini adalah downsampling.

Untuk mengurangi sampel gambar, kita perlu mengubah setiap kuadrat p * p piksel dalam gambar asli menjadi satu piksel dalam gambar tujuan.

Untuk alasan kinerja, Browser melakukan downsampling yang sangat sederhana: untuk membangun gambar yang lebih kecil, mereka hanya akan memilih SATU piksel dalam sumber dan menggunakan nilainya untuk tujuan. yang 'lupa' beberapa detail dan menambahkan suara.

Namun ada pengecualian untuk itu: karena downsampling gambar 2X sangat mudah untuk dihitung (rata-rata 4 piksel untuk membuatnya) dan digunakan untuk retina / piksel HiDPI, kasing ini ditangani dengan benar -Peramban menggunakan 4 piksel untuk membuat satu-.

TAPI ... jika Anda menggunakan downsampling 2X beberapa kali, Anda akan menghadapi masalah bahwa kesalahan pembulatan berturut-turut akan menambah terlalu banyak noise.
Yang lebih buruk, Anda tidak akan selalu mengubah ukuran dengan kekuatan dua, dan mengubah ukuran ke kekuatan terdekat + mengubah ukuran terakhir sangat bising.

Apa yang Anda cari adalah downsampling piksel-sempurna, yaitu: pengambilan sampel ulang gambar yang akan mengambil semua piksel input ke dalam akun - apa pun skalanya-.
Untuk melakukan itu kita harus menghitung, untuk setiap piksel input, kontribusinya terhadap satu, dua, atau empat piksel tujuan tergantung apakah proyeksi skala dari piksel input tepat di dalam piksel tujuan, tumpang tindih dengan batas X, perbatasan Y, atau keduanya .
(Skema akan menyenangkan di sini, tetapi saya tidak memilikinya.)

Berikut adalah contoh skala kanvas vs skala sempurna pixel saya pada skala 1/3 dari zombat.

Perhatikan bahwa gambar mungkin diskalakan di Browser Anda, dan .jpegized oleh SO.
Namun kita melihat bahwa ada jauh lebih sedikit kebisingan terutama di rumput di belakang wombat, dan cabang-cabang di sebelah kanan. Suara di bulu membuatnya lebih kontras, tapi sepertinya dia punya rambut putih - tidak seperti gambar sumber -.
Gambar kanan kurang menarik tetapi pasti lebih bagus.

masukkan deskripsi gambar di sini

Berikut kode untuk melakukan penurunan skala pixel sempurna:

hasil biola: http://jsfiddle.net/gamealchemist/r6aVp/embedded/result/
fiddle sendiri: 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;
}

Hal ini cukup rakus memori, karena penyangga mengapung diperlukan untuk menyimpan nilai-nilai menengah gambar tujuan (-> jika kita menghitung hasil kanvas, kita menggunakan 6 kali memori sumber gambar dalam algoritma ini).
Ini juga cukup mahal, karena setiap piksel sumber digunakan apa pun ukuran tujuan, dan kami harus membayar untuk getImageData / putImageDate, cukup lambat juga.
Tetapi tidak ada cara untuk lebih cepat daripada memproses setiap nilai sumber dalam kasus ini, dan situasinya tidak terlalu buruk: Untuk gambar 740 * 556 saya dari wombat, pemrosesan membutuhkan antara 30 dan 40 ms.

GameAlchemist
sumber
Mungkinkah lebih cepat jika Anda skala gambar sebelum Anda taruh di kanvas?
confile
saya tidak mengerti ... sepertinya itu yang saya lakukan. Buffer serta kanvas yang saya buat (resCV) memiliki ukuran gambar yang diskalakan. Saya pikir satu-satunya cara untuk mendapatkannya lebih cepat adalah dengan menggunakan perhitungan integer seperti breshensam. Tapi 40ms hanya lambat untuk gim video (25 fps), bukan untuk aplikasi menggambar.
GameAlchemist
apakah Anda melihat peluang untuk membuat algoritme Anda lebih cepat dengan tetap menjaga kualitas?
confile
1
saya mencoba membulatkan buffer (bagian terbaru dari algoritma) menggunakan 0 | bukannya Mat.ceil. Ini sedikit lebih cepat. Tapi bagaimanapun juga ada beberapa overhead dengan get / putImageData dan lagi, kita tidak bisa menghindari untuk memproses setiap pixel.
GameAlchemist
4
Ok, jadi saya melihat kode: Anda sangat dekat dari solusi. Dua kesalahan: indeks Anda padam oleh satu untuk tX + 1 (mereka +3, +4, + 5, + 6 bukannya +4, +5, +6, +7), dan mengubah baris dalam rgba adalah mul oleh 4, bukan 3. Saya baru saja menguji 4 nilai acak untuk memeriksa (0,1, 0,15, 0,33, 0,8) sepertinya ok. biola Anda yang diperbarui ada di sini: jsfiddle.net/gamealchemist/kpQyE/3
GameAlchemist
51

Contoh kanvas cepat dengan kualitas baik: http://jsfiddle.net/9g9Nv/442/

Pembaruan: versi 2.0 (lebih cepat, pekerja web + objek yang dapat ditransfer) - 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
sumber
Saya membutuhkan kualitas terbaik
confile
18
diperbaiki, saya mengubah "baik" menjadi "terbaik", apakah ini tidak apa-apa sekarang? : D. Di sisi lain, jika Anda ingin resample mungkin - gunakan imagemagick.
ViliusL
@confile imgur.com aman digunakan di jsfiddle, tetapi admin melakukan sesuatu yang salah? Anda tidak melihat kualitas yang baik, karena peramban Anda memberikan kesalahan fatal pada CORS. (tidak dapat menggunakan gambar dari situs remote)
ViliusL
oke Anda bisa menggunakan gambar PNG lainnya dengan area transparan. Ada ide tentang ini?
confile
4
@ confile Anda benar, pada beberapa kasus gambar transparan memiliki masalah di area yang tajam. Saya melewatkan kasus-kasus ini dengan tes saya. Memperbaiki ukuran juga memperbaiki dukungan gambar jarak jauh pada fiddle: jsfiddle.net/9g9Nv/49
ViliusL
28

Saran 1 - memperpanjang jalur pipa proses

Anda dapat menggunakan step-down seperti yang saya jelaskan di tautan yang Anda rujuk tetapi Anda tampaknya menggunakannya dengan cara yang salah.

Langkah ke bawah tidak diperlukan untuk skala gambar dengan rasio di atas 1: 2 (biasanya, tetapi tidak terbatas pada). Di sinilah Anda perlu melakukan penskalaan drastis yang Anda perlukan untuk membaginya dalam dua (dan jarang, lebih banyak) langkah tergantung pada konten gambar (khususnya di mana frekuensi tinggi seperti garis tipis terjadi).

Setiap kali Anda membuat sampel gambar, Anda akan kehilangan detail dan informasi. Anda tidak dapat mengharapkan gambar yang dihasilkan sejelas aslinya.

Jika Anda kemudian memperkecil gambar dalam banyak langkah, Anda akan kehilangan banyak informasi secara total dan hasilnya akan buruk seperti yang Anda perhatikan.

Coba hanya dengan satu langkah ekstra, atau di atas dua.

Konvolusi

Dalam kasus Photoshop perhatikan bahwa itu menerapkan konvolusi setelah gambar telah sampel ulang, seperti mempertajam. Ini bukan hanya interpolasi dwi-kubik yang terjadi sehingga untuk sepenuhnya meniru Photoshop kita perlu juga menambahkan langkah-langkah yang dilakukan Photoshop (dengan pengaturan default).

Untuk contoh ini saya akan menggunakan jawaban asli saya yang Anda rujuk di posting Anda, tetapi saya telah menambahkan lilitan yang tajam padanya untuk meningkatkan kualitas sebagai proses posting (lihat demo di bawah).

Berikut ini adalah kode untuk menambahkan filter mempertajam (ini didasarkan pada filter konvolusi generik - Saya meletakkan matriks bobot untuk mempertajam di dalamnya serta faktor campuran untuk menyesuaikan pengucapan efek):

Pemakaian:

sharpen(context, width, height, mixFactor);

Nilai tersebut mixFactoradalah antara [0,0, 1.0] dan memungkinkan Anda mengecilkan efek mempertajam - rule-of-thumb: semakin sedikit ukuran semakin sedikit efek yang dibutuhkan.

Fungsi (berdasarkan cuplikan ini ):

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

Hasil dari menggunakan kombinasi ini adalah:

DEMO ONLINE DI SINI

Hasilnya downsample dan mempertajam konvolusi

Bergantung pada seberapa banyak penajaman yang ingin Anda tambahkan ke campuran, Anda bisa mendapatkan hasil dari "buram" default menjadi sangat tajam:

Variasi mempertajam

Saran 2 - implementasi algoritma tingkat rendah

Jika Anda ingin mendapatkan hasil terbaik dari segi kualitas, Anda harus menggunakan level rendah dan mempertimbangkan untuk menerapkan, misalnya algoritma baru ini untuk melakukannya.

Lihat Interpolation-Dependent Image Downsampling (2011) dari IEEE.
Berikut ini tautan ke kertas secara lengkap (PDF) .

Tidak ada implementasi dari algoritma ini di JavaScript AFAIK pada saat ini sehingga Anda siap untuk melakukan hand-full jika Anda ingin terlibat dalam tugas ini.

Intinya adalah (kutipan dari kertas):

Abstrak

Algoritma down-sampling adaptif berorientasi interpolasi diusulkan untuk pengkodean gambar bit-rate rendah dalam makalah ini. Diberikan gambar, algoritma yang diusulkan dapat memperoleh gambar resolusi rendah, dari mana gambar berkualitas tinggi dengan resolusi yang sama dengan gambar input dapat diinterpolasi. Berbeda dari algoritma down-sampling tradisional, yang independen dari proses interpolasi, algoritma down-sampling yang diusulkan bergantung pada down-sampling ke proses interpolasi. Akibatnya, algoritma down-sampling yang diusulkan mampu mempertahankan informasi asli dari gambar input ke tingkat terbesar. Gambar down-sampel kemudian dimasukkan ke dalam JPEG. Pemrosesan posting berbasis variasi total (TV) kemudian diterapkan pada gambar beresolusi rendah yang terkompresi. Akhirnya,Hasil eksperimental memverifikasi bahwa dengan menggunakan gambar downsampled oleh algoritma yang diusulkan, gambar interpolasi dengan kualitas yang jauh lebih tinggi dapat dicapai. Selain itu, algoritma yang diusulkan mampu mencapai kinerja yang unggul daripada JPEG untuk pengkodean gambar bit rate rendah.

Jepretan dari kertas

(lihat tautan yang disediakan untuk semua detail, rumus, dll.)

Komunitas
sumber
Ini juga solusi yang bagus. Terima kasih!
confile
Ini solusi hebat. Saya mencobanya pada file png dengan area transparan. Inilah hasilnya: jsfiddle.net/confile/5CD4N Apakah Anda tahu apa yang harus dilakukan untuk membuatnya berfungsi?
confile
1
ini jenius! tapi tolong bisakah Anda menjelaskan apa yang sebenarnya Anda lakukan? lol .. saya benar-benar ingin tahu seluk beluk ... mungkin sumber daya untuk belajar?
carinlynchin
1
@Carine yang bisa sedikit banyak untuk bidang komentar yang buruk :) tetapi, menurunkan contoh sekelompok piksel untuk rata-rata yang baru mewakili kelompok itu. Ini sebenarnya adalah filter low-pass yang memperkenalkan beberapa keburaman secara keseluruhan. Untuk mengkompensasi hilangnya ketajaman cukup menerapkan lilitan penajaman. Karena penajaman mungkin sangat terasa, kita bisa mencampurkannya dengan gambar sehingga kita dapat mengontrol tingkat penajaman. Harapan yang memberi wawasan.
21

Jika Anda ingin menggunakan kanvas saja, hasil terbaik adalah dengan beberapa downsteps. Tapi itu belum baik. Untuk kualitas yang lebih baik Anda perlu implementasi js murni. Kami baru saja merilis pica - downscaler kecepatan tinggi dengan kualitas / kecepatan variabel. Singkatnya, ini mengubah ukuran 1280 * 1024px dalam ~ 0,1s, dan gambar 5000 * 3000px dalam 1s, dengan kualitas tertinggi (filter lanczos dengan 3 lobus). Pica memiliki demo , tempat Anda dapat bermain dengan gambar, tingkat kualitas, dan bahkan mencobanya di perangkat seluler.

Pica belum memiliki topeng unsharp, tapi itu akan ditambahkan segera. Itu jauh lebih mudah daripada menerapkan filter konvolusi kecepatan tinggi untuk mengubah ukuran.

Vitaly
sumber
16

Mengapa menggunakan kanvas untuk mengubah ukuran gambar? Browser modern semuanya menggunakan interpolasi bikubik - proses yang sama digunakan oleh Photoshop (jika Anda melakukannya dengan benar) - dan mereka melakukannya lebih cepat daripada proses kanvas. Cukup tentukan ukuran gambar yang Anda inginkan (gunakan hanya satu dimensi, tinggi atau lebar, untuk mengubah ukuran secara proporsional).

Ini didukung oleh sebagian besar browser, termasuk versi IE yang lebih baru. Versi sebelumnya mungkin memerlukan CSS khusus browser .

Fungsi sederhana (menggunakan jQuery) untuk mengubah ukuran gambar adalah seperti ini:

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

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

Kemudian gunakan saja nilai yang dikembalikan untuk mengubah ukuran gambar dalam satu atau kedua dimensi.

Jelas ada perbaikan yang berbeda yang bisa Anda lakukan, tetapi ini menyelesaikan pekerjaan.

Rekatkan kode berikut ke konsol halaman ini dan saksikan apa yang terjadi pada gravatars:

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

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

$('.user-gravatar32 img').each(function(){
  var newDimensions = resizeImage( this, 150);
  this.style.width = newDimensions.width + "px";
  this.style.height = newDimensions.height + "px";
});
Robusto
sumber
2
Perhatikan juga bahwa jika Anda hanya menentukan satu dimensi, peramban (modern) akan secara otomatis mempertahankan rasio aspek alami gambar.
André Dion
38
Mungkin dia perlu mengirim gambar yang diubah ukurannya ke server.
Sergiu Paraschiv
2
@Sergiu: Tidak perlu, tetapi perhatikan bahwa jika Anda beralih dari gambar yang sangat kecil ke yang sangat besar, Anda tidak akan mendapatkan hasil yang luar biasa bahkan dari server.
Robusto
2
@ Ragusto Saya harus meletakkan gambar di kanvas setelahnya dan mengirimkannya ke server nanti. Saya ingin memperkecil gambar besar menjadi kecil, memodifikasi warna di kanvas dan mengirim hasilnya ke server. Menurut Anda apa yang harus saya lakukan?
confile
9
@ Ragusto Ini masalahnya. Menampilkan gambar kecil pada klien itu mudah. img.width nad img.height sangat sepele. Saya ingin menurunkannya hanya sekali dan tidak lagi di server.
confile
8

Bukan jawaban yang tepat untuk orang yang benar-benar perlu mengubah ukuran gambar itu sendiri, tetapi hanya untuk mengecilkan ukuran file .

Saya memiliki masalah dengan gambar "langsung dari kamera", yang sering diunggah pelanggan saya dalam format JPEG "tidak terkompresi".

Tidak begitu terkenal adalah, bahwa kanvas mendukung (di sebagian besar browser 2017) untuk mengubah kualitas JPEG

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

Dengan trik ini saya dapat mengurangi foto 4k x 3k dengan> 10Mb menjadi 1 atau 2Mb, tentu tergantung pada kebutuhan Anda.

Lihat di sini

setengah bit
sumber
4

Berikut ini adalah layanan Angular yang dapat digunakan kembali untuk mengubah ukuran gambar / kanvas berkualitas tinggi: https://gist.github.com/fisch0920/37bac5e741eaec60e983

Layanan ini mendukung lanczos convolution dan step-wise downscaling. Pendekatan konvolusi adalah kualitas yang lebih tinggi dengan biaya lebih lambat, sedangkan pendekatan penurunan langkah-bijaksana menghasilkan hasil yang cukup antialiased dan secara signifikan lebih cepat.

Contoh penggunaan:

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

Ini adalah filter ubah ukuran Hermite yang ditingkatkan yang menggunakan 1 pekerja sehingga jendela tidak membeku.

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

Saya menemukan solusi yang tidak perlu mengakses langsung data piksel dan mengulanginya untuk melakukan downsampling. Tergantung pada ukuran gambar, ini bisa menjadi sumber daya yang sangat intensif, dan akan lebih baik untuk menggunakan algoritma internal browser.

Fungsi drawImage () menggunakan metode interpolasi linier, tetangga-tetangga terdekat. Itu bekerja dengan baik ketika Anda tidak mengubah ukuran lebih dari setengah ukuran aslinya .

Jika Anda beralih ke hanya mengubah ukuran maks satu setengah pada satu waktu, hasilnya akan cukup baik, dan jauh lebih cepat daripada mengakses data piksel.

Fungsi ini downsample menjadi setengah sekaligus hingga mencapai ukuran yang diinginkan:

  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
sumber
Bisakah Anda memposting jsfiddle dan beberapa gambar yang dihasilkan?
confile
Di tautan di bagian bawah Anda dapat menemukan gambar yang dihasilkan menggunakan teknik ini
Jesús Carrera
1

Mungkin Anda dapat mencoba ini, yang selalu saya gunakan dalam proyek saya. Dengan cara ini Anda tidak hanya bisa mendapatkan gambar berkualitas tinggi, tetapi elemen lain di kanvas Anda.

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

bukannya 0,85 , jika kita menambahkan 1,0 . Anda akan mendapatkan jawaban yang tepat.

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

Anda bisa mendapatkan gambar yang jelas dan cerah. Silakan periksa

Phoenix
sumber
0

Saya benar-benar mencoba untuk menghindari menjalankan melalui data gambar, terutama pada gambar yang lebih besar. Jadi saya datang dengan cara yang agak sederhana untuk mengurangi ukuran gambar dengan sopan tanpa batasan atau batasan menggunakan beberapa langkah tambahan. Rutin ini turun ke setengah langkah serendah mungkin sebelum ukuran target yang diinginkan. Kemudian timbangan itu menjadi dua kali ukuran target dan kemudian setengah lagi. Kedengarannya lucu pada awalnya, tetapi hasilnya sangat bagus dan pergi ke sana dengan cepat.

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
sumber
-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>
moldcraft
sumber
-1

DEMO : Mengubah ukuran gambar dengan fiddler Demo Kanvas JS dan HTML.

Anda mungkin menemukan 3 metode berbeda untuk melakukan pengubahan ukuran ini, yang akan membantu Anda memahami bagaimana kode bekerja dan mengapa.

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

Kode lengkap dari kedua demo, dan metode TypeScript yang mungkin ingin Anda gunakan dalam kode Anda, dapat ditemukan di proyek GitHub.

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

Ini adalah kode terakhir:

export class ImageTools {
base64ResizedImage: string = null;

constructor() {
}

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

    img.onload = () => {

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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