Zmiana rozmiaru płótna HTML5 (zmniejszenie skali) Wysoka jakość obrazu?

149

Używam elementów kanwy html5 do zmiany rozmiaru obrazów w mojej przeglądarce. Okazuje się, że jakość jest bardzo niska. Znalazłem to: Wyłącz interpolację podczas skalowania <canvas>, ale nie pomaga to w poprawie jakości.

Poniżej znajduje się mój kod css i js, a także obraz wyskalowany w Photoshopie i skalowany w API Canvas.

Co muszę zrobić, aby uzyskać optymalną jakość podczas skalowania obrazu w przeglądarce?

Uwaga: chcę zmniejszyć duży obraz do małego, zmodyfikować kolor na płótnie i wysłać wynik z płótna na serwer.

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

Obraz zmieniony w Photoshopie:

wprowadź opis obrazu tutaj

Obraz zmieniony na płótnie:

wprowadź opis obrazu tutaj

Edytować:

Próbowałem zmniejszyć skalowanie w więcej niż jednym kroku, jak zaproponowano w:

Zmiana rozmiaru obrazu na kanwie HTML5 i drawImage w formacie HTML5 : jak zastosować antyaliasing

Oto funkcja, której użyłem:

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


}

Oto wynik, jeśli użyję rozmiaru 2 stopni w dół:

wprowadź opis obrazu tutaj

Oto wynik, jeśli użyję rozmiaru 3 stopni w dół:

wprowadź opis obrazu tutaj

Oto wynik, jeśli użyję rozmiaru 4 stopniowego:

wprowadź opis obrazu tutaj

Oto wynik, jeśli użyję rozmiaru o 20 stopni:

wprowadź opis obrazu tutaj

Uwaga: Okazuje się, że od 1 do 2 kroków następuje duża poprawa jakości obrazu, ale im więcej kroków dodasz do procesu, tym bardziej rozmyty staje się obraz.

Czy istnieje sposób na rozwiązanie problemu polegającego na tym, że obraz staje się bardziej rozmyty, im więcej kroków dodajesz?

Edytuj 2013-10-04: Wypróbowałem algorytm GameAlchemist. Oto wynik w porównaniu do Photoshopa.

Zdjęcie PhotoShop:

PhotoShop Image

Algorytm GameAlchemist:

Algorytm GameAlchemist

konfile
źródło
2
Możesz spróbować stopniowo
skalować
1
możliwy duplikat kanwy Html5 drawImage: jak zastosować antyaliasing . Zobacz, czy to nie działa. Jeśli obrazy są duże i zmniejszone do małych, musisz to zrobić krokami (patrz przykładowe obrazy w linku)
2
@confile wyłączając interpolację pogorszy sprawę. Chcesz mieć to włączone. Spójrz na link, który podałem powyżej. Pokazuję tam, jak korzystać z kroków, aby zmniejszyć większe obrazy i zachować jakość. Jak mówi Scott, chcesz przedkładać jakość nad szybkość.
1
@ Ken-AbdiasSoftware Wypróbowałem podejście, ale problem polega na tym, że im więcej rund używam do skalowania krokowego, będzie gorzej. Masz jakiś pomysł, jak to naprawić?
konfile
3
Z pewnością szanse na odtworzenie funkcjonalności drogiego profesjonalnego oprogramowania do edycji zdjęć przy użyciu HTML5 są dość nikłe? Prawdopodobnie możesz się zbliżyć (ish), ale dokładnie tak, jak to działa w Photoshopie, wyobrażałem sobie, że byłoby niemożliwe!
Liam

Odpowiedzi:

171

Ponieważ Twoim problemem jest zmniejszenie skali obrazu, nie ma sensu mówić o interpolacji - czyli o tworzeniu pikseli -. Problem polega na tym, że próbkowanie spada.

Aby zmniejszyć próbkowanie obrazu, musimy zamienić każdy kwadrat p * p pikseli w oryginalnym obrazie na pojedynczy piksel w obrazie docelowym.

Ze względu na wydajność przeglądarki wykonują bardzo proste próbkowanie w dół: aby zbudować mniejszy obraz, wybierają po prostu JEDEN piksel w źródle i używają jego wartości jako miejsca docelowego. który „zapomina” o niektórych szczegółach i dodaje szumu.

Jest jednak wyjątek od tej reguły: ponieważ próbkowanie obrazu 2X jest bardzo proste do obliczenia (średnio 4 piksele, aby jeden) i jest używane do pikseli siatkówki / HiDPI, ten przypadek jest obsługiwany prawidłowo - przeglądarka wykorzystuje 4 piksele, aby uzyskać jeden-.

