Pixelmanipulation im Browser

In diesem Tutorial geht es darum, ein Bild mittels Zugriff auf die Pixelebene zu verändern. Als Voraussetzungen werden grundlegende Konzepte der Programmierung (Variablen, Arrays, Schleifen) gefordert.

Wie entsteht ein digitales Bild – wie wird es abgespeichert

Die Zeiten von analogen Filmrollen und Dunkelkammern zur Entwicklung von Bildern sind so gut wie Geschichte. Die Einführung des digitalen Bildformates und der Digitalkamera veränderte die Fotografie und vor allem auch die Bildbearbeitung grundlegend. Im folgenden Abschnitt geht es darum, wie Bilder entstehen (digitalisiert werden), wie sie gespeichert und mit welchen Methoden digitale Bilder bearbeitet werden können.

Das Bild und der Computer

Um zu verstehen, wie wir digitale Bilder bearbeiten können, ist es zuerst einmal wichtig zu wissen, wie ein solches entsteht und wie es im Speicher abgelegt wird. Wenn wir ein Bild mit einer digitalen Kamera aufnehmen, geschehen im Grunde zwei Dinge:
1. Diskretisierung (Zerlegung in Bildpunkte)
Pic1.JPG
Abbildung 1
Pic2.JPG
Abbildung 2

Die Auflösung eines Bildes gibt an, wie groß der Raster des Bildes ist, also wie viele Pixel ein Bild besteht. Die Auflösung wird folgendermaßen beschrieben:
(Anzahl der Bildpunkte je Zeile) x (Anzahl der Bildpunkte je Spalte)


Pic3_1.JPG
Abbildung 3

Als Beispiel nehmen wir eine Auflösung von 1024 x 768 Pixel an. Dies würde bedeuten, dass das Bild aus einem Raster aus Bildpunkten mit 1024 Zeilen und 786 Spalten besteht. Multiplizieren wir diese beiden Zahlen, erhalten wir die Gesamtanzahl der Pixel. In diesem Fall besteht unser Bild aus 804864 Bildpunkten.
Dies führt uns zu einer weiteren Möglichkeit die Auflösung anzugeben, nämlich als Gesamtanzahl der Bildpunkte. Wir erhalten also eine Auflösung von ca. 0,8 Megapixel (1 Megapixel = 1 000 000 Pixel).
Ein Bild wird also in Pixel eingeteilt. Bis jetzt haben wir aber erst einen Raster mit leeren Feldern. Im nächsten Schritt bringen wir Farbe mit ins Spiel.

2. Quantisierung (Umwandlung der Farbinformation in einen digitalen Wert)

Jedem dieser Pixel wird eine eindeutige Farbe zugeteilt. Ein Sensor in der Digitalkamera wandelt analoge Farbinformationen in digitale um. Dabei wird den verschiedenen Farben ein numerischer Wert zugeteilt. In jedem Pixel wird also eine Zahl gespeichert, die angibt um welche Farbe es sich handelt. So wie uns die Auflösung Auskunft darüber gibt, wie viele Pixel wir zur Verfügung haben, sagt uns die Farbtiefe, wie viele verschiedene Farben wir zur Verfügung haben.
Bis jetzt haben wir ein Bild als eindimensionales Gitter angesehen, bei dem in jedem Kästchen eine Zahl steht, die uns angibt, welche Farbe es hat. Das ist nur eine vereinfachte Sicht auf unser Bild. Im am häufigsten verwendeten RGB-Farbraum gibt es ca. 16,7 Millionen verschiedene Farbabstufungen. Damit nicht jeder dieser Abstufungen ein Wert zugeteilt werden muss, greifen wir auf das Prinzip der additiven Farbmischung zurück. Das RGB-Modell besteht aus drei verschiedenen Farbkanälen (Rot, Grün, Blau). Durch Mischen dieser Töne erhält man alle verschiedenen Farben des Farbraumes.
Um dieses Modell in unserem gespeicherten Bild zu realisieren, erweitern wir unser Pixelgitter um zwei weitere Ebenen. Nun erhalten wir ein Konstrukt, bei dem drei solcher Gitter übereinanderliegen. Eine solche Ebene (Gitter) nennen wir Farbkanal.
Die Farbtiefe gibt Auskunft darüber, wie viele verschiedene Abstufungen einer Farbe im jeweiligen Farbkanal zur Verfügung stehen. Zur Veranschaulichung betrachten wir den roten Farbkanal. Eine Farbtiefe von 1 bpp (= Bit pro Pixel) bedeutet, dass wir 21 verschiedene Rottöne zur Verfügung haben (0 = Schwarz, 1 = Rot). Für eine Farbtiefe von 2 bpp erhalten wir 22 = 4 verschiedene Abstufungen (0 = Schwarz, 1 = Dunkelrot, 2 = Mittelrot, 3 = Hellrot). Am gebräuchlichsten ist eine Farbtiefe von 8 bpp. Daraus ergeben sich 28 = 256 verschiedene Zustände, also Werte von 0 bis 255, welche verschiedenen Rottöne beschreiben, wobei 0 für Schwarz und 255 für Weiß steht.
Beim RGB Farbraum mit einer Farbtiefe von 8 Bit pro Kanal erhalten wir also (2^8)^3 = 2^24 = 16.777.216 theoretisch mögliche Farben.

