1/26/2016

HTML5 Tutorial: Canvas Pixel Manipulation

In diesem Tutorial wird beschrieben, wie ihr mit Hilfe vom ImageData Objekt Zugriff auf die Pixel des HTML5 Canvas Elements bekommt. 

Zuerst schauen wir uns die Grundlagen an bevor wir tiefer in die Materie eintauchen.
Voraussetzung für dieses Tutorial ist, dass ihr bereits mit dem Canvas Element vertraut seid und wisst wie ihr damit umzugehen habt. Und natürlich solltet ihr zumindest grundliegende Kenntnisse in JavaScript haben. 

Das Thema „Pixel Manipulation“ ist die Grundlage so ziemlich von allen Experimenten die ich in letzter Zeit hier auf meinem Blog veröffentlich habe. Hier eine Auswahl an Experimenten (Die meisten mit Partikeln in 3D, dazu wird es ein weiteres Tutorial geben):


Los geht’s.

Was ist das ImageData Objekt,
wie erstellen wir eins und wie kommen wir an die Pixelinformationen?

Das ImageData Objekt hat 3 Eigenschaften (Read only):

ImageData.width
ImageData.height
ImageData.data

Die beiden Eigenschaften width und height beinhalten die Werte für die Breite und Höhe.
Die data Eigenschaft beinhaltet die Pixelinformationen an die wir gelangen wollen und mit deren Hilfe wir Zugriff auf die Pixel bekommen. ImageData.data ist im Prinzip nichts anderes als ein Array. Genauer gesagt ein Uint8ClampedArray das die Pixelinformationen in der Reihenfolge RGBA beinhaltet mit Werten von 0 – 255. Später mehr dazu.

Zunächst einmal erstellen wir ein ImageData Objekt:

var canvas = document.createElement( 'canvas' );
    canvas.width = 100;
    canvas.height = 100;

document.body.appendChild( canvas );

var context = canvas.getContext( '2d' );

var imageData = context.createImageData( 100, 100 );

Wir haben nun mit Hilfe der Methode createImageData() ein leeres ImageData Objekt erstellt mit einer Breite und Höhe von 100 Pixeln. Alle Pixel in diesem neuen Objekt sind per Voreinstellung auf schwarz mit einer Transparenz von 0 gesetzt. Oder anders ausgedrückt, jedes Pixel hat folgende Werte:

R = 0;//rot
G = 0;//grün
B = 0;//blau
A = 0;//alpha

Neben der Methode createImageData() gibt es noch eine weitere Möglichkeit ein ImageData Objekt zu erstellen. Ihr könnt mit Hilfe der Methode getImageData() auch die Pixelinformationen aus einer Canvas auslesen.
Im folgenden Beispiel erstellen wir wieder eine Canvas. Aber im Gegensatz zum vorherigen Beispiel färben wir dieses Mal die Canvas per fillRect rot ein. Dementsprechend sind die RGBA Pixelinformationen die wir erhalten anders als im ersten Beispiel.

var canvas = document.createElement( 'canvas' );
    canvas.width = 100;
    canvas.height = 100;

document.body.appendChild( canvas );

var context = canvas.getContext( '2d' );
    context.fillStyle = '#FF0000';
    context.fillRect( 0, 0, 100, 100 );

var imageData = context.getImageData( 0, 0, 100, 100 );

Beispiel.

Nun haben wir ein ImageData Objekt erstellt das die Pixelinformationen der Canvas enthält.
Da wir unsere Canvas rot eingefärbt haben besitzen die Pixel nun folgende Werte:

R = 255;//rot
G = 0;//grün
B = 0;//blau
A = 255;//alpha

Wir haben also zwei Methoden zur Auswahl mit denen wir ein ImageData Objekt erstellen können.
Die Methode createImageData() eignet sich vor allem dann, wenn man ein völlig neues und leeres „Bild“ als Grundlage haben möchte. Mit der Methode getImageData() hingegen kann man z.B. die Pixel eines geladenen Bildes auslesen und verändern.

