Zmiana rozmiaru obrazu w kanwie HTML5

314

Próbuję utworzyć miniaturę po stronie klienta przy użyciu javascript i elementu canvas, ale kiedy zmniejszam obraz, wygląda to okropnie. Wygląda na to, że został zmniejszony w Photoshopie z ustawieniem ponownego próbkowania na „Nearest Neighbor” zamiast na Bicubic. Wiem, że to możliwe, aby wyglądało to dobrze, ponieważ ta strona może to zrobić dobrze, używając również płótna. Próbowałem użyć tego samego kodu, który robią, jak pokazano w linku „[Źródło]”, ale nadal wygląda okropnie. Czy coś mi brakuje, jakieś ustawienie, które należy ustawić, czy coś?

EDYTOWAĆ:

Próbuję zmienić rozmiar pliku JPG. Próbowałem zmienić rozmiar tego samego pliku JPG na połączonej stronie i w Photoshopie, a po zmniejszeniu rozmiar wygląda dobrze.

Oto odpowiedni kod:

reader.onloadend = function(e)
{
    var img = new Image();
    var ctx = canvas.getContext("2d");
    var canvasCopy = document.createElement("canvas");
    var copyContext = canvasCopy.getContext("2d");

    img.onload = function()
    {
        var ratio = 1;

        if(img.width > maxWidth)
            ratio = maxWidth / img.width;
        else if(img.height > maxHeight)
            ratio = maxHeight / img.height;

        canvasCopy.width = img.width;
        canvasCopy.height = img.height;
        copyContext.drawImage(img, 0, 0);

        canvas.width = img.width * ratio;
        canvas.height = img.height * ratio;
        ctx.drawImage(canvasCopy, 0, 0, canvasCopy.width, canvasCopy.height, 0, 0, canvas.width, canvas.height);
    };

    img.src = reader.result;
}

EDYCJA 2:

Wydaje mi się, że się myliłem, połączona witryna nie radziła sobie lepiej z redukcją obrazu. Wypróbowałem inne sugerowane metody i żadna z nich nie wygląda lepiej. Oto, co spowodowały różne metody:

Photoshop:

alternatywny tekst

Brezentowy:

alternatywny tekst

Obraz z renderowaniem obrazu: zoptymalizuj Zestaw jakości i skaluj według szerokości / wysokości:

alternatywny tekst

Obraz z renderowaniem obrazu: zoptymalizuj Zestaw jakości i skaluj za pomocą -moz-transform:

alternatywny tekst

Zmiana rozmiaru płótna na pixastic:

alternatywny tekst

Myślę, że to oznacza, że ​​firefox nie używa bicubic próbkowania, jak powinien. Muszę tylko poczekać, aż faktycznie to dodadzą.

EDYCJA 3:

Oryginalny obraz

Telanor
źródło
Czy możesz opublikować kod, którego używasz do zmiany rozmiaru obrazu?
Xavi
Czy próbujesz zmienić rozmiar obrazu GIF lub podobnego przy użyciu ograniczonej palety? Nawet w Photoshopie obrazy te nie są dobrze skalowane, chyba że zostaną przekonwertowane na RGB.
leepowers
Czy możesz opublikować kopię oryginalnego obrazu?
Xavi
Zmiana rozmiaru obrazu za pomocą javascript jest trochę kłopotliwa - nie tylko używasz mocy obliczeniowej klienta do zmiany rozmiaru obrazu, ale robisz to przy każdym ładowaniu strony. Dlaczego nie zapisać w zmniejszonej wersji z Photoshopa i podawać go zamiast / w tandemie z oryginalnym obrazem?
określa
29
Ponieważ tworzę program do przesyłania zdjęć z możliwością zmiany rozmiaru i kadrowania zdjęć przed ich przesłaniem.
Telanor,

Odpowiedzi:

393

Co więc zrobisz, jeśli wszystkie przeglądarki (właściwie Chrome 5 dał mi całkiem niezłą) nie zapewnią wystarczająco dobrej jakości ponownego próbkowania? Sam je wdrażasz! Och, daj spokój, wkraczamy w nową erę Web 3.0, przeglądarek zgodnych z HTML5, super zoptymalizowanych kompilatorów Jvascript JIT, maszyn wielordzeniowych (†), z mnóstwem pamięci, czego się boisz? Hej, w javascript jest słowo java, więc powinno to gwarantować wydajność, prawda? Oto kod generujący miniaturę:

// returns a function that calculates lanczos weight
function lanczosCreate(lobes) {
    return function(x) {
        if (x > lobes)
            return 0;
        x *= Math.PI;
        if (Math.abs(x) < 1e-16)
            return 1;
        var xx = x / lobes;
        return Math.sin(x) * Math.sin(xx) / x / xx;
    };
}

// elem: canvas element, img: image element, sx: scaled width, lobes: kernel radius
function thumbnailer(elem, img, sx, lobes) {
    this.canvas = elem;
    elem.width = img.width;
    elem.height = img.height;
    elem.style.display = "none";
    this.ctx = elem.getContext("2d");
    this.ctx.drawImage(img, 0, 0);
    this.img = img;
    this.src = this.ctx.getImageData(0, 0, img.width, img.height);
    this.dest = {
        width : sx,
        height : Math.round(img.height * sx / img.width),
    };
    this.dest.data = new Array(this.dest.width * this.dest.height * 3);
    this.lanczos = lanczosCreate(lobes);
    this.ratio = img.width / sx;
    this.rcp_ratio = 2 / this.ratio;
    this.range2 = Math.ceil(this.ratio * lobes / 2);
    this.cacheLanc = {};
    this.center = {};
    this.icenter = {};
    setTimeout(this.process1, 0, this, 0);
}

thumbnailer.prototype.process1 = function(self, u) {
    self.center.x = (u + 0.5) * self.ratio;
    self.icenter.x = Math.floor(self.center.x);
    for (var v = 0; v < self.dest.height; v++) {
        self.center.y = (v + 0.5) * self.ratio;
        self.icenter.y = Math.floor(self.center.y);
        var a, r, g, b;
        a = r = g = b = 0;
        for (var i = self.icenter.x - self.range2; i <= self.icenter.x + self.range2; i++) {
            if (i < 0 || i >= self.src.width)
                continue;
            var f_x = Math.floor(1000 * Math.abs(i - self.center.x));
            if (!self.cacheLanc[f_x])
                self.cacheLanc[f_x] = {};
            for (var j = self.icenter.y - self.range2; j <= self.icenter.y + self.range2; j++) {
                if (j < 0 || j >= self.src.height)
                    continue;
                var f_y = Math.floor(1000 * Math.abs(j - self.center.y));
                if (self.cacheLanc[f_x][f_y] == undefined)
                    self.cacheLanc[f_x][f_y] = self.lanczos(Math.sqrt(Math.pow(f_x * self.rcp_ratio, 2)
                            + Math.pow(f_y * self.rcp_ratio, 2)) / 1000);
                weight = self.cacheLanc[f_x][f_y];
                if (weight > 0) {
                    var idx = (j * self.src.width + i) * 4;
                    a += weight;
                    r += weight * self.src.data[idx];
                    g += weight * self.src.data[idx + 1];
                    b += weight * self.src.data[idx + 2];
                }
            }
        }
        var idx = (v * self.dest.width + u) * 3;
        self.dest.data[idx] = r / a;
        self.dest.data[idx + 1] = g / a;
        self.dest.data[idx + 2] = b / a;
    }

    if (++u < self.dest.width)
        setTimeout(self.process1, 0, self, u);
    else
        setTimeout(self.process2, 0, self);
};
thumbnailer.prototype.process2 = function(self) {
    self.canvas.width = self.dest.width;
    self.canvas.height = self.dest.height;
    self.ctx.drawImage(self.img, 0, 0, self.dest.width, self.dest.height);
    self.src = self.ctx.getImageData(0, 0, self.dest.width, self.dest.height);
    var idx, idx2;
    for (var i = 0; i < self.dest.width; i++) {
        for (var j = 0; j < self.dest.height; j++) {
            idx = (j * self.dest.width + i) * 3;
            idx2 = (j * self.dest.width + i) * 4;
            self.src.data[idx2] = self.dest.data[idx];
            self.src.data[idx2 + 1] = self.dest.data[idx + 1];
            self.src.data[idx2 + 2] = self.dest.data[idx + 2];
        }
    }
    self.ctx.putImageData(self.src, 0, 0);
    self.canvas.style.display = "block";
};

... dzięki którym możesz uzyskać takie wyniki!

img717.imageshack.us/img717/8910/lanczos358.png

w każdym razie oto „poprawiona” wersja twojego przykładu:

img.onload = function() {
    var canvas = document.createElement("canvas");
    new thumbnailer(canvas, img, 188, 3); //this produces lanczos3
    // but feel free to raise it up to 8. Your client will appreciate
    // that the program makes full use of his machine.
    document.body.appendChild(canvas);
};

Teraz nadszedł czas, aby udostępnić swoje najlepsze przeglądarki i sprawdzić, która z nich najprawdopodobniej podniesie ciśnienie krwi Twojego klienta!

Umm, gdzie jest mój tag sarkazmu?

(ponieważ wiele części kodu jest opartych na Generatorze Galerii Anrieff, czy jest ono również objęte GPL2? Nie wiem)

tak naprawdę ze względu na ograniczenie javascript, wielordzeniowy nie jest obsługiwany.

syockit
źródło
2
Sam próbowałem go zaimplementować, robiąc to samo, co ty, kopiując kod z edytora obrazów open source. Ponieważ nie byłem w stanie znaleźć żadnej solidnej dokumentacji na temat algorytmu, trudno mi było go zoptymalizować. W końcu mój był trochę powolny (zmiana rozmiaru obrazu zajęła kilka sekund). Kiedy będę miał okazję, wypróbuję twoją i sprawdzę, czy będzie szybciej. I myślę, że webmasterzy umożliwiają teraz obsługę wielordzeniowego javascript. Zamierzałem spróbować ich użyć, aby przyspieszyć, ale miałem problem z wymyśleniem, jak zrobić z tego algorytm wielowątkowy
Telanor,
3
Przepraszam, zapomniałem o tym! Zredagowałem odpowiedź. I tak nie będzie szybko, bicubic powinien być szybszy. Nie wspominając o tym, że algorytm, którego użyłem, nie jest zwykłym dwukierunkowym zmienianiem rozmiaru (linijka po linii, poziomym, a następnie pionowym), więc jest wolniejszy.
syockit
5
Jesteś niesamowity i zasługujesz na mnóstwo niesamowitości.
Rocklan
5
Daje to przyzwoite wyniki, ale zajmuje 7,4 sekundy na zdjęcie 1,8 MP w najnowszej wersji Chrome ...
mpen
2
Jak takie metody radzą sobie z tak wysokim wynikiem? Przedstawione rozwiązanie całkowicie nie uwzględnia skali logarytmicznej używanej do przechowywania informacji o kolorze. RGB wynoszący 127 127 127 to jedna czwarta jasności 255, 255, 255, a nie połowa. Próbkowanie w dół w roztworze powoduje przyciemnienie obrazu. Szkoda, że ​​to jest zamknięte, ponieważ istnieje bardzo prosta i szybka metoda zmniejszenia rozmiaru, która daje jeszcze lepsze wyniki niż Photoshop (OP musiał mieć źle ustawione preferencje). Podano próbkę
Blindman67
37

Algorytm szybkiego zmieniania rozmiaru / ponownego próbkowania obrazu za pomocą filtra Hermite z JavaScript. Obsługuje przezroczystość, daje dobrą jakość. Zapowiedź:

wprowadź opis zdjęcia tutaj

Aktualizacja : wersja 2.0 dodana na GitHub (szybciej, pracownicy sieci + obiekty zbywalne). Wreszcie mam to działa!

Git: https://github.com/viliusle/Hermite-resize
Demo: http://viliusle.github.io/miniPaint/