Pic4.gif
Abbildung 4

Abbildung 4 zeigt eine graphische Veranschaulichung der drei Farbebenen und der numerischen Werte für die einzelnen Farbabstufungen.

Wir wissen bis jetzt, dass Bilder in Pixel aufgeteilt und in drei verschiedene Farbkanäle aufgespalten werden. Jedes Pixel besteht also aus einem Rot-, Gelb- und Blauwert. Mithilfe der additiven Farbmischung erhalten wir die gewünschte Farbe.

Als letzten Schritt geben wir noch eine vierte Ebene hinzu, den α-Kanal. Dieser gibt uns Auskunft über die Transparenz eines Pixels. Zur Veranschaulichung denken wir an Bilder, die z.B. im GIF-Dateiformat gespeichert sind. Bei solchen kann es vorkommen, dass der Hintergrund transparent ist. Der α-Kanal wird ebenfalls durch 256 Werte beschrieben, wobei 0 „vollständig transparent“ (unsichtbar) und 255 „nicht transparent“ entspricht.

Pic5.JPG














Abbildung 5
Pic6.JPG
Abbildung 6

Abbildung 5 zeigt ein Bild mit weißem, Abbildung 6 ein Bild mit transparentem Hintergrund. In den meisten Bildbearbeitungsprogrammen wird ein transparenter Hintergrund als Karomuster dargestellt.

Nachdem wir uns jetzt mit den Grundlagen über digitale Bilder beschäftigt haben, gehen wir nun ans Eingemachte und sehen im nächsten Abschnit, wie wir auf die Pixelebene zugreifen können und welche Möglichkeiten der Manipulation uns zur Verfügung stehen.

Bildbearbeitung durch Pixelmanipulation

Wenn wir mit dem Computer ein Bild bearbeiten, egal ob mit Bildbearbeitungsprogrammen oder durch direkten Zugriff auf die Pixelebene eines Bildes, geschieht immer dasselbe. Es werden Farbinformationen einzelner Pixel verändert, Bildpunkte getauscht oder der Wert des α-Kanals verändert.

Ein Werkzeug

Bei der Wahl eines geeigneten Werkzeugs gibt es fast unbegrenzt viele Möglichkeiten. Es gelingt mit fast allen Programmiersprachen auf die Pixelebene zuzugreifen, sei es die C Familie, Java, JavaScript, Python, etc. Zu den Auswahlmöglichkeiten einer geeigneten Sprache kommt auch noch die Wahlmöglichkeit der Entwicklungsumgebung. Um den Vorbereitungs- und Installationsaufwand gering zu halten, verwenden wir in diesem Tutorial einen Onlineeditor von w3schools.com, indem wir mit Hilfe von JavaScript die Pixelmanipulationen durchführen werden.
Den Editor und das Beispiel auf das wir uns konzentrieren findet ihr unter folgendem Link:

http://www.w3schools.com/tags/tryit.asp?filename=tryhtml5_canvas_getimagedata2

Im linken Teil befindet sich ein Codeeditor, in dem ihr den gegebenen Code bearbeiten oder eigene Beispiele ausprobieren könnt. Wenn ihr auf den „See Result“ Button klickt, erscheint im rechten Fenster das Ergebnis.