Schauen wir uns nun die Werte genauer an die wir aus dem data Array auslesen können. Zunächst einmal die Grundlagen zu RGBA:

R = rot (ein Wert von 0 – 255)
G = grün (ein Wert von 0 – 255)
B = blau (ein Wert von 0 – 255)
A = alpha (ein Wert von 0 – 255. 0 ist transparent und 255 voll sichtbar)

Im folgenden Beispiel lesen wir die Daten für das erste Pixel aus unserem per getImageData() erstellten imageData Objekt aus:

var r = imageData.data[ 0 ];
var g = imageData.data[ 1 ];
var b = imageData.data[ 2 ];
var a = imageData.data[ 3 ];

Wir wissen nun die Farbe des ersten Pixels (links oben) und können dem Beispiel entnehmen, dass die ersten vier Werte aus dem data Array, nämlich 0, 1, 2 und 3 ein Pixel repräsentieren. Folgerichtig besteht jedes Pixel aus vier Werten (r, g, b, a) und das hilft uns dabei wenn wir über das data Array iterieren wollen um an alle Pixelinformationen zu gelangen.
Um über das data Array zu iterieren nehmen wir eine übliche for-Schleife:

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

}

Auf diese Weise erhalten wir die RGBA Farbinformationen eines jeden im data Array gespeicherten Pixels. Der Trick hierbei besteht darin den Schlüssel i nicht um den Wert 1 zu erhöhen (i++), sondern um 4 (i += 4).
Denn, wie wir bereits wissen, besteht jedes Pixel aus 4 Werten (r, g, b, a) und somit kommen wir in jedem Schritt der for-Schleife an die Farbinformationen eines Pixels.
Hier gibt es noch ein sehr gutes Tutorial genau zu dem Thema.


Um das iterieren über das Array zu optimieren empfehle ich unbedingt 2 Änderungen an dem obigen Beispiel vorzunehmen. Zum einen speichern wir imgData.data in einer extra Variablen und die for-Schleife schreiben wir wie folgt auch etwas anders.

var data = imageData.data;

for ( var i = 0, l = data.length; i < l; i += 4 ) {

    var r = data[ i ];
    var g = data[ i + 1 ];
    var b = data[ i + 2 ];
    var a = data[ i + 3 ];

}

Auf diese Weise läuft das Auslesen der Pixelinformationen performanter ab. In den folgenden Beispielen setze ich voraus das data als extra Variable gespeichert ist.
Hier noch ein schöner Link mit weiteren Tipps zur Optimierung.


Auslesen ist das eine,
aber nun gehen wir einen Schritt weiter und verändern die Farbinformationen.

Im folgenden Beispiel geben wir allen Pixeln aus unserem data Array die RGBA Werte für die Farbe Grün.

Grün sieht so aus:

R = 0; 
G = 255; 
B = 0; 
A = 255;

Und nun ändern wir die Werte, die zuvor noch rot waren entsprechend in grün um:

for ( var i = 0, l = data.length; i < l; i += 4 ) {

    data[ i ]     = 0;
    data[ i + 1 ] = 255;
    data[ i + 2 ] = 0;
    data[ i + 3 ] = 255;

}

Wir haben nun allen Pixeln die im data Array enthalten sind neue Werte zugewiesen.
Doch wieso wirkt sich diese Änderung nicht aus? Unsere Canvas ist immer noch rot und nicht grün.
Damit sich unsere „Pixel Manipulation“ auswirkt müssen wir noch einen Schritt durchführen:

context.putImageData( imageData, 0, 0 );

Mit der Methode putImageData() kopieren wir die von uns abgeänderten Pixelinformationen des ImageData Objekts zurück in die Canvas an die Position x = 0 und y = 0.
Und nun ist unsere Canvas nicht mehr rot, sondern grün.

