Pixelmanipulation

Pixelmanipulation

Die Methoden unserer Wahl, um Pixelwerte zu lesen und zu manipulieren, lauten getImageData(), putImageData() und createImageData(). Nachdem in allen dreien der Begriff ImageData heraussticht, gilt es diesen als ersten zu definieren.

Arbeiten mit dem ImageData-Objekt

Nähern wir uns dem ImageData-Objekt mit einem 2 x 2 Pixel großen Canvas, auf den wir vier 1 x 1 Pixel große, gefüllte Rechtecke in den benannten Farben navy, teal, lime und yellow zeichnen.

context.fillStyle = "navy";
context.fillRect(0,0,1,1);
context.fillStyle = "teal";
context.fillRect(1,0,1,1);
context.fillStyle = "lime";
context.fillRect(0,1,1,1);
context.fillStyle = "yellow";
context.fillRect(1,1,1,1);

Über die Methode getImageData(sx, sy, sw, sh) greifen wir im nächsten Schritt auf das ImageData-Objekt zu, wobei die vier Argumente den gewünschten Canvas-Ausschnitt als Rechteck festlegen.

ImageData = context.getImageData(
0,0,canvas.width,canvas.height
);

Das ImageData-Objekt selbst besitzt die Attribute ImageData.width, ImageData.height und ImageData.data, wobei sich hinter Letzterem die tatsächlichen Pixelwerte im sogenannten CanvasPixelArray verstecken. Dabei handelt es sich um ein flaches Array mit Rot-, Grün-, Blau- und Alpha-Werten für jedes Pixel im gewählten Ausschnitt, beginnend links oben, von links nach rechts und von oben nach unten. Die Anzahl aller Werte ist im Attribut ImageData.data.length gespeichert.

Mithilfe einer einfachen for-Schleife können wir nun die einzelnen Werte des CanvasPixelArray auslesen und mit alert() sichtbar machen. Beginnend bei 0, arbeiten wir uns von Pixel zu Pixel vor, indem wir nach jedem Schleifendurchgang den Zähler um 4 erhöhen. Die RGBA-Werte ergeben sich dann über Offsets von der aktuellen Position aus, wobei Rot beim Zähler i, Grün bei i+1, Blau bei i+2 und die Alpha-Komponente bei i+3 zu finden ist.

for (var i=0; i<ImageData.data.length; i+=4) {
var r = ImageData.data[i];
var g = ImageData.data[i+1];
var b = ImageData.data[i+2];
var a = ImageData.data[i+3];
alert(r+" "+g+" "+b+" "+a);
}

Genau demselben Prinzip folgt das Modifizieren von Pixelwerten, indem wir jetzt das CanvasPixelArray in-place durch Zuweisung neuer Werte verändern. In unserem Beispiel werden die RGB-Werte mit Math.random() auf Zufallszahlen zwischen 0 und 255 gesetzt; die Alpha-Komponente bleibt unberührt.

for (var i=0; i<ImageData.data.length; i+=4) {
ImageData.data[i] = parseInt(Math.random()*255);
ImageData.data[i+1] = parseInt(Math.random()*255);
ImageData.data[i+2] = parseInt(Math.random()*255);
}

Der Canvas erscheint nach diesem Schritt allerdings noch unverändert. Erst durch Zurückschreiben des modifizierten CanvasPixelArray über die Methode putImageData() werden die neuen Farben sichtbar. Beim Aufruf von putImageData() sind maximal sieben Parameter erlaubt.

context.putImageData(
ImageData, dx, dy, [ dirtyX, dirtY, dirtyWidth, dirtyHeight ]
)