Pic.JPG Abbildung 7

Ein einführendes Beispiel - Negativ

Unter oben genanntem Link findet ihr ein in ein HTML-Dokument eingebettetes JavaScript, welches aus dem gegebenen Bild ein Negativ erstellt. Im folgenden Abschnitt werden wir uns ansehen, wo und wie hier auf die einzelnen Pixel bzw. Farbkanäle zugegriffen wird. Wir werden also als erstes den Code analysieren.

Codeanalyse

Das sollte der Code sein, den ihr unter oben genanntem Link im HTML-Editor findet.

<!DOCTYPE html>
<html>
<body>
 
<img id="scream" src="img_the_scream.jpg" alt="The Scream" width="220" height="277">
<canvas id="myCanvas" width="220" height="277" style="border:1px solid #d3d3d3;">
Your browser does not support the HTML5 canvas tag.</canvas>
 
<script>
document.getElementById("scream").onload = function() {
    var c = document.getElementById("myCanvas");
    var ctx = c.getContext("2d");
    var img = document.getElementById("scream");
    ctx.drawImage(img, 0, 0);
    var imgData = ctx.getImageData(0, 0, c.width, c.height);
    // invert colors
    var i;
    for (i = 0; i < imgData.data.length; i += 4) {
        imgData.data[i] = 255 - imgData.data[i];
        imgData.data[i+1] = 255 - imgData.data[i+1];
        imgData.data[i+2] = 255 - imgData.data[i+2];
        imgData.data[i+3] = 255;
    }
    ctx.putImageData(imgData, 0, 0);
};
</script>
 
</body>
</html>
 

Der erste Teil des Codes beschreibt ein HTML-Dokument, welches für die Darstellung des Bildes im linken Teil verantwortlich ist.
<!DOCTYPE html>
gibt an, dass es sich um ein HTML-Dokument handelt

<html> … </html>
zwischen diesen Tags befindet sich der HTML-Code

<img id="scream" src="img_the_scream.jpg" alt="The Scream" width="220" height="277">

Das <img> Tag definiert ein Bild in einer HTML-Seite.

id="name" -> damit geben wir dem Bild in unserem HTML-Dokument einen Namen

src="URL" -> gibt an, wo sich unser Bild befindet (die URL des Bildes)

alt="alt text" -> alternativer Text für das Bild

width/hight -> Größe des Bildes in Pixel

<canvas id="myCanvas" width="220" height="277" style="border:1px solid #d3d3d3;">
Das <canvas> Element ist in HTML eine rechteckige Fläche, welche einen Container für Graphikelement darstellt.
id, width, hight haben die gleiche Bedeutung wie oben.
style="border:…" definiert den Rahmen für das Canvas-Element

<script> …  </script>
Innerhalb dieser Tags befindet sich ein JavaScript-Code, welcher für den Zugriff auf die Pixelebene und die Manipulation der Pixel verantwortlich ist.

document.getElementById("scream").onload = function(){
...
}
Beim laden (onload) des Bildes („scream“) wird die Funktion function() ausgeführt.

    var c = document.getElementById("myCanvas");
    var ctx = c.getContext("2d");
    var img = document.getElementById("scream");
    ctx.drawImage(img, 0, 0);
var c: -> beschreibt unser Canvas
var ctx: -> beinhaltet den Kontext des Canvas
var img: -> unser Bild (scream)
ctx.drawImage(img, 0, 0); -> Zeichnen des Bildes in den Canvas (Bild wird hier einfach dupliziert)

Der bis hierhin behandelte Code dient lediglich zur Vorbereitung bzw. zur Darstellung der Bilder in unserer HTML-Seite. Der Kern des Scripts, also den Zugriff und die tatsächliche Manipulation der Pixel, beinhaltet folgender Code:
var imgData = ctx.getImageData(0, 0, c.width, c.height);

var imgData ist ein Array, welches die Farbinformationen und Transparenzeigenschaften des Bildes beinhaltet. Diese sind folgendermaßen angeordnet:

Pic8.JPG
Abbildung 8



