Bildfilter mit Canvas

Ilmari Heikkinen

Einleitung

Das HTML5-Canvas-Element kann zum Schreiben von Bildfiltern verwendet werden. Zeichnen Sie dazu ein Bild auf einen Canvas, lesen Sie die Canvas-Pixel zurück und führen Sie den Filter darauf aus. Das Ergebnis können Sie auf einen neuen Canvas schreiben.

Klingt einfach? Gut. Los geht's!

Das ursprüngliche Testbild
Ursprüngliches Test-Image

Pixel verarbeiten

Rufen Sie zuerst die Bildpixel ab:

Filters = {};
Filters.getPixels = function(img) {
var c = this.getCanvas(img.width, img.height);
var ctx = c.getContext('2d');
ctx.drawImage(img);
return ctx.getImageData(0,0,c.width,c.height);
};

Filters.getCanvas = function(w,h) {
var c = document.createElement('canvas');
c.width = w;
c.height = h;
return c;
};

Als Nächstes brauchen wir eine Möglichkeit, Bilder zu filtern. Wie wäre es mit einer filterImage-Methode, die einen Filter und ein Bild verwendet und die gefilterten Pixel zurückgibt?

Filters.filterImage = function(filter, image, var_args) {
var args = [this.getPixels(image)];
for (var i=2; i<arguments.length; i++) {
args.push(arguments[i]);
}
return filter.apply(null, args);
};

Einfache Filter ausführen

Nachdem wir die Pixelverarbeitungspipeline zusammengestellt haben, ist es an der Zeit, einige einfache Filter zu schreiben. Konvertieren wir das Bild zunächst in Graustufen.

Filters.grayscale = function(pixels, args) {
var d = pixels.data;
for (var i=0; i<d.length; i+=4) {
var r = d[i];
var g = d[i+1];
var b = d[i+2];
// CIE luminance for the RGB
// The human eye is bad at seeing red and blue, so we de-emphasize them.
var v = 0.2126*r + 0.7152*g + 0.0722*b;
d[i] = d[i+1] = d[i+2] = v
}
return pixels;
};

Sie können die Helligkeit anpassen, indem Sie den Pixeln einen festen Wert hinzufügen:

Filters.brightness = function(pixels, adjustment) {
var d = pixels.data;
for (var i=0; i<d.length; i+=4) {
d[i] += adjustment;
d[i+1] += adjustment;
d[i+2] += adjustment;
}
return pixels;
};

Außerdem lässt sich der Grenzwert eines Bildes recht einfach festlegen. Sie vergleichen einfach den Graustufenwert eines Pixels mit dem Grenzwert und legen die Farbe basierend darauf fest:

Filters.threshold = function(pixels, threshold) {
var d = pixels.data;
for (var i=0; i<d.length; i+=4) {
var r = d[i];
var g = d[i+1];
var b = d[i+2];
var v = (0.2126*r + 0.7152*g + 0.0722*b >= threshold) ? 255 : 0;
d[i] = d[i+1] = d[i+2] = v
}
return pixels;
};

Bilder drehen

Faltungsfilter sind sehr nützliche generische Filter für die Bildverarbeitung. Die Grundidee ist, dass Sie die gewogene Summe eines Rechtecks von Pixeln aus dem Quellbild als Ausgabewert verwenden. Faltungsfilter können für Unkenntlichmachungen, Schärfe, Prägungen, Kantenerkennung und eine Vielzahl anderer Dinge verwendet werden.

Filters.tmpCanvas = document.createElement('canvas');
Filters.tmpCtx = Filters.tmpCanvas.getContext('2d');

Filters.createImageData = function(w,h) {
return this.tmpCtx.createImageData(w,h);
};

Filters.convolute = function(pixels, weights, opaque) {
var side = Math.round(Math.sqrt(weights.length));
var halfSide = Math.floor(side/2);
var src = pixels.data;
var sw = pixels.width;
var sh = pixels.height;
// pad output by the convolution matrix
var w = sw;
var h = sh;
var output = Filters.createImageData(w, h);
var dst = output.data;
// go through the destination image pixels
var alphaFac = opaque ? 1 : 0;
for (var y=0; y<h; y++) {
for (var x=0; x<w; x++) {
  var sy = y;
  var sx = x;
  var dstOff = (y*w+x)*4;
  // calculate the weighed sum of the source image pixels that
  // fall under the convolution matrix
  var r=0, g=0, b=0, a=0;
  for (var cy=0; cy<side; cy++) {
    for (var cx=0; cx<side; cx++) {
      var scy = sy + cy - halfSide;
      var scx = sx + cx - halfSide;
      if (scy >= 0 && scy < sh && scx >= 0 && scx < sw) {
        var srcOff = (scy*sw+scx)*4;
        var wt = weights[cy*side+cx];
        r += src[srcOff] * wt;
        g += src[srcOff+1] * wt;
        b += src[srcOff+2] * wt;
        a += src[srcOff+3] * wt;
      }
    }
  }
  dst[dstOff] = r;
  dst[dstOff+1] = g;
  dst[dstOff+2] = b;
  dst[dstOff+3] = a + alphaFac*(255-a);
}
}
return output;
};

Hier ist ein 3:3-Filter für die Scharfstellen. Hier sehen Sie, wie die Gewichtung auf das Pixel in der Mitte fokussiert wird. Damit die Helligkeit des Bildes erhalten bleibt, sollte die Summe der Matrixwerte eins sein.

Filters.filterImage(Filters.convolute, image,
[  0, -1,  0,
-1,  5, -1,
  0, -1,  0 ]
);

Hier ist ein weiteres Beispiel für einen Faltungsfilter, das Feld-Weichzeichnen. Die Box-Blur gibt den Durchschnitt der Pixelwerte innerhalb der Faltungsmatrix aus. Dazu erstellen Sie eine Faltungsmatrix der Größe NxN mit einer Gewichtung von 1 / (NxN). Auf diese Weise fließt jedes Pixel in der Matrix in gleichem Maße zum Ausgabebild bei und die Summe der Gewichtungen ergibt eins.

Filters.filterImage(Filters.convolute, image,
[ 1/9, 1/9, 1/9,
1/9, 1/9, 1/9,
1/9, 1/9, 1/9 ]
);

Wir können komplexere Bildfilter erstellen, indem wir vorhandene Filter kombinieren. Schreiben wir beispielsweise einen Sobel-Filter. Ein Sobel-Filter berechnet die vertikalen und horizontalen Farbverläufe des Bildes und kombiniert die berechneten Bilder, um Kanten im Bild zu finden. Hier implementieren wir den Sobel-Filter, indem wir zuerst das Bild in Grau skalieren, dann die horizontalen und vertikalen Farbverläufe nehmen und schließlich die Farbverlaufsbilder zu dem endgültigen Bild kombinieren.

Im Zusammenhang mit der Terminologie bezeichnet "Farbverlauf" hier die Änderung des Pixelwerts an einer Bildposition. Wenn ein Pixel einen linken Nachbarn mit dem Wert 20 und einen rechten Nachbarn mit dem Wert 50 hat, beträgt der horizontale Farbverlauf beim Pixel 30. Der vertikale Farbverlauf hat das gleiche Prinzip, verwendet aber die obigen und die unteren benachbarten Elemente.

var grayscale = Filters.filterImage(Filter.grayscale, image);
// Note that ImageData values are clamped between 0 and 255, so we need
// to use a Float32Array for the gradient values because they
// range between -255 and 255.
var vertical = Filters.convoluteFloat32(grayscale,
[ -1, 0, 1,
-2, 0, 2,
-1, 0, 1 ]);
var horizontal = Filters.convoluteFloat32(grayscale,
[ -1, -2, -1,
  0,  0,  0,
  1,  2,  1 ]);
var final_image = Filters.createImageData(vertical.width, vertical.height);
for (var i=0; i<final_image.data.length; i+=4) {
// make the vertical gradient red
var v = Math.abs(vertical.data[i]);
final_image.data[i] = v;
// make the horizontal gradient green
var h = Math.abs(horizontal.data[i]);
final_image.data[i+1] = h;
// and mix in some blue for aesthetics
final_image.data[i+2] = (v+h)/4;
final_image.data[i+3] = 255; // opaque alpha
}

Es gibt noch eine ganze Reihe von coolen Faltungsfiltern, die nur darauf warten, dass Sie sie entdecken. Implementieren Sie beispielsweise einen Laplace-Filter im obigen Faltungsspielzeug und sehen Sie sich an, was er bewirkt.

Fazit

Ich hoffe, dass dieser kleine Artikel Ihnen eine Einführung in die grundlegenden Konzepte des Schreibens von Bildfiltern in JavaScript mithilfe des HTML-Canvas-Tags erleichtert. Wir empfehlen Ihnen, noch einige weitere Bildfilter zu implementieren. Das macht wirklich Spaß!

Wenn Sie eine bessere Leistung Ihrer Filter benötigen, können Sie sie in der Regel so portieren, dass sie für die Bildverarbeitung WebGL-Fragment-Shader verwenden. Mit Shaders können Sie die meisten einfachen Filter in Echtzeit ausführen und sie dann für die Nachbearbeitung von Videos und Animationen verwenden.