Die ersten drei Argumente sind verpflichtend anzugeben und beinhalten neben dem ImageData-Objekt die Koordinate des Ursprungspunktes dx/dy, von dem aus das CanvasPixelArray über seine width- und height-Attribute aufgetragen wird. Die optionalen dirty-Parameter dienen dazu, einen bestimmten Bereich des CanvasPixelArray auszuschneiden und nur diesen mit reduzierter Breite und Höhe zurückzuschreiben. Abbildung 1 zeigt unseren 4-Pixel-Canvas vor und nach der Modifikation und listet die jeweiligen Werte des CanvasPixelArray auf.

pixelmanipulation_01

Auf direktem Weg lässt sich ein leeres ImageData-Objekt über die Methode createImageData() initialisieren. Breite und Höhe entsprechen dabei den Argumenten sw/sh oder den Dimensionen eines beim Aufruf übergebenen ImageDataObjekts. In beiden Fällen werden alle Pixel des CanvasPixelArray auf transparent/schwarz, also rgba(0,0,0,0), gesetzt.

context.createImageData(sw, sh)
context.createImageData(imagedata)

Den 2 x 2 Pixel großen, modifizierten Canvas in Abbildung 1 könnten wir mithilfe von createImageData() demnach auch direkt erzeugen und über putImageData() zeichnen:

var imagedata = context.createImageData(2,2);
for (var i=0; i<ImageData.data.length; i+=4) {
imagedata.data[i] = parseInt(Math.random()*255);
imagedata.data[i+1] = parseInt(Math.random()*255);
imagedata.data[i+2] = parseInt(Math.random()*255);
}
context.putImageData(imagedata,0,0);

So viel zur nüchternen CanvasPixelArray-Theorie, die Praxis ist viel spannender, denn mit getImageData(), putImageData(), createImageData() und etwas Mathematik lassen sich sogar eigene Farbfilter zum Manipulieren von Bildern schreiben. Wie das geht, zeigt der folgende Abschnitt.

Farbmanipulation mit getImageData(), createImageData() und putImageData()

Das Musterbild für alle Beispiele ist wieder die Aufnahme aus dem Yosemite-Nationalpark, die onload mit drawImage() auf den Canvas gezeichnet wird. In einem zweiten Schritt definieren wir über getImageData() das originale CanvasPixelArray, das wir dann im dritten Schritt modifizieren. Dabei werden in einer for-Schleife die RGBA-Werte jedes Pixels nach einer mathematischen Formel neu berechnet und in ein zuvor über createImageData() erzeugtes CanvasPixelArray eingetragen, das wir am Ende mit putImageData() wieder auf den Canvas zurückschreiben.

Der Code liefert das JavaScript-Grundgerüst für alle Filter, die in Abbildung 2 Verwendung finden. Die Funktion grayLuminosity() ist nicht Teil des Code-Beispiels, sondern wird, wie alle anderen Filter, im Anschluss behandelt.

pixelmanipulation_02

var image = new Image();
image.src = "images/yosemite.jpg";
image.onload = function() {
context.drawImage(image,0,0,360,240);
var modified = context.createImageData(360,240);
var imagedata = context.getImageData(0,0,360,240);
for (var i=0; i<imagedata.data.length; i+=4) {
var rgba = grayLuminosity(
imagedata.data[i+0],
imagedata.data[i+1],
imagedata.data[i+2],
imagedata.data[i+3]
);
modified.data[i+0] = rgba[0];
modified.data[i+1] = rgba[1];
modified.data[i+2] = rgba[2];
modified.data[i+3] = rgba[3];
}
context.putImageData(modified,0,0);
};

Info

Das Server-Icon in der rechten unteren Ecke von Abbildung 2 signalisiert, dass dieses Beispiel bei Verwendung von Firefox als Browser nur über einen Server mit dem http://-Protokoll aufgerufen werden kann.

var grayLightness = function(r,g,b,a) {
var val = parseInt(
(Math.max(r,g,b)+Math.min(r,g,b))*0.5
);
return [val,val,val,a];
};
var grayLuminosity = function(r,g,b,a) {
var val = parseInt(
(r*0.21)+(g*0.71)+(b*0.07)
);
return [val,val,val,a];
};
var grayAverage = function(r,g,b,a) {
var val = parseInt(
(r+g+b)/3.0
);
return [val,val,val,a];
};

Mit grayLuminosity() verwenden wir in Abbildung 2 die zweite Formel und ersetzen die RGB-Komponenten jedes Pixels durch den neu berechneten Wert. Nicht vergessen dürfen wir in dieser und allen folgenden Berechnungen, dass RGBA-Werte nur Integerzahlen sein dürfen - die JavaScript-Methode parseInt() stellt dies sicher.

var sepiaTone = function(r,g,b,a) {
var rS = (r*0.393)+(g*0.769)+(b*0.189);
var gS = (r*0.349)+(g*0.686)+(b*0.168);
var bS = (r*0.272)+(g*0.534)+(b*0.131);
return [
(rS>255) ? 255 : parseInt(rS),
(gS>255) ? 255 : parseInt(gS),
(bS>255) ? 255 : parseInt(bS),
a
];
};

Durch Aufsummieren der multiplizierten Komponenten können in jeder der drei Berechnungen natürlich auch Werte größer als 255 entstehen - in diesen Fällen wird 255 als neuer Wert eingesetzt.

Sehr einfach ist das Invertieren von Farben im Filter invertColor(), denn jede RGB-Komponente muss nur von 255 abgezogen werden.

var invertColor = function(r,g,b,a) {
return [
(255-r),
(255-g),
(255-b),
a
];
};

Der Filter swapChannels() modifiziert die Reihenfolge der Farbkanäle. Dazu müssen wir als vierten Parameter die gewünschte Neuanordnung in einem Array definieren, wobei 0 für Rot, 1 für Grün, 2 für Blau und 3 für den AlphaKanal anzugeben ist. Beim Vertauschen der Kanäle hilft uns das Array rgba mit den entsprechenden Eingangswerten, das wir in neuer Reihung zurückliefern. Ein Wechsel von RGBA nach BRGA, wie in unserem Beispiel, lässt sich mit order=[2, 0, 1, 3] realisieren.

var swapChannels = function(r,g,b,a,order) {
var rgba = [r,g,b,a];
return [
rgba[order[0]],
rgba[order[1]],
rgba[order[2]],
rgba[order[3]]
];
};

Die letzte Methode, monoColor(), setzt die RGB-Komponente jedes Pixels auf eine bestimmte Farbe und verwendet den Grauwert des Ausgangspixels als Alpha-Komponente. Der vierte Parameter beim Aufruf definiert die gewünschte Farbe als Array von RGB-Werten - in unserem Fall ist dies Blau mit color= [0, 0, 255].

var monoColor = function(r,g,b,a,color) {
return [
color[0],
color[1],
color[2],
255-(parseInt((r+g+b)/3.0))
];
};

Die vorgestellten Filter sind noch sehr einfach gestrickt, da sie Farbwerte einzelner Pixel immer ohne Berücksichtigung der Nachbarpixel verändern. Bezieht man diese in die Berechnung ein, sind komplexere Methoden wie Schärfen, Unschärfemasken oder Kantenerkennung möglich.

Die Methode getImageData() liefert ein ImageData-Objekt, das rohe (nicht vorab mit dem Alphawert multiplizierte) Pixel (als R-, G-, B- und A-Komponenten) aus einem rechteckigen Bereich des Canvas liefert. Leere ImageData-Objekte können Sie mit createImageData() erstellen. Die Pixel in einem ImageData-Objekt sind schreibbar, können also beliebig gesetzt werden. Dann können Sie diese Pixel mit putImageData() wieder in das Canvas kopieren.