Jetzt haben wir also eine Datenstruktur, in welcher wir unsere Bildinformationen gespeichert haben. Im nächsten Schritt werden wir nun darauf zugreifen und diese so manipulieren, dass wir ein Negativ unseres originalen Bildes erhalten.

    var i;
    for (i = 0; i < imgData.data.length; i += 4) {
        imgData.data[i] = 255 - imgData.data[i];
        imgData.data[i+1] = 255 - imgData.data[i+1];
        imgData.data[i+2] = 255 - imgData.data[i+2];
        imgData.data[i+3] = 255;
    }
var i ist die Zählvariable, mitderen Hilfe wir durch das Array hüpfen.
Mit Hilfe der for-Schleife gehen wir jedes Pixel der Reihe nach durch. Die Zählvariable wird hier um 4 erhöht, da die Informationen für ein Pixel im Array hintereinander liegen. Das heißt, dass für jedes Pixel genau 4 Array-Einträge benötigt werden. Es sind genau 4, weil jeder Eintrag einen bestimmten Kanal beschreibt: [i+0] für den Rot-Kanal, [i+1]für den Grün-Kanal, [i+2]für den Blau-Kanal, [i+3] für den α-Kanal.

Um das Negativ eines Bildes zu erhalten, geschieht folgendes: Jeder Farbwert wird durch seinen inversen ersetzt. Dies geschieht, indem der aktuellen Farbwert (für jeden Farbkanal getrennt) von 255 (unserem größtem Farbwert) abgezogen wird. Der α-Kanal bleibt unverändert auf 255. Das geschieht im Codeabschnitt innerhalb der for-Schleife.

ctx.putImageData(imgData, 0, 0);
Mit der letzten Codezeile übertragen wir die neu erstellten Farbwerte auf unser tatsächliches Bild.

Bei Ausführung des Codes erhalten wir also dieses Ergebnis:
Pic9.JPG
Abbildung 9

Ein weiteres Beispiel – Rotstufen

Als nächstes wollen wir das Bild so bearbeiten, dass nur noch die Rottöne übrig bleiben. Um dies zu erreichen, müssen wir alle Farbkanäle außer dem roten auf 0 setzen. Der Codeteil, welcher uns zu diesem Zweck interessiert, ist jener innerhalb der for-Schleife. Wir belassen die Werte des Rotkanales so wie sie sind und setzen die Werte der anderen Farbkanäle auf 0.

// imgData.data[i] = 255 - imgData.data[i]; -> Diese Zeile können wir löschen
imgData.data[i+1] = 0;
imgData.data[i+2] = 0;
imgData.data[i+3] = 255;
 
Durch die Änderungen im Code sollten wir folgendes Ergebnis erhalten:
Pic10.JPG
Abbildung 10

Aufgabe 1 - Blautöne

Verändere den Code so, dass nur noch Blautöne angezeigt werden.


Aufgabe 2 – Schwarzweiß

Mit dem bisher Gelernten und ein paar kleinen Hinweisen solltet ihr es nun schaffen, aus einem Farbbild ein Schwarzwießbild zu erstellen. Die Farbwerte für den Rot-, Grün- bzw. Blaukanal eines Pixels sind hier alle gleich, nämlich das arithmetische Mittel dieser drei Werte. Geht dafür wie folgt vor:
  • Definiert vor der „Bearbeitungs-for-schleife“ eine neue Variable (muss nicht initialisiert werden). Also z.B. var avg;
  • Als nächstes berechnet ihr innerhalb der „Bearbeitungs-for-schleife“ das arithmetische Mittel der Rot-, Grün- und Blauwerte und weist es der oben definierten Variable zu.avg = (rot + grün + blau) / 3;
  • Jetzt müsst ihr diesen Wert nur noch den drei Kanälen zuteilen. imgData.data[rot] = avg; ...

Die obigen Codefragmente sind nur Hilfestellungen (Pseudocode). Die Bezeichnungen rot, grün, blau müssen entsprechend abgeändert werden.

Bei korrekter Umsetzung solltet ihr folgendes Ergebnis erhalten:
Pic11.JPG

Abbildung 11

Ein weiteres Beispiel – Spiegeln und Drehen