ALE ... jeśli użyjesz kilkukrotnie downsamplingu 2X, napotkasz problem, że kolejne błędy zaokrąglania będą powodować zbyt duży szum.
Co gorsza, nie zawsze zmieniasz rozmiar o potęgę dwóch, a zmiana rozmiaru do najbliższej potęgi + ostatnia zmiana rozmiaru jest bardzo głośna.

To, czego szukasz, to próbkowanie w dół z dokładnością do piksela, czyli ponowne próbkowanie obrazu, które uwzględni wszystkie piksele wejściowe - niezależnie od skali -.
Aby to zrobić, musimy obliczyć, dla każdego piksela wejściowego, jego udział w jednym, dwóch lub czterech pikselach docelowych, w zależności od tego, czy skalowana projekcja pikseli wejściowych znajduje się bezpośrednio w pikselach docelowych, zachodzi na obramowanie X, obramowanie Y, czy oba .
(Schemat byłby tutaj fajny, ale ja go nie mam).

Oto przykład skali płótna w porównaniu z moją idealną skalą piksela w skali 1/3 zombata.

Zauważ, że obraz może zostać przeskalowany w Twojej przeglądarce i jest .jpegized przez SO.
Widzimy jednak, że jest znacznie mniej hałasu, zwłaszcza w trawie za wombatem i gałęziach po jego prawej stronie. Hałas w futrze sprawia, że ​​jest bardziej kontrastowy, ale wygląda na to, że ma białe włosy - w przeciwieństwie do zdjęcia źródłowego -.
Właściwy obraz jest mniej chwytliwy, ale zdecydowanie ładniejszy.

wprowadź opis obrazu tutaj

Oto kod do wykonania idealnego skalowania w dół:

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

Jest dość chciwy na pamięć, ponieważ do przechowywania pośrednich wartości obrazu docelowego potrzebny jest bufor typu float (-> jeśli policzymy płótno wyników, w tym algorytmie wykorzystamy 6 razy pamięć obrazu źródłowego).
Jest to również dość drogie, ponieważ każdy piksel źródłowy jest używany niezależnie od rozmiaru docelowego, a my musimy zapłacić za getImageData / putImageDate, również dość powolny.
Ale w tym przypadku nie ma sposobu, aby być szybszym niż przetwarzanie każdej wartości źródłowej, a sytuacja nie jest taka zła: w przypadku mojego obrazu wombata 740 * 556 przetwarzanie trwa od 30 do 40 ms.

GameAlchemist
źródło
Czy mogłoby być szybciej, gdybyś przeskalował obraz przed umieszczeniem go na płótnie?
konfile
nie rozumiem ... wydaje się, że to właśnie robię. Bufor, jak również utworzone przeze mnie płótno (resCV) mają rozmiar skalowanego obrazu. Myślę, że jedynym sposobem na przyspieszenie byłoby użycie obliczeń całkowitych podobnych do breshensam. Ale 40 ms jest powolne tylko dla gry wideo (25 kl./s), a nie dla aplikacji do rysowania.
GameAlchemist
czy widzisz szansę na przyspieszenie algorytmu przy zachowaniu jakości?
confile
1
próbowałem zaokrąglić bufor (ostatnia część algorytmu) za pomocą 0 | zamiast Mat.ceil. Jest trochę szybszy. Ale w każdym razie jest sporo narzutów z get / putImageData i znowu nie możemy uniknąć przetwarzania każdego piksela.
GameAlchemist
4
Ok, więc obejrzałem kod: byłeś bardzo blisko rozwiązania. Dwa błędy: twoje indeksy zostały zmniejszone o jeden dla tX + 1 (były + 3, + 4, + 5, + 6 zamiast +4, +5, +6, +7), a zmiana linii w rgba to mn o 4, a nie 3. Właśnie przetestowałem 4 losowe wartości, aby sprawdzić (0,1, 0,15, 0,33, 0,8) wydawało się w porządku. twoje zaktualizowane skrzypce jest tutaj: jsfiddle.net/gamealchemist/kpQyE/3
GameAlchemist
51

Szybkie ponowne próbkowanie płótna z dobrą jakością: http://jsfiddle.net/9g9Nv/442/

