Html5 canvas drawImage: jak zastosować antyaliasing

82

Proszę spojrzeć na następujący przykład:

http://jsfiddle.net/MLGr4/47/

var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");

img = new Image();
img.onload = function(){
    canvas.width = 400;
    canvas.height = 150;
    ctx.drawImage(img, 0, 0, img.width, img.height, 0, 0, 400, 150);
}
img.src = "http://openwalls.com/image/1734/colored_lines_on_blue_background_1920x1200.jpg";

Jak widać, obraz nie jest wygładzany, chociaż mówi się, że drawImage automatycznie stosuje wygładzanie. Próbowałem na wiele różnych sposobów, ale to nie działa. Czy możesz mi powiedzieć, jak mogę uzyskać wygładzony obraz? Dzięki.

Dundar
źródło

Odpowiedzi:

178

Przyczyna

Niektóre obrazy są po prostu bardzo trudne do próbkowania i interpolacji, na przykład ten z krzywymi, gdy chcesz przejść od dużego do małego.

Wygląda na to, że przeglądarki zazwyczaj używają interpolacji bi-liniowej (próbkowanie 2x2) z elementem canvas, a nie bi-sześciennej (próbkowanie 4x4) z (prawdopodobnych) powodów wydajnościowych.

Jeśli krok jest zbyt duży, po prostu nie ma wystarczającej liczby pikseli do próbkowania, co znajduje odzwierciedlenie w wyniku.

Z perspektywy sygnału / procesora DSP można to postrzegać jako zbyt wysoką wartość progową filtru dolnoprzepustowego, co może skutkować aliasowaniem, jeśli w sygnale występuje wiele wysokich częstotliwości (szczegółów).

Rozwiązanie

Aktualizacja 2018:

Oto fajna sztuczka, której możesz użyć w przeglądarkach, które obsługują filterwłaściwość w kontekście 2D. To wstępnie rozmywa obraz, co w istocie jest tym samym, co resampling, a następnie skaluje się w dół. Pozwala to na duże kroki, ale wymaga tylko dwóch kroków i dwóch losowań.

Rozmycie wstępne, używając liczby kroków (rozmiar oryginalny / rozmiar docelowy / 2) jako promienia (może być konieczne dostosowanie tego heurystycznego w oparciu o przeglądarkę i kroki nieparzyste / parzyste - tutaj pokazano tylko uproszczone):

const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");

if (typeof ctx.filter === "undefined") {
 alert("Sorry, the browser doesn't support Context2D filters.")
}

const img = new Image;
img.onload = function() {

  // step 1
  const oc = document.createElement('canvas');
  const octx = oc.getContext('2d');
  oc.width = this.width;
  oc.height = this.height;

  // steo 2: pre-filter image using steps as radius
  const steps = (oc.width / canvas.width)>>1;
  octx.filter = `blur(${steps}px)`;
  octx.drawImage(this, 0, 0);

  // step 3, draw scaled
  ctx.drawImage(oc, 0, 0, oc.width, oc.height, 0, 0, canvas.width, canvas.height);

}
img.src = "//i.stack.imgur.com/cYfuM.jpg";
body{ background-color: ivory; }
canvas{border:1px solid red;}
<br/><p>Original was 1600x1200, reduced to 400x300 canvas</p><br/>
<canvas id="canvas" width=400 height=250></canvas>

Obsługa filtra jako ogf Oct / 2018:

Aktualizacja 2017: W specyfikacji została zdefiniowana nowa właściwość do ustawiania jakości ponownego próbkowania:

context.imageSmoothingQuality = "low|medium|high"

Obecnie jest obsługiwany tylko w Chrome. Decyzję o metodach stosowanych na poziomie pozostawia się sprzedawcy, ale rozsądnie jest założyć, że Lanczos ma „wysoką” lub równoważną jakość. Oznacza to, że obniżanie może zostać całkowicie pominięte lub można zastosować większe kroki z mniejszą liczbą przerysowań, w zależności od rozmiaru obrazu i

Wsparcie dla imageSmoothingQuality:

przeglądarka. Do tego czasu ...:
Koniec transmisji

Rozwiązaniem jest użycie obniżenia, aby uzyskać właściwy wynik. Zmniejszenie oznacza stopniowe zmniejszanie rozmiaru, aby umożliwić ograniczonemu zakresowi interpolacji pokrycie wystarczającej liczby pikseli do próbkowania.

Pozwoli to na dobre wyniki również w przypadku interpolacji bi-liniowej (w rzeczywistości zachowuje się ona podobnie jak bi-sześcienna), a narzut jest minimalny, ponieważ w każdym kroku jest mniej pikseli do próbkowania.

Idealnym krokiem jest przejście do połowy rozdzielczości w każdym kroku, aż do ustawienia docelowego rozmiaru (dzięki Joe Mabel za wspomnienie o tym!).

Zmodyfikowane skrzypce

Używanie bezpośredniego skalowania jak w pierwotnym pytaniu:

NORMALNY OBRAZ W MNIEJSZYM OBRAZU

Korzystanie z obniżania, jak pokazano poniżej:

OBRAZ W DÓŁ

W takim przypadku musisz zejść w 3 krokach:

W kroku 1 zmniejszamy obraz do połowy, używając płótna pozaekranowego:

// step 1 - create off-screen canvas
var oc   = document.createElement('canvas'),
    octx = oc.getContext('2d');

oc.width  = img.width  * 0.5;
oc.height = img.height * 0.5;

octx.drawImage(img, 0, 0, oc.width, oc.height);

Krok 2 ponownie wykorzystuje obszar roboczy poza ekranem i ponownie rysuje obraz zmniejszony do połowy:

// step 2
octx.drawImage(oc, 0, 0, oc.width * 0.5, oc.height * 0.5);

I jeszcze raz rysujemy na głównym płótnie, ponownie zmniejszonym do połowy, ale do ostatecznego rozmiaru:

// step 3
ctx.drawImage(oc, 0, 0, oc.width * 0.5, oc.height * 0.5,
                  0, 0, canvas.width,   canvas.height);

Wskazówka:

Możesz obliczyć całkowitą liczbę potrzebnych kroków, korzystając z tego wzoru (zawiera ostatni krok ustawiania rozmiaru docelowego):

steps = Math.ceil(Math.log(sourceWidth / targetWidth) / Math.log(2))

źródło
4
Pracując z kilkoma bardzo dużymi obrazami początkowymi (8000 x 6000 i większe), uważam za przydatne powtórzenie kroku 2, aż osiągnę współczynnik 2 żądanego rozmiaru.
Joe Mabel
Działa jak marzenie! Dzięki!
Vlad Tsepelev
1
Jestem zdezorientowany różnicą między krokiem 2 i 3 ... czy ktoś może wyjaśnić?
carinlynchin
1
@Carine to trochę skomplikowane, ale płótno próbuje zapisać plik PNG tak szybko, jak to możliwe. Plik png obsługuje wewnętrznie 5 różnych typów filtrów, które mogą poprawić kompresję (gzip), ale aby znaleźć najlepszą kombinację, wszystkie te filtry muszą być przetestowane w każdym wierszu obrazu. Byłoby to czasochłonne w przypadku dużych obrazów i mogłoby zablokować przeglądarkę, więc większość przeglądarek po prostu używa filtru 0 i wypycha go w nadziei uzyskania pewnej kompresji. Możesz wykonać ten proces ręcznie, ale oczywiście wymaga to trochę więcej pracy. Lub uruchom go za pomocą interfejsów API usług, takich jak tinypng.com.
1
@Kaiido nie zostało zapomniane, a „kopiowanie” jest bardzo wolne. Jeśli potrzebujesz przezroczystości, szybciej możesz użyć clearRect () i main lub alt. płótno jako cel.
12

Gorąco polecam pica do takich zadań. Jego jakość przewyższa wielokrotne zmniejszanie rozmiaru i jest jednocześnie dość szybka. Oto demo .

lawina 1
źródło
4
    var getBase64Image = function(img, quality) {
    var canvas = document.createElement("canvas");
    canvas.width = img.width;
    canvas.height = img.height;
    var ctx = canvas.getContext("2d");

    //----- origin draw ---
    ctx.drawImage(img, 0, 0, img.width, img.height);

    //------ reduced draw ---
    var canvas2 = document.createElement("canvas");
    canvas2.width = img.width * quality;
    canvas2.height = img.height * quality;
    var ctx2 = canvas2.getContext("2d");
    ctx2.drawImage(canvas, 0, 0, img.width * quality, img.height * quality);

    // -- back from reduced draw ---
    ctx.drawImage(canvas2, 0, 0, img.width, img.height);

    var dataURL = canvas.toDataURL("image/png");
    return dataURL;
    // return dataURL.replace(/^data:image\/(png|jpg);base64,/, "");
}
Kamil
źródło
1
jaki jest zakres wartości parametru „jakość”?
serup
między zerem a jedynką [0, 1]
Iván Rodríguez
4

Jako dodatek do odpowiedzi Kena, oto inne rozwiązanie, aby wykonać downsampling w połowie (więc wynik wygląda dobrze przy użyciu algorytmu przeglądarki):

  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
3

W przypadku, gdy ktoś jeszcze szuka odpowiedzi, istnieje inny sposób, w jaki możesz użyć obrazka tła zamiast drawImage (). W ten sposób nie stracisz żadnej jakości obrazu.

JS:

    var canvas=document.getElementById("canvas");
    var ctx=canvas.getContext("2d");
   var url = "http://openwalls.com/image/17342/colored_lines_on_blue_background_1920x1200.jpg";

    img=new Image();
    img.onload=function(){

        canvas.style.backgroundImage = "url(\'" + url + "\')"

    }
    img.src="http://openwalls.com/image/17342/colored_lines_on_blue_background_1920x1200.jpg";

działające demo

Munkhjargal Narmandakh
źródło
2

Stworzyłem usługę Angular wielokrotnego użytku do obsługi wysokiej jakości zmiany rozmiaru obrazów dla każdego, kto jest zainteresowany: https://gist.github.com/fisch0920/37bac5e741eaec60e983

Usługa obejmuje stopniowe zmniejszanie skali Kena, a także zmodyfikowaną wersję podejścia splotu lanczosa, które można znaleźć tutaj .

Uwzględniłem oba rozwiązania, ponieważ oba mają swoje wady / zalety. Podejście splotu Lanczosa zapewnia wyższą jakość kosztem wolniejszego, 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