Bis jetzt haben wir nur die Farbeigenschaften unseres Bildes verändert. Als nächstes wollen wir durch Tauschen von verschiedenen Pixeln das Bild drehen und spiegeln. Damit wir Pixel tauschen können, benötigen wir als erstes eine Kopie des Arrays, in welchem die Farbinformationen gespeichert sind. Diese Kopie erstellen wir, indem wir ein neues leeres Array initialisieren und mit Hilfe einer Schleife die einzelnen Farbwerte unseres Bildes in dieses übertragen. Dies muss vor der eigentlichen Bearbeitung geschehen, das heißt, die neue Schleife sollte sich direkt oberhalb der for-Schleife unserer Vorlage befinden. Der einzufügende Code sollte wie folgt aussehen:

var copy = [ ];                //Initialisieren der Variable copy (Array)
for (i = 0; i < imgData.data.length; i++) {
     copy[i] = imgData.data[i];    //Kopieren der einzelnen Werte
}
 

Als erstes Beispiel zu diesem Thema wollen wir unser Bild um 180° drehen und gleichzeitig spiegeln. Um diesen Effekt zu erreichen, müssen wir die Pixel in umgedrehter Reihenfolge anordnen. Das heißt, das Pixel, welches in unserer Datenstruktur vorher das erste war, wird nun zum letzten, das zweite zum vorletzten usw. Mit Hilfe unserer „Bearbeitungs-for-Schleife“ speichern wir die Farbwerte aus der Kopie in der gewünschten Reihenfolge in unser Image Array. Das ganze sollte folgendermaßen aussehen:

for (i = 0; i < imgData.data.length; i += 4) {
        imgData.data[i] = copy[copy.length-4-i];
        imgData.data[i+1] = copy[copy.length-3-i];
        imgData.data[i+2] = copy[copy.length-2-i];
        imgData.data[i+3] = 255;
}

Wir tauschen also die Position von immer vier zusammenhängenden Arrayelementen, welche ein Pixel bilden.

Wir erhalten also folgendes Ergebnis:
Pic12.JPG
Abbildung 12

Aufgabe 3 - Einfaches Spiegeln und Drehen


Obwohl es unlogisch klingt, gestaltet sich das einfache Spiegeln und das einfache Drehen um 90° bzw. 180° des Bildes wegen unserer gegebenen Datenstruktur (eindimensionales Array) etwas schwieriger. Die dritte Aufgabe besteht darin, mögliche Ursachen zu finden, welche diese Operationen erschweren und welche Konstrukte man zur Realisierung dieser Befehle einführen müsste. Dankt dabei vor allem daran, ob es wichtig ist zu wissen, in der wievielten Zeile/Spalte und an welcher Position in dieser das jeweilige Pixel steht.


Lösungen zu den Aufgaben:

Aufgabe 1:

imgData.data[i] = 0;
imgData.data[i+1] = 0;
//imgData.data[i+2] = 255 - imgData.data[i+2]; -> Diese Zeile können wir löschen
imgData.data[i+3] = 255;

Aufgabe 2:

var avg;
for (i = 0; i < imgData.data.length; i += 4) {
        avg = (imgData.data[i]+imgData.data[i+1]+imgData.dasta[i+2])/3;
        imgData.data[i] = avg;
        imgData.data[i+1] = avg;
        imgData.data[i+2] = avg;
        imgData.data[i+3] = 255;
 }

Aufgabe 3:

Das Problem, welches beim einfachen Spiegeln oder Drehen auftritt ist, dass es notwendig ist zu wissen, wie lange eine Zeile bzw. Spalte ist und in welcher Position von dieser ein jeweiliges Pixel steht. Nehmen wir als Beispiel die Spiegelung an der Vertikalen Achse her. Hier muss der jeweils erste Bildpunkt mit dem jeweils letzten einer Zeile getauscht werden, der zweite mit dem vorletzten usw. Dies muss für alle Zeilen geschehen. Um herauszufinden wann eine Zeile aufhört und eine neue anfängt bedarf es einer zweiten for-Schleife oder einer Modulo Rechnung. Wollen wir herauszufinden, in welcher Spalte ein Bildpunkt steht und wo diese anfängt und aufhört wird dieses Problem noch verschärft (Spiegelung an der horizontalen Achse). Bei der Drehung um 90° müssen wir auch schon im Vorfeld daran denken, dass Breite und Höhe unseres Bildes verschieden sein können.