Hier nun zusammengefasst das komplette Beispiel:

var canvas = document.createElement( 'canvas' );
    canvas.width = 100;
    canvas.height = 100;
 
document.body.appendChild( canvas );

var context = canvas.getContext( '2d' );
    context.fillStyle = '#FF0000';
    context.fillRect( 0, 0, 100, 100 );

var imageData = context.getImageData( 0, 0, 100, 100 );
var data = imageData.data;

for ( var i = 0, l = data.length; i < l; i += 4 ) {

    data[ i ]     = 0;
    data[ i + 1 ] = 255;
    data[ i + 2 ] = 0;
    data[ i + 3 ] = 255;

}

context.putImageData( imageData, 0, 0 );

Beispiel in JSFiddle.

Wir verfügen nun über die Grundlagen und wissen wie wir Pixelinformationen auslesen und verändern können.
Natürlich macht es wenig Sinn eine komplette Fläche, also eine komplette Canvas auf diese Weise von einer Farbe in eine andere umzufärben. Dafür gibt es auch andere Methoden für die wir nicht einmal Zugriff auf die Pixel benötigen. Um aber beispielhaft zu erklären wie das ganze grundliegend funktioniert ist es dann eben doch sinnvoll.

Folgend ein etwas anderes Beispiel, welches man ohne den Zugriff auf die Pixel so nicht umsetzen könnte. Wir geben jetzt jedem einzelnen Pixel aus dem data Array per random einen zufälligen Farbwert:

var canvas = document.createElement( 'canvas' );
    canvas.width = 100;
    canvas.height = 100;
 
document.body.appendChild( canvas );

var context = canvas.getContext( '2d' );

var imageData = context.getImageData( 0, 0, 100, 100 );
var data = imageData.data;

for ( var i = 0, l = data.length; i < l; i += 4 ) {

    data[ i ]     = Math.floor( Math.random() * 255 );
    data[ i + 1 ] = Math.floor( Math.random() * 255 );
    data[ i + 2 ] = Math.floor( Math.random() * 255 );
    data[ i + 3 ] = 255;

}

context.putImageData( imageData, 0, 0 );

Beispiel in JSFiddle.
  
Und hier noch drei weitere JSFiddle Beispiele. Dieses Mal laden wir ein externes Bild und zeichnen dieses, nachdem es geladen worden ist, per drawImage in die Canvas und verändern dessen Pixelinformationen.

Beispiel 1: Farben invertieren
Beispiel 2: Ein Graustufenbild erzeugen
Beispiel 3: Per Schwellenwertverfahren Farbwerte zufällig segmentieren

Hier noch ein externes Tutorial zu diesem Thema, das etwas tiefer in die Materie Filter eintaucht und sehr schöne Beispiele beinhaltet.

Anhand dieser Beispiele sollte klar werden wie mächtig der Zugriff auf die Pixel sein kann.
Achtung: Wenn wir mit der Methode drawImage ein Bild in die Canvas zeichnen und Zugriff auf die Pixel haben wollen, dann muss sich dieses Bild auf dem gleichen Server befinden auf dem auch das JavaScript ausgeführt wird! Ist das nicht der Fall wird die Konsole eures Browsers eine Fehlermeldung ausgeben und der Zugriff auf die Pixel wird verweigert:


SecurityError: The operation is insecure.

Man kann das umgehen. Aber der Weg ist steinig und nicht in jedem Fall durchzuführen.
Das Stichwort hierbei lautet CORS. Vereinfacht ausgedrückt: Der Besitzer des Bildes muss euch den Zugriff auf das Bild erlauben. Wie man das macht wird hier beschrieben.

Wenn ihr dann ein solches Bild von einem anderen Server laden wollt, dann müsst ihr unbedingt die folgende Zeile in eurem Script ergänzen:


image.crossOrigin = "anonymous";

