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ą filter
wł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() {
const oc = document.createElement('canvas');
const octx = oc.getContext('2d');
oc.width = this.width;
oc.height = this.height;
const steps = (oc.width / canvas.width)>>1;
octx.filter = `blur(${steps}px)`;
octx.drawImage(this, 0, 0);
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:
CanvasRenderingContext2D.filter
api.CanvasRenderingContext2D.filter
On Standard Track, Experimental
https:
DESKTOP > |Chrome |Edge |Firefox |IE |Opera |Safari
:----------------|:--------:|:--------:|:--------:|:--------:|:--------:|:--------
filter ! | 52 | ? | 49 | - | - | -
MOBILE > |Chrome/A |Edge/mob |Firefox/A |Opera/A |Safari/iOS|Webview/A
:----------------|:--------:|:--------:|:--------:|:--------:|:--------:|:--------
filter ! | 52 | ? | 49 | - | - | 52
! = Experimental
Data from MDN - "npm i -g mdncomp" (c) epistemex
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
:
CanvasRenderingContext2D.imageSmoothingQuality
api.CanvasRenderingContext2D.imageSmoothingQuality
On Standard Track, Experimental
https:
DESKTOP > |Chrome |Edge |Firefox |IE |Opera |Safari
:----------------------|:--------:|:--------:|:--------:|:--------:|:--------:|:--------:
imageSmoothingQuality !| 54 | ? | - | ? | 41 | Y
MOBILE > |Chrome/A |Edge/mob |Firefox/A |Opera/A |Safari/iOS|Webview/A
:----------------------|:--------:|:--------:|:--------:|:--------:|:--------:|:--------:
imageSmoothingQuality !| 54 | ? | - | 41 | Y | 54
! = Experimental
Data from MDN - "npm i -g mdncomp" (c) epistemex
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:
Korzystanie z obniżania, jak pokazano poniżej:
W takim przypadku musisz zejść w 3 krokach:
W kroku 1 zmniejszamy obraz do połowy, używając płótna pozaekranowego:
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:
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:
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))