Diese Methoden zur Pixelmanipulation bieten einen sehr elementaren Zugriff auf das Canvas. Das Rechteck, das Sie an getImageData() übergeben, bezieht sich auf das Standardkoordinatensystem: Seine Ausmaße werden in CSS-Pixeln gemessen, und die aktuelle Transformation wirkt sich darauf nicht aus. Rufen Sie putImageData() auf, wird ebenfalls das Standardkoordinatensystem genutzt. Außerdem ignoriert putImageData() alle Grafikattribute. Die Methode führt kein Compositing durch, multipliziert keine Pixel mit globalAlpha und zeichnet keine Schatten.

Die Methoden zur Pixelmanipulation sind gut zur Implementierung einer Bildverarbeitung geeignet. Beispiel 1 zeigt, wie man eine einfache Bewegungsunschärfe oder einen "Verwischeffekt" auf den Zeichnungen auf einem Canvas umsetzt. Das Beispiel illustriert die Verwendung von getImageData() und putImageData() und zeigt, wie man die Pixelwerte in einem ImageData-Objekt durchläuft und bearbeitet, erklärt die jeweiligen Aspekte allerdings nicht im Detail.

Beispiel 1: Bewegungsunschärfe mit ImageData

// Die Pixel des Rechtecks nach rechts verwischen und damit
// eine Art Bewegungsunschärfe erzeugen, als würden sich
// Objekte von rechts nach links bewegen. n muss 2 oder größer
// sein. Größere Werte führen zu stärkerer Verzerrung. Das
// Rechteck wird im Standard-Koordinatensystem angegeben.
function smear(c, n, x, y, w, h) {

// Das ImageData-Objekt abrufen, das das Rechteck
// der zu verwischenden Pixel repräsentiert.
var pixels = c.getImageData(x,y,w,h);

// Das Verwischen wird vor Ort ausgeführt und erfordert
// nur das ImageData-Objekt. Einige Bildverarbeitungs-
// algorithmen erfordern zusätzliche ImageData-Objekte,
// um transformierte Pixelwerte zu speichern. Würden wir
// einen Ausgabepuffer benötigen, könnten wir so ein neues
// ImageData-Objekt mit den gleichen Maßen erstellen:
var output_pixels = c.createImageData(pixels);

// Diese Maße können sich von den Argumenten für Breite
// und Höhe unterscheiden: Einem CSS-Pixel können mehrere
// Gerätepixel entsprechen.
var width = pixels.width, height = pixels.height;

// Das ist das byte-Array, das die rohen Pixeldaten von
// links oben nach rechts unten enthält. Jedes Pixel nimmt
// vier aufeinanderfolgende Bytes in der Reihenfolge R, G, B, A ein.
var data = pixels.data;

// Ab dem zweiten Pixel werden in jeder Zeile die Pixel
// verschmiert, indem sie durch 1/n des eigenen
// Werts plus m/n des Werts des vorangehenden Pixels
// ersetzt werden.
var m = n-1;

for(var row = 0; row < height; row++) { // Für jede Zeile
// die Position des zweiten Pixels der Zeile berechnen
var i = row*width*4 + 4;

// Für alle Pixel der Zeile ab dem zweiten Pixel
for(var col 1; col < width; col++, i += 4) {
data[i] =   (data[i]+data[i-4]*m)/n;    // Rot
data[i+1] = (data[i+1]+data[i-3]*m)/n;  // Grün
data[i+2] = (data[i+2]+data[i-2]*m)/n;  // Blau
data[i+3] = (data[i+3]+data[i-1]*m)/n;  // Alpha
}
}

// Jetzt die verschmierten Pixeldaten wieder in das Canvas
// kopieren
c.putImageData(pixels, x, y);
}

Beachten Sie, dass getImageData() den gleichen Cross-Origin-Sicherheitsbeschränkungen unterliegt wie toDataURL(): Die Methode funktioniert nicht, wenn in das Canvas (direkt mit drawImage() oder indirekt über ein CanvasPattern) ein Bild gezeichnet wurde, das eine andere Herkunft hat als das Dokument, das das Canvas enthält.