Aktualizacja: wersja 2.0 (szybsza, pracownicy sieciowi + obiekty do przenoszenia) - 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
źródło
Potrzebuję najlepszej jakości
confile
18
naprawiono, zmieniłem „dobry” na „najlepszy”, czy teraz jest w porządku? :RE. Z drugiej strony, jeśli chcesz możliwie najlepszej resample - użyj imagemagick.
ViliusL
@confile imgur.com można było bezpiecznie używać w jsfiddle, ale administratorzy zrobili coś nie tak? Nie widzisz dobrej jakości, ponieważ Twoja przeglądarka wyświetla błąd krytyczny CORS. (nie można używać obrazu ze zdalnych witryn)
ViliusL
OK, możesz użyć dowolnego innego obrazu PNG z przezroczystymi obszarami. Masz jakiś pomysł na to?
konfile
4
@confile miałeś rację, w niektórych przypadkach przezroczyste obrazy miały problemy w ostrych obszarach. W moim teście przegapiłem te przypadki. Naprawiono zmianę rozmiaru również naprawiono zdalną obsługę obrazu na skrzypcach: jsfiddle.net/9g9Nv/49
ViliusL
28

Sugestia 1 - przedłużyć rurociąg technologiczny

Możesz użyć step-down, jak opisuję w linkach, do których się odnosisz, ale wydaje się, że używasz ich w niewłaściwy sposób.

Zmniejszenie nie jest konieczne, aby skalować obrazy do proporcji powyżej 1: 2 (zazwyczaj, ale nie tylko). To jest miejsce, w którym musisz zrobić plik drastycznego skalowania w dół, musisz podzielić go na dwa (a rzadko więcej) etapy w zależności od zawartości obrazu (w szczególności tam, gdzie występują wysokie częstotliwości, takie jak cienkie linie).

Za każdym razem, gdy próbujesz pobrać obraz, stracisz szczegóły i informacje. Nie można oczekiwać, że wynikowy obraz będzie tak wyraźny jak oryginał.

Jeśli następnie pomniejszasz obrazy w wielu krokach, w sumie stracisz wiele informacji, a wynik będzie kiepski, jak już zauważyłeś.

Spróbuj z jednym dodatkowym krokiem lub dwoma dodatkowymi krokami.

Zwoje

W przypadku Photoshopa zauważ, że stosuje on splot po ponownym próbkowaniu obrazu, na przykład wyostrzenie. Nie chodzi tylko o interpolację bi-sześcienną, więc aby w pełni emulować Photoshopa, musimy również dodać kroki, które wykonuje Photoshop (przy domyślnej konfiguracji).

W tym przykładzie użyję mojej oryginalnej odpowiedzi, do której odwołujesz się w swoim poście, ale dodałem do niej splot wyostrzający, aby poprawić jakość jako proces postu (patrz demo na dole).

Oto kod dodawania filtra wyostrzającego (jest oparty na ogólnym filtrze splotowym - w nim umieściłem macierz wag do wyostrzania, a także współczynnik mieszania, aby dostosować wymowę efektu):

Stosowanie:

sharpen(context, width, height, mixFactor);

Plik mixFactorMa wartość pomiędzy [0.0, 1.0] i pozwala zrobić bagatelizować efektu wyostrzania - praworządności, zasada: im mniej tym mniejsza wielkość efektu jest potrzebny.

Funkcja (na podstawie tego fragmentu ):

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

Rezultatem zastosowania tej kombinacji będzie:

DEMO ONLINE TUTAJ

Zmniejsz próbkowanie i wyostrz splot

W zależności od tego, ile wyostrzenia chcesz dodać do mieszanki, możesz uzyskać efekt od domyślnego „rozmytego” do bardzo ostrego:

Odmiany wyostrzenia

Sugestia 2 - implementacja algorytmu niskiego poziomu

Jeśli chcesz uzyskać najlepszy wynik pod względem jakości, musisz zejść na niższy poziom i rozważyć wdrożenie na przykład tego zupełnie nowego algorytmu, aby to zrobić.

Zobacz próbkowanie obrazu zależne od interpolacji (2011) z IEEE.
Tutaj jest link do pełnego artykułu (PDF) .

W tej chwili nie ma implementacji tego algorytmu w JavaScript AFAIK, więc jeśli chcesz rzucić się w wir tego zadania, możesz mieć wiele do zrobienia.

Istota to (fragmenty artykułu):

Abstrakcyjny

W tym artykule zaproponowano adaptacyjny algorytm próbkowania w dół zorientowany na interpolację dla kodowania obrazów o niskiej przepływności. Mając obraz, proponowany algorytm jest w stanie uzyskać obraz o niskiej rozdzielczości, z którego można interpolować obraz wysokiej jakości o tej samej rozdzielczości co obraz wejściowy. W odróżnieniu od tradycyjnych algorytmów próbkowania w dół, które są niezależne od procesu interpolacji, proponowany algorytm próbkowania w dół uzależnia próbkowanie w dół do procesu interpolacji. W konsekwencji zaproponowany algorytm próbkowania w dół jest w stanie w największym stopniu zachować pierwotne informacje obrazu wejściowego. Pobierany w dół obraz jest następnie przesyłany do formatu JPEG. Przetwarzanie końcowe oparte na całkowitej wariacji (TV) jest następnie stosowane do zdekompresowanego obrazu o niskiej rozdzielczości. Ostatecznie,Wyniki eksperymentalne potwierdzają, że wykorzystując obraz próbkowany w dół przez proponowany algorytm, można uzyskać interpolowany obraz o znacznie wyższej jakości. Poza tym proponowany algorytm jest w stanie osiągnąć lepszą wydajność niż JPEG przy kodowaniu obrazów o niskiej przepływności.

Migawka z papieru

(zobacz podany link, aby uzyskać wszystkie szczegóły, formuły itp.)

Społeczność
źródło
To też świetne rozwiązanie. Dziękuję Ci!
konfile
To świetne rozwiązanie. Wypróbowałem to na plikach png z przezroczystymi obszarami. Oto wynik: jsfiddle.net/confile/5CD4N Czy masz pomysł, co zrobić, aby to zadziałało?
konfile
1
to jest GENIUSZ! ale czy możesz wyjaśnić, co dokładnie robisz? lol .. całkowicie chcę poznać tajniki ... może zasoby do nauki?
carinlynchin
1
@Carine to może być trochę za słabe pole komentarza :), ale skalowanie w dół powoduje ponowne próbkowanie grupy pikseli, aby uśrednić nową reprezentującą tę grupę. W efekcie jest to filtr dolnoprzepustowy, który ogólnie wprowadza pewne rozmycie. Aby skompensować utratę ostrości, po prostu zastosuj splot wyostrzający. Ponieważ wyostrzenie może być bardzo wyraźne, możemy zamiast tego zmieszać je z obrazem, aby kontrolować poziom wyostrzenia. Mam nadzieję, że to daje pewien wgląd.
21

Jeśli chcesz używać tylko płótna, najlepszym rezultatem będzie kilka kroków w dół. Ale to jeszcze nie wystarczy. Aby uzyskać lepszą jakość, potrzebujesz czystej implementacji js. Właśnie wydaliśmy pica - szybki downscaler ze zmienną jakością / prędkością. Krótko mówiąc, zmienia rozmiar 1280 * 1024 pikseli w ~ 0,1 s i obrazu 5000 * 3000 pikseli w 1 s, z najwyższą jakością (filtr lanczos z 3 płatami). Pica ma wersję demonstracyjną , w której możesz bawić się swoimi obrazami, poziomami jakości, a nawet wypróbować na urządzeniach mobilnych.

Pica nie ma jeszcze nieostrej maski, ale zostanie ona dodana wkrótce. To znacznie łatwiejsze niż zaimplementowanie filtra splotu o dużej szybkości w celu zmiany rozmiaru.

Witalij
źródło
16

Po co używać płótna do zmiany rozmiaru obrazów? Wszystkie współczesne przeglądarki używają interpolacji bicubic - tego samego procesu, którego używa Photoshop (jeśli robisz to dobrze) - i robią to szybciej niż proces płótna. Po prostu określ żądany rozmiar obrazu (użyj tylko jednego wymiaru, wysokości lub szerokości, aby zmienić rozmiar proporcjonalnie).

Jest to obsługiwane przez większość przeglądarek, w tym nowsze wersje IE. Wcześniejsze wersje mogą wymagać CSS dla konkretnej przeglądarki .

Prosta funkcja (wykorzystująca jQuery) do zmiany rozmiaru obrazu wyglądałaby następująco:

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

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

Następnie użyj zwróconej wartości, aby zmienić rozmiar obrazu w jednym lub obu wymiarach.

Oczywiście są różne udoskonalenia, które możesz wprowadzić, ale to spełnia swoje zadanie.

Wklej następujący kod do konsoli tej strony i obserwuj, co stanie się z gravatarami:

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
źródło
2
Zwróć również uwagę, że jeśli określisz tylko jeden wymiar, (nowoczesna) przeglądarka automatycznie zachowa naturalne proporcje obrazu.
André Dion
38
Może musi wysłać obraz o zmienionym rozmiarze na serwer.
Sergiu Paraschiv
2
@Sergiu: Nie jest to konieczne, ale pamiętaj, że jeśli przechodzisz od bardzo małego obrazu do bardzo dużego, nie uzyskasz świetnych wyników nawet z serwera.
Robusto
2
@Robusto Muszę później umieścić obraz na płótnie i wysłać go później na serwer. Chcę zmniejszyć duży obraz do małego, zmodyfikować kolor na płótnie i wysłać wynik na serwer. Jak myślisz, co powinienem zrobić?
konfile
9
@Robusto To jest problem. Pokazywanie małego obrazu na kliencie jest łatwe. img.width nad img.height jest takie trywialne. Chcę go zmniejszyć tylko raz, a nie ponownie na serwerze.
konfile
8

Nie jest to dobra odpowiedź dla osób, które naprawdę potrzebują zmienić rozmiar samego obrazu, ale po prostu zmniejszyć rozmiar pliku .

Miałem problem ze zdjęciami „prosto z aparatu”, które moi klienci często przesyłali w „nieskompresowanym” formacie JPEG.

Nie tak dobrze wiadomo, że płótno obsługuje (w większości przeglądarek 2017) zmianę jakości JPEG

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

Dzięki tej sztuczce mógłbym zmniejszyć zdjęcia 4k x 3k z> 10Mb do 1 lub 2Mb, z pewnością zależy to od twoich potrzeb.

Popatrz tutaj

półbito
źródło
4

Oto usługa Angular wielokrotnego użytku do zmiany rozmiaru obrazu / płótna w wysokiej jakości: https://gist.github.com/fisch0920/37bac5e741eaec60e983

Usługa obsługuje splot lanczosa i stopniowe zmniejszanie skali. Podejście splotowe zapewnia wyższą jakość kosztem wolniejszego działania, podczas gdy podejście stopniowego zmniejszania skali daje rozsądnie antyaliasowane wyniki i jest znacznie szybsze.

Przykładowe użycie:

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
źródło
4

To jest ulepszony filtr zmiany rozmiaru Hermite, który wykorzystuje 1 pracownika, dzięki czemu okno nie zamarza.

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
źródło
3

Znalazłem rozwiązanie, które nie wymaga bezpośredniego dostępu do danych pikseli i ich zapętlania, aby wykonać downsampling. W zależności od rozmiaru obrazu może to wymagać dużych zasobów i lepiej byłoby użyć wewnętrznych algorytmów przeglądarki.

Funkcja drawImage () wykorzystuje metodę ponownego próbkowania z interpolacją liniową i metodą najbliższego sąsiada. Że działa dobrze, gdy nie jest zmiana rozmiaru w dół więcej niż połowę pierwotnego rozmiaru .

Jeśli zrobisz pętlę, aby zmienić rozmiar tylko maksymalnie o połowę na raz, wyniki byłyby całkiem dobre i znacznie szybsze niż dostęp do danych pikseli.

Ta funkcja zmniejsza próbkowanie o połowę, aż do osiągnięcia pożądanego rozmiaru:

  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
źródło
Czy mógłbyś opublikować plik jsfiddle i kilka wynikowych obrazów?
konfile
W linku na dole można znaleźć obrazy powstałe przy użyciu tej techniki
Jesús Carrera
1

Może ty możesz spróbować tego, co zawsze używam w swoim projekcie, dzięki czemu możesz nie tylko uzyskać wysokiej jakości obraz, ale każdy inny element na swoim płótnie.

/* 
 * @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
źródło
0

zamiast 0,85 , jeśli dodamy 1,0 . Otrzymasz dokładną odpowiedź.

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

Możesz uzyskać wyraźny i jasny obraz. Proszę sprawdzić

Feniks
źródło
0

Naprawdę staram się unikać przeglądania danych obrazu, szczególnie w przypadku większych obrazów. W ten sposób wymyśliłem dość prosty sposób na przyzwoite zmniejszenie rozmiaru obrazu bez żadnych ograniczeń i ograniczeń za pomocą kilku dodatkowych kroków. Ta procedura sprowadza się do najniższego możliwego pół kroku przed pożądanym rozmiarem docelowym. Następnie skaluje go do dwukrotności rozmiaru docelowego, a następnie ponownie o połowę. Na początku brzmi śmiesznie, ale wyniki są zdumiewająco dobre i szybko się udają.

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
źródło
-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
źródło
-1

DEMO : Zmiana rozmiaru obrazów za pomocą skrzynek JS i HTML Canvas Demo.

Możesz znaleźć 3 różne metody zmiany rozmiaru, które pomogą ci zrozumieć, jak działa kod i dlaczego.

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

Pełny kod zarówno demonstracji, jak i metody TypeScript, której możesz chcieć użyć w swoim kodzie, można znaleźć w projekcie GitHub.

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

To jest ostateczny 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
        }
    };
}}
Eyal c
źródło