/**
 * 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
Może możesz dołączyć linki do dema miniPaint i repozytorium Github?
syockit,
1
Czy udostępnisz również wersję dla webmasterów? Prawdopodobnie z powodu narzutu konfiguracji jest wolniejszy w przypadku małych obrazów, ale może być przydatny w przypadku większych obrazów źródłowych.
syockit,
dodano demo, linki git, a także wersję wielordzeniową. Przy okazji nie poświęciłem zbyt wiele czasu na optymalizację wersji wielordzeniowej ... Jedną wersję uważam za dobrze zoptymalizowaną.
ViliusL,
Ogromna różnica i przyzwoita wydajność. Dziękuję Ci bardzo! przed i po
KevBurnsJr
2
@ViliusL Ah teraz przypomniałem sobie, dlaczego pracownicy sieci nie działali tak dobrze. Nie mieli wcześniej wspólnej pamięci i nadal jej nie mają! Może kiedyś, gdy uda im się to rozwiązać, twój kod przyjdzie do użycia (to, a może ludzie zamiast tego używają PNaCl)
syockit
26

Wypróbuj pica - to wysoce zoptymalizowany resizer z wybieranymi algorytmami. Zobacz demo .

Na przykład oryginalny obraz z pierwszego postu jest zmieniany w 120ms z filtrem Lanczos i oknem 3px lub 60ms z filtrem Box i oknem 0,5px. W przypadku ogromnego obrazu 17 MB 5000 x 3000 pikseli zmiana rozmiaru zajmuje około 1 sekundy na komputerze i 3 sekundy na telefonie komórkowym.

Wszystkie zasady zmiany rozmiaru zostały bardzo dobrze opisane w tym wątku, a pica nie dodaje nauki o rakietach. Ale jest bardzo dobrze zoptymalizowany dla nowoczesnych JIT-ów i jest gotowy do użycia po wyjęciu z pudełka (npm lub altana). Korzysta także z pracowników sieci, jeśli są dostępne, aby uniknąć zawieszenia interfejsu.

Planuję również wkrótce dodać obsługę maski wyostrzającej, ponieważ jest to bardzo przydatne po zmniejszeniu skali.

Witalij
źródło
14

Wiem, że to stary wątek, ale może być przydatny dla niektórych osób, takich jak ja, że ​​kilka miesięcy później ten problem pojawia się po raz pierwszy.

Oto kod, który zmienia rozmiar obrazu przy każdym ponownym załadowaniu obrazu. Wiem, że to wcale nie jest optymalne, ale zapewniam to jako dowód koncepcji.

Przepraszam również za użycie jQuery dla prostych selektorów, ale po prostu czuję się zbyt swobodnie ze składnią.

$(document).on('ready', createImage);
$(window).on('resize', createImage);

var createImage = function(){
  var canvas = document.getElementById('myCanvas');
  canvas.width = window.innerWidth || $(window).width();
  canvas.height = window.innerHeight || $(window).height();
  var ctx = canvas.getContext('2d');
  img = new Image();
  img.addEventListener('load', function () {
    ctx.drawImage(this, 0, 0, w, h);
  });
  img.src = 'http://www.ruinvalor.com/Telanor/images/original.jpg';
};
html, body{
  height: 100%;
  width: 100%;
  margin: 0;
  padding: 0;
  background: #000;
}
canvas{
  position: absolute;
  left: 0;
  top: 0;
  z-index: 0;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Canvas Resize</title>
  </head>
  <body>
    <canvas id="myCanvas"></canvas>
  </body>
</html>

Moja funkcja createImage jest wywoływana raz, gdy dokument jest ładowany, a następnie jest wywoływana za każdym razem, gdy okno odbiera zdarzenie zmiany rozmiaru.

Przetestowałem to w Chrome 6 i Firefox 3.6, oba na Macu. Ta „technika” zjada procesor tak, jakby latem był lodem, ale działa.

cesarsalazar
źródło
9

Wprowadziłem kilka algorytmów do interpolacji obrazu na tablicach pikseli HTML, które mogą się tu przydać:

https://web.archive.org/web/20170104190425/http://jsperf.com:80/pixel-interpolation/2

Można je kopiować / wklejać i można ich używać wewnątrz robotów internetowych do zmiany rozmiaru obrazów (lub dowolnej innej operacji wymagającej interpolacji - obecnie używam ich do odrzucania obrazów).

Nie dodałem powyższych elementów lanczos, więc możesz dodać je jako porównanie, jeśli chcesz.

Daniel
źródło
6

To jest funkcja javascript zaadaptowana z kodu @ Telanor. Przekazując obraz base64 jako pierwszy argument do funkcji, zwraca base64 obrazu o zmienionym rozmiarze. maxWidth i maxHeight są opcjonalne.

function thumbnail(base64, maxWidth, maxHeight) {

  // Max size for thumbnail
  if(typeof(maxWidth) === 'undefined') var maxWidth = 500;
  if(typeof(maxHeight) === 'undefined') var maxHeight = 500;

  // Create and initialize two canvas
  var canvas = document.createElement("canvas");
  var ctx = canvas.getContext("2d");
  var canvasCopy = document.createElement("canvas");
  var copyContext = canvasCopy.getContext("2d");

  // Create original image
  var img = new Image();
  img.src = base64;

  // Determine new ratio based on max size
  var ratio = 1;
  if(img.width > maxWidth)
    ratio = maxWidth / img.width;
  else if(img.height > maxHeight)
    ratio = maxHeight / img.height;

  // Draw original image in second canvas
  canvasCopy.width = img.width;
  canvasCopy.height = img.height;
  copyContext.drawImage(img, 0, 0);

  // Copy and resize second canvas to first canvas
  canvas.width = img.width * ratio;
  canvas.height = img.height * ratio;
  ctx.drawImage(canvasCopy, 0, 0, canvasCopy.width, canvasCopy.height, 0, 0, canvas.width, canvas.height);

  return canvas.toDataURL();

}
Christophe Marois
źródło
twoje podejście jest bardzo szybkie, ale daje niewyraźny obraz, jak widać tutaj: stackoverflow.com/questions/18922880/…
confile
6

Gorąco polecam sprawdzenie tego linku i upewnienie się, że jest ustawiony na wartość true.

Kontrolowanie zachowania skalowania obrazu

Wprowadzone w Gecko 1.9.2 (Firefox 3.6 / Thunderbird 3.1 / Fennec 1.0)

Gecko 1.9.2 wprowadziło właściwość mozImageSmoothingEnabled do elementu canvas; jeśli ta wartość logiczna ma wartość false, obrazy nie zostaną wygładzone podczas skalowania. Domyślnie ta właściwość ma wartość true. zobaczyć zwykły wydruk?

  1. cx.mozImageSmoothingEnabled = false;
Evan Carroll
źródło
5

Jeśli po prostu próbujesz zmienić rozmiar obrazu, zalecam ustawienie widthi heightobrazu za pomocą CSS. Oto szybki przykład:

.small-image {
    width: 100px;
    height: 100px;
}

Pamiętaj, że heighti widthmożna również ustawić za pomocą JavaScript. Oto przykładowy kod:

var img = document.getElement("my-image");
img.style.width = 100 + "px";  // Make sure you add the "px" to the end,
img.style.height = 100 + "px"; // otherwise you'll confuse IE

Ponadto, aby upewnić się, że obraz o zmienionym rozmiarze wygląda dobrze, dodaj następujące reguły css do selektora obrazu:

O ile mi wiadomo, wszystkie przeglądarki oprócz IE używają dwububnego algorytmu do domyślnego zmieniania rozmiaru obrazów, więc obrazy o zmienionym rozmiarze powinny wyglądać dobrze w Firefox i Chrome.

Jeśli ustawienie css widthi heightnie działa, możesz chcieć grać z css transform:

Jeśli z jakiegokolwiek powodu musisz użyć płótna, pamiętaj, że można zmienić rozmiar obrazu na dwa sposoby: zmieniając rozmiar płótna za pomocą css lub rysując obraz w mniejszym rozmiarze.

Zobacz to pytanie, aby uzyskać więcej informacji.

Xavi
źródło
5
Niestety zmiana rozmiaru płótna ani rysowanie obrazu w mniejszym rozmiarze nie rozwiązuje problemu (w Chrome).
Nestor
1
Chrome 27 tworzy ładny obraz o zmienionym rozmiarze, ale nie można skopiować wyniku na płótno; próba zrobienia tego spowoduje skopiowanie oryginalnego obrazu.
syockit
4

Do zmiany rozmiaru do obrazu o szerokości mniejszej niż oryginał używam:

    function resize2(i) {
      var cc = document.createElement("canvas");
      cc.width = i.width / 2;
      cc.height = i.height / 2;
      var ctx = cc.getContext("2d");
      ctx.drawImage(i, 0, 0, cc.width, cc.height);
      return cc;
    }
    var cc = img;
    while (cc.width > 64 * 2) {
      cc = resize2(cc);
    }
    // .. than drawImage(cc, .... )

i działa =).

Yaffle
źródło
4

otrzymałem ten obraz, klikając prawym przyciskiem myszy element canvas w Firefoksie i zapisując jako.

alternatywny tekst

var img = new Image();
img.onload = function () {
    console.debug(this.width,this.height);
    var canvas = document.createElement('canvas'), ctx;
    canvas.width = 188;
    canvas.height = 150;
    document.body.appendChild(canvas);
    ctx = canvas.getContext('2d');
    ctx.drawImage(img,0,0,188,150);
};
img.src = 'original.jpg';

w każdym razie oto „poprawiona” wersja twojego przykładu:

var img = new Image();
// added cause it wasnt defined
var canvas = document.createElement("canvas");
document.body.appendChild(canvas);

var ctx = canvas.getContext("2d");
var canvasCopy = document.createElement("canvas");
// adding it to the body

document.body.appendChild(canvasCopy);

var copyContext = canvasCopy.getContext("2d");

img.onload = function()
{
        var ratio = 1;

        // defining cause it wasnt
        var maxWidth = 188,
            maxHeight = 150;

        if(img.width > maxWidth)
                ratio = maxWidth / img.width;
        else if(img.height > maxHeight)
                ratio = maxHeight / img.height;

        canvasCopy.width = img.width;
        canvasCopy.height = img.height;
        copyContext.drawImage(img, 0, 0);

        canvas.width = img.width * ratio;
        canvas.height = img.height * ratio;
        // the line to change
        // ctx.drawImage(canvasCopy, 0, 0, canvasCopy.width, canvasCopy.height, 0, 0, canvas.width, canvas.height);
        // the method signature you are using is for slicing
        ctx.drawImage(canvasCopy, 0, 0, canvas.width, canvas.height);
};

// changed for example
img.src = 'original.jpg';
Robert
źródło
Próbowałem robić to, co zrobiłeś, i to nie wychodzi tak dobrze jak twoje. Chyba że coś przeoczyłem, jedyną zmianą, którą wprowadziłeś, było użycie podpisu metody skalowania zamiast wycinania, prawda? Z jakiegoś powodu to nie działa dla mnie.
Telanor
3

Problem z niektórymi z tych rozwiązań polega na tym, że uzyskują bezpośredni dostęp do danych pikseli i przechodzą przez nie w celu przeprowadzenia próbkowania w dół. W zależności od rozmiaru obrazu może to być bardzo wymagające pod względem zasobów i lepiej byłoby użyć wewnętrznych algorytmów przeglądarki.

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

Jeśli zapętlisz się, aby zmienić rozmiar 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óbkę do połowy za każdym razem, aż osiągnie pożądany rozmiar:

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

Podziękowania dla tego postu

Jesús Carrera
źródło
2

Więc coś interesującego, co znalazłem jakiś czas temu podczas pracy z płótnem, które może być pomocne:

Aby samodzielnie zmienić rozmiar kontrolki obszaru roboczego, musisz użyć atrybutów height=""i width=""(lub canvas.width/ canvas.heightelements). Jeśli użyjesz CSS do zmiany rozmiaru płótna, to faktycznie rozciągnie (tj. Zmieni rozmiar) zawartość płótna, aby dopasować do całego płótna (zamiast po prostu zwiększać lub zmniejszać obszar płótna.

Warto spróbować spróbować narysować obraz w kontrolce obszaru roboczego z atrybutami wysokości i szerokości ustawionymi na rozmiar obrazu, a następnie użyć CSS, aby zmienić rozmiar obszaru roboczego do poszukiwanego rozmiaru. Być może wykorzystałoby to inny algorytm zmiany rozmiaru.

Należy również zauważyć, że canvas ma różne efekty w różnych przeglądarkach (a nawet w różnych wersjach różnych przeglądarek). Algorytmy i techniki stosowane w przeglądarkach mogą się z czasem zmieniać (szczególnie w przypadku Firefoxa 4 i Chrome 6, które pojawią się wkrótce, co położy duży nacisk na wydajność renderowania na płótnie).

Ponadto możesz też spróbować SVG, ponieważ prawdopodobnie używa on również innego algorytmu.

Powodzenia!

mattbasta
źródło
1
Ustawienie szerokości lub wysokości płótna za pomocą atrybutów HTML powoduje, że płótno jest czyszczone, więc nie można zmienić rozmiaru tej metody. Ponadto SVG jest przeznaczony do obsługi obrazów matematycznych. Muszę być w stanie narysować PNG i takie, aby mi nie pomogły.
Telanor,
Znalazłem (w Chrome) ustawienie wysokości i szerokości płótna oraz zmianę rozmiaru za pomocą CSS. Nawet wykonanie zmiany rozmiaru za pomocą opcji -webkit-transform zamiast szerokości / wysokości CSS nie powoduje uruchomienia interpolacji.
Nestor
2

Mam wrażenie, że moduł, który napisałem, da podobne wyniki do Photoshopa, ponieważ zachowuje dane kolorów poprzez uśrednienie ich, a nie zastosowanie algorytmu. To trochę powolne, ale dla mnie jest najlepsze, ponieważ zachowuje wszystkie dane kolorów.

https://github.com/danschumann/limby-resize/blob/master/lib/canvas_resize.js

Nie pobiera najbliższego sąsiada i nie upuszcza innych pikseli ani nie próbkuje grupy i nie przyjmuje losowej średniej. Pobiera dokładną proporcję, jaką każdy piksel źródłowy powinien wyprowadzić do piksela docelowego. Średni kolor piksela w źródle będzie średnim kolorem piksela w miejscu docelowym, którym, jak sądzę, nie będą te inne formuły.

przykład użycia znajduje się na dole https://github.com/danschumann/limby-resize

AKTUALIZACJA PAŹDZIERNIKA 2018 : Obecnie mój przykład jest bardziej akademicki niż cokolwiek innego. Webgl jest prawie w 100%, więc lepiej byłoby zmienić jego rozmiar, aby uzyskać podobne wyniki, ale szybciej. Uważam, że PICA.js to robi. -

Funkodebat
źródło
1

Przekształciłem odpowiedź @ syockit oraz podejście stopniowe w usługę Angular wielokrotnego użytku dla wszystkich zainteresowanych: https://gist.github.com/fisch0920/37bac5e741eaec60e983

Dołączyłem oba rozwiązania, ponieważ oba mają swoje zalety / wady. Metoda konwergencji lanczos ma wyższą jakość kosztem bycia wolniejszym, podczas gdy stopniowe zmniejszanie skali daje rozsądnie wyniki antyaliasingu 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
1

Szybki i prosty zmieniacz obrazów JavaScript:

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

Historia

To naprawdę jest po wielu rundach badań, czytania i prób.

Algorytm resizera używa skryptu Hermite @ ViliusL (resizer Hermite jest naprawdę najszybszy i daje dość dobrą wydajność). Rozszerzony o potrzebne funkcje.

Widelec 1 pracownika do zmiany rozmiaru, aby nie zamroził przeglądarki podczas zmiany rozmiaru, w przeciwieństwie do wszystkich innych dostępnych tam modyfikatorów JS.

Calvintwr
źródło
0

Dzięki @syockit za świetną odpowiedź. musiałem jednak sformatować trochę w następujący sposób, aby działało. Być może z powodu problemów ze skanowaniem DOM:

$(document).ready(function () {

$('img').on("load", clickA);
function clickA() {
    var img = this;
    var canvas = document.createElement("canvas");
    new thumbnailer(canvas, img, 50, 3);
    document.body.appendChild(canvas);
}

function thumbnailer(elem, img, sx, lobes) {
    this.canvas = elem;
    elem.width = img.width;
    elem.height = img.height;
    elem.style.display = "none";
    this.ctx = elem.getContext("2d");
    this.ctx.drawImage(img, 0, 0);
    this.img = img;
    this.src = this.ctx.getImageData(0, 0, img.width, img.height);
    this.dest = {
        width: sx,
        height: Math.round(img.height * sx / img.width)
    };
    this.dest.data = new Array(this.dest.width * this.dest.height * 3);
    this.lanczos = lanczosCreate(lobes);
    this.ratio = img.width / sx;
    this.rcp_ratio = 2 / this.ratio;
    this.range2 = Math.ceil(this.ratio * lobes / 2);
    this.cacheLanc = {};
    this.center = {};
    this.icenter = {};
    setTimeout(process1, 0, this, 0);
}

//returns a function that calculates lanczos weight
function lanczosCreate(lobes) {
    return function (x) {
        if (x > lobes)
            return 0;
        x *= Math.PI;
        if (Math.abs(x) < 1e-16)
            return 1
        var xx = x / lobes;
        return Math.sin(x) * Math.sin(xx) / x / xx;
    }
}

process1 = function (self, u) {
    self.center.x = (u + 0.5) * self.ratio;
    self.icenter.x = Math.floor(self.center.x);
    for (var v = 0; v < self.dest.height; v++) {
        self.center.y = (v + 0.5) * self.ratio;
        self.icenter.y = Math.floor(self.center.y);
        var a, r, g, b;
        a = r = g = b = 0;
        for (var i = self.icenter.x - self.range2; i <= self.icenter.x + self.range2; i++) {
            if (i < 0 || i >= self.src.width)
                continue;
            var f_x = Math.floor(1000 * Math.abs(i - self.center.x));
            if (!self.cacheLanc[f_x])
                self.cacheLanc[f_x] = {};
            for (var j = self.icenter.y - self.range2; j <= self.icenter.y + self.range2; j++) {
                if (j < 0 || j >= self.src.height)
                    continue;
                var f_y = Math.floor(1000 * Math.abs(j - self.center.y));
                if (self.cacheLanc[f_x][f_y] == undefined)
                    self.cacheLanc[f_x][f_y] = self.lanczos(Math.sqrt(Math.pow(f_x * self.rcp_ratio, 2) + Math.pow(f_y * self.rcp_ratio, 2)) / 1000);
                weight = self.cacheLanc[f_x][f_y];
                if (weight > 0) {
                    var idx = (j * self.src.width + i) * 4;
                    a += weight;
                    r += weight * self.src.data[idx];
                    g += weight * self.src.data[idx + 1];
                    b += weight * self.src.data[idx + 2];
                }
            }
        }
        var idx = (v * self.dest.width + u) * 3;
        self.dest.data[idx] = r / a;
        self.dest.data[idx + 1] = g / a;
        self.dest.data[idx + 2] = b / a;
    }

    if (++u < self.dest.width)
        setTimeout(process1, 0, self, u);
    else
        setTimeout(process2, 0, self);
};

process2 = function (self) {
    self.canvas.width = self.dest.width;
    self.canvas.height = self.dest.height;
    self.ctx.drawImage(self.img, 0, 0);
    self.src = self.ctx.getImageData(0, 0, self.dest.width, self.dest.height);
    var idx, idx2;
    for (var i = 0; i < self.dest.width; i++) {
        for (var j = 0; j < self.dest.height; j++) {
            idx = (j * self.dest.width + i) * 3;
            idx2 = (j * self.dest.width + i) * 4;
            self.src.data[idx2] = self.dest.data[idx];
            self.src.data[idx2 + 1] = self.dest.data[idx + 1];
            self.src.data[idx2 + 2] = self.dest.data[idx + 2];
        }
    }
    self.ctx.putImageData(self.src, 0, 0);
    self.canvas.style.display = "block";
}
});
Manish
źródło
-1

Właśnie uruchomiłem stronę porównań obok siebie i chyba że coś się ostatnio zmieniło, nie widziałem lepszej redukcji (skalowania) przy użyciu canvas względem zwykłego css. Testowałem w FF6 Mac OSX 10.7. Wciąż nieco miękki w porównaniu do oryginału.

Natknąłem się jednak na coś, co zrobiło ogromną różnicę i polegało na użyciu filtrów obrazów w przeglądarkach obsługujących płótno. W rzeczywistości można manipulować obrazami w podobny sposób jak w Photoshopie za pomocą rozmycia, wyostrzenia, nasycenia, tętnienia, skali szarości itp.

Następnie znalazłem niesamowitą wtyczkę jQuery, która sprawia, że ​​zastosowanie tych filtrów jest bardzo proste: http://codecanyon.net/item/jsmanipulate-jquery-image-manipulation-plugin/428234

Po prostu stosuję filtr wyostrzania zaraz po zmianie rozmiaru obrazu, co powinno dać pożądany efekt. Nie musiałem nawet używać elementu canvas.

Julian Dormon
źródło
-1

Szukasz innego świetnego prostego rozwiązania?

var img=document.createElement('img');
img.src=canvas.toDataURL();
$(img).css("background", backgroundColor);
$(img).width(settings.width);
$(img).height(settings.height);

To rozwiązanie wykorzysta algorytm zmiany rozmiaru przeglądarki! :)

ale500
źródło
Pytanie dotyczy próbkowania obrazu w dół, a nie tylko zmiany jego rozmiaru.
Jesús Carrera
[...] Próbuję zmienić rozmiar pliku JPG. Próbowałem zmienić rozmiar tego samego pliku JPG na połączonej stronie i w Photoshopie, a po zmniejszeniu wygląda dobrze. [...] dlaczego nie możesz użyć <img> Jesus Carrera?
ale500