Html5 canvas drawImage: Anwenden von Antialiasing

81

Bitte schauen Sie sich das folgende Beispiel an:

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

Wie Sie sehen, ist das Bild kein Anti-Aliasing, obwohl drawImage das Anti-Aliasing automatisch anwendet. Ich habe viele verschiedene Möglichkeiten ausprobiert, aber es scheint nicht zu funktionieren. Könnten Sie mir bitte sagen, wie ich ein Anti-Aliasing-Image erhalten kann? Vielen Dank.

Dundar
quelle

Antworten:

174

Ursache

Einige Bilder sind nur sehr schwer herunterzusampeln und zu interpolieren, wie dieses mit Kurven, wenn Sie von einer großen zu einer kleinen Größe wechseln möchten.

Browser scheinen aus (wahrscheinlichen) Leistungsgründen in der Regel eine bi-lineare Interpolation (2x2-Abtastung) mit dem Canvas-Element anstelle einer bi-kubischen (4x4-Abtastung) zu verwenden.

Wenn der Schritt zu groß ist, gibt es einfach nicht genügend Pixel zum Abtasten, von denen sich das Ergebnis widerspiegelt.

Aus Signal- / DSP-Sicht könnte dies als zu hoher Schwellenwert eines Tiefpassfilters angesehen werden, was zu einem Aliasing führen kann, wenn das Signal viele hohe Frequenzen (Details) enthält.

Lösung

Update 2018:

Hier ist ein netter Trick, den Sie für Browser verwenden können, der die filterEigenschaft im 2D-Kontext unterstützt. Dadurch wird das Bild vorab verwischt, was im Wesentlichen dem Resampling entspricht, und dann verkleinert. Dies ermöglicht große Schritte, benötigt jedoch nur zwei Schritte und zwei Ziehungen.

Vorunschärfe mit Anzahl der Schritte (Originalgröße / Zielgröße / 2) als Radius (möglicherweise müssen Sie diese heuristisch basierend auf dem Browser und ungeraden / geraden Schritten anpassen - hier nur vereinfacht dargestellt):

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>

Unterstützung für Filter ab Oktober 2018:

Update 2017: In den Spezifikationen ist jetzt eine neue Eigenschaft zum Festlegen der Resampling-Qualität definiert:

context.imageSmoothingQuality = "low|medium|high"

Es wird derzeit nur in Chrome unterstützt. Die tatsächlichen Methoden, die pro Stufe verwendet werden, sind dem Anbieter überlassen, aber es ist vernünftig, Lanczos für "hoch" oder eine gleichwertige Qualität anzunehmen. Dies bedeutet, dass das Herunterschalten insgesamt übersprungen werden kann oder größere Schritte mit weniger Neuzeichnungen verwendet werden können, abhängig von der Bildgröße und

Unterstützung für imageSmoothingQuality:

Browser. Bis dahin ..:
Ende der Übertragung

Die Lösung besteht darin, Step-Down zu verwenden, um ein korrektes Ergebnis zu erzielen. Step-down bedeutet, dass Sie die Größe schrittweise reduzieren, damit der begrenzte Interpolationsbereich genügend Pixel für die Abtastung abdeckt.

Dies ermöglicht auch bei der bi-linearen Interpolation gute Ergebnisse (sie verhält sich dabei tatsächlich ähnlich wie die bi-kubische Interpolation), und der Overhead ist minimal, da in jedem Schritt weniger Pixel abgetastet werden müssen.

Der ideale Schritt besteht darin, in jedem Schritt die Hälfte der Auflösung zu wählen, bis Sie die Zielgröße festgelegt haben (danke an Joe Mabel für die Erwähnung!).

Geänderte Geige

Verwenden der direkten Skalierung wie in der ursprünglichen Frage:

NORMALES DOWN-SCALED-BILD

Verwenden Sie Step-Down wie unten gezeigt:

ABGESCHRITTENES BILD

In diesem Fall müssen Sie in drei Schritten zurücktreten:

In Schritt 1 reduzieren wir das Bild auf die Hälfte, indem wir eine Leinwand außerhalb des Bildschirms verwenden:

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

In Schritt 2 wird die Leinwand außerhalb des Bildschirms wiederverwendet und das auf die Hälfte reduzierte Bild erneut gezeichnet:

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

Und wir zeichnen noch einmal auf die Hauptleinwand, wieder auf die Hälfte reduziert , aber auf die endgültige Größe:

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

Trinkgeld:

Mit dieser Formel können Sie die Gesamtzahl der erforderlichen Schritte berechnen (sie enthält den letzten Schritt zum Festlegen der Zielgröße):

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

quelle
4
Wenn ich mit einigen sehr großen Anfangsbildern (8000 x 6000 und höher) arbeite, finde ich es nützlich, Schritt 2 grundsätzlich zu wiederholen, bis ich innerhalb eines Faktors von 2 der gewünschten Größe bin.
Joe Mabel
Klappt wunderbar! Danke!
Vlad Tsepelev
1
Ich bin verwirrt über den Unterschied zwischen dem 2. und 3. Schritt ... kann jemand erklären?
Carinlynchin
1
@Carine es ist ein bisschen kompliziert, aber Canvas versucht, ein PNG so schnell wie möglich zu speichern. Die PNG-Datei unterstützt intern 5 verschiedene Filtertypen, die sich bei der Komprimierung (gzip) verbessern können. Um jedoch die beste Kombination zu finden, müssen alle diese Filter pro Bildzeile getestet werden. Dies wäre für große Bilder zeitaufwändig und könnte den Browser blockieren. Daher verwenden die meisten Browser nur Filter 0 und drücken ihn heraus, in der Hoffnung, eine gewisse Komprimierung zu erzielen. Sie können diesen Vorgang manuell ausführen, aber es ist offensichtlich etwas mehr Arbeit. Oder führen Sie es über Service-APIs wie die von tinypng.com aus.
1
@ Kaiido es wird nicht vergessen und "Kopieren" ist sehr langsam. Wenn Sie Transparenz benötigen, ist es schneller, clearRect () zu verwenden und main oder alt zu verwenden. Leinwand als Ziel.
12

Ich kann pica für solche Aufgaben nur empfehlen . Seine Qualität ist dem mehrfachen Downsizing überlegen und gleichzeitig recht schnell. Hier ist eine Demo .

Lawine1
quelle
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
quelle
1
Was ist der Wertebereich des Parameters 'Qualität'?
Serup
zwischen null und eins [0, 1]
Iván Rodríguez
4

Zusätzlich zu Kens Antwort hier eine weitere Lösung, um das Downsampling in zwei Hälften durchzuführen (damit das Ergebnis mit dem Algorithmus des Browsers gut aussieht):

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

Falls noch jemand nach einer Antwort sucht, gibt es eine andere Möglichkeit, das Hintergrundbild anstelle von drawImage () zu verwenden. Auf diese Weise verlieren Sie keine Bildqualität.

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

Arbeitsdemo

Munkhjargal Narmandakh
quelle
2

Ich habe einen wiederverwendbaren Angular-Dienst erstellt, um die Größe von Bildern in hoher Qualität für alle Interessierten zu ändern : https://gist.github.com/fisch0920/37bac5e741eaec60e983

Der Service umfasst Kens schrittweise downscaling Ansatz sowie eine modifizierte Version des lanczos Faltung Ansatz gefunden hier .

Ich habe beide Lösungen aufgenommen, weil beide ihre eigenen Vor- und Nachteile haben. Der lanczos-Faltungsansatz bietet eine höhere Qualität auf Kosten der Langsamkeit, während der schrittweise Downscaling-Ansatz einigermaßen antialiasierte Ergebnisse liefert und erheblich schneller ist.

Anwendungsbeispiel:

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
quelle