Wichtig hierbei. Diese extra Zeile muss vor der Zeile eingefügt werden in der ihr den src-Pfad des Bildes angebt. Hier ein Beispiel wie ihr ein solches Bild laden könnt:

var image = document.createElement( 'IMG' );
    image.onload = function () {
    
        //Image ready
        
    }
    image.crossOrigin = "anonymous";
    image.src = 'http://www.beispiel.de/seinBild.jpg';

Und hier noch einmal zum Vergleich ein Beispiel wie ihr ein Bild vom gleichen Server laden könnt:

var image = document.createElement( 'IMG' );
    image.onload = function () {
    
        //Image ready
        
    }
    image.src = 'deinBild.jpg';

Das aber nur kurz zur Erklärung. Nicht das ihr euch wundert, wenn ihr ein Bild von einem anderen/fremden Server ladet und dann die hier gelernten Inhalte nicht funktionieren.

Kommen wir nun aber zurück zum eigentlichen Inhalt dieses Tutorials. 

Bisher haben wir einfach immer nur alle Pixel eines Bildes ausgelesen und/oder verändert. Was aber wenn wir gezielt einzelne Pixel ansprechen/auslesen wollen?
Dazu brauchen wir zwei Funktionen/Methoden, die es uns erlauben anhand von XY-Koordinaten einzelne Pixel aus dem data Array anzusprechen.

Fangen wir damit an einzelne Pixel aus dem data Array per XY-Koordinaten zu verändern.
Das Problem. Wir bekommen per data Array nur über den Index Zugriff auf die einzelnen Werte.
Daher müssen wir einen kleinen Trick anwenden um unsere X und Y Werte in den Index umzurechnen. Eigentlich ganz einfach. Und so wird’s gemacht:

var i = ( x + y * imageData.width ) * 4;//i = index

Verpacken wir das jetzt in einer Funktion, die als Paramater X, Y und ein Objekt (c) mit den Farbwerten RGBA entgegen nimmt, kommt das dabei raus:

function setPixel( x, y, c ) {

    var i = ( x + y * imageData.width ) * 4;

    data[ i ] = c.r;
    data[ i + 1 ] = c.g;
    data[ i + 2 ] = c.b;
    data[ i + 3 ] = c.a;

};

Wir haben nun eine Funktion, die es uns erlaubt die Farbwerte eines per XY-Koordinaten ausgewählten Pixels zu verändern.
Dabei ist zu beachten, dass die X und Y Werte ganzzahlige Werte ohne Komma und Dezimalstellen sind. Schließlich gibt es keine Pixel auf Koordinaten mit Komma. Es gilt also, wenn vorhanden, Fließkommazahlen (Float) vorab in Integer umzuwandeln. Vereinfacht ausgedrückt: Bevor wir die Funktion setPixel aufrufen, müssen wir dafür sorgen, dass die XY-Koordinaten keine Kommas mehr enthalten. Es gibt mehrere Wege das zu tun. Ich zeige hier 2 Möglichkeiten auf, wobei ich immer die zweite Variante verwende.

Variante 1 mit Math.floor():

var x = 19.123425235;
var y = 45.534534213;
var c = { r:0, g:0, b:0, a:255 };

function setPixel( x, y, c ) {

    var i = ( x + y * imageData.width ) * 4;

    data[ i ] = c.r;
    data[ i + 1 ] = c.g;
    data[ i + 2 ] = c.b;
    data[ i + 3 ] = c.a;

};

setPixel( Math.floor( x ), Math.floor( y ), c );

Variante 2 mit Bitwise OR operator:

var x = 19.123425235;
var y = 45.534534213;
var c = { r:0, g:0, b:0, a:255 };

function setPixel( x, y, c ) {

    var i = ( x + y * imageData.width ) * 4;

    data[ i ] = c.r;
    data[ i + 1 ] = c.g;
    data[ i + 2 ] = c.b;
    data[ i + 3 ] = c.a;

};

setPixel( x | 0, y | 0, c );

In beiden Beispielen färben wir jeweils ein Pixel schwarz ein. Daher auch die Variable c, bzw. das Objekt c mit den Werten für RGBA:

var c = { r:0, g:0, b:0, a:255 };

Wie wir sehen beinhalten die Variablen für die XY-Koordinaten Fließkommazahlen. Einfach nur um das mit der Umwandlung zu verdeutlichen. Würden wir an dieser Stelle keine Umwandlung vornehmen, dann würden die hier gezeigten Beispiele versagen und die Funktion setPixel wäre nicht mehr in der Lage aus den XY-Koordinaten den Index für das Array zu berechnen.
Ich bevorzuge die Variante 2 mit dem Bitwise OR operator, weil das, wenn man sehr viele Pixel Zugriffe auf einmal hat, einfach ein Stück weit performanter läuft als mit Math.floor().

Und noch eine Kleinigkeit gilt es bei der Funktion setPixel zu beachten. Führt ihr Berechnungen durch, bei denen es vorkommen kann, dass die XY-Koordinaten außerhalb des Bereichs der Canvas liegen, dann müsst/solltet ihr durch eine zusätzliche If-Bedingung sicherstellen, dass diese nicht gezeichnet werden. Das führt sonst zu komischen, manchmal auch interessanten, aber bestimmt nicht gewollten Effekten. Hier ein Beispiel dafür, mit der Annahme, dass unsere Canvas eine Breite und Höhe von 100 Pixeln hat und das unser Pixel außerhalb davon liegt:

var x = -19.123425235;
var y = 450.53453213;
var c = { r:0, g:0, b:0, a:255 };

function setPixel( x, y, c ) {

    var i = ( x + y * imageData.width ) * 4;

    data[ i ] = c.r;
    data[ i + 1 ] = c.g;
    data[ i + 2 ] = c.b;
    data[ i + 3 ] = c.a;

};

if ( x > 0 && x <= 100 && y > 0 && y <= 100 )
 setPixel( x | 0, y | 0, c );

Hier noch einmal als Beispiel in JSFiddle mit Koordinaten innerhalb der Canvas.

Jetzt, wo wir wissen wie man die Farbwerte eines Pixels verändert, soll es nun darum gehen die Farbinformationen bestimmter Pixel auszulesen. Die folgende Funktion nimmt die Parameter X und Y entgegen und gibt ein Objekt zurück mit den Werten RGBA:

function getPixel( x, y ) {

    var i = ( x + y * imageData.width ) * 4;

    return { r:data[ i ],
             g:data[ i + 1 ],
             b:data[ i + 2 ],
             a:data[ i + 3 ] }

};

In diesem Beispiel in JSFiddle lesen wir per Zufall den Farbwert eines Pixels aus.

Mit den beiden Methoden setPixel und getPixel haben wir nun ganz komfortabel Zugriff auf die einzelnen Pixel der Canvas. Von hier an stehen uns im Prinzip alle Türen offen.

Folgend ein paar JSFiddle Beispiele mit denen wir per XY-Koordinaten die Pixel der Canvas verändern und somit Bilder aus Code generieren.


In einem weiteren Tutorial, das auf diesem aufbauen wird, werde ich dann erklären wie ihr die dritte Dimension und Partikel (wie hier bei diversen Beispielen schon gezeigt) auf eure Canvas bringt. 

Hier noch zum Abschluss eine kleine aber feine Auswahl an deutschsprachigen Tutorials rund um dieses Thema:


Ich hoffe das Tutorial hat euch gefallen. Falls noch irgendetwas unklar sein sollte hinterlasst einfach einen Kommentar oder schreibt mir. Ich würde mich freuen wenn ihr wieder vorbei schaut zu Teil 2.

Keine Kommentare: