Informatik

Das Bild als Gitter

Halte dein Handy nah genug an ein Foto, und irgendwann siehst du keine Kabel und keine LED mehr, sondern Kästchen. Genau so liegt ein Bild im Speicher: als Tabelle aus Zahlen, Zeile für Zeile.

Das Gitter anfassen

Der Ausschnitt unten zeigt zehn Spalten und acht Zeilen aus dem echten Foto – jede Kachel ein Bildpunkt, jede Zahl eine Helligkeit zwischen 0 und 255. Fahre mit der Maus über das Gitter: Zeile und Spalte des Bildpunkts werden markiert, und die Zeile darunter zeigt die Java-Schreibweise bild[zeile][spalte]. Ein Klick hält den Bildpunkt fest, ein zweiter Klick gibt ihn wieder frei. Was diese Schreibweise genau bedeutet, klären die nächsten Abschnitte.

Probiere danach die Filter aus. Jeder wendet seine Regel – angezeigt als Java-Code – auf jeden einzelnen Bildpunkt an.

64 82 240 240 94 189 73 69 100 76 72 244 236 236 148 174 78 75 99 74 101 174 236 232 237 81 71 67 82 80 77 97 240 236 240 95 83 107 25 96 89 92 244 236 236 106 73 101 25 24 91 93 252 244 134 95 39 38 22 17 90 92 96 95 155 102 32 34 30 14 101 95 91 76 91 97 22 20 85 25

Eine Zeile kennst du schon

Nimm dir nur die oberste Zeile des Gitters vor. Das sind zehn Zahlen nebeneinander, also ein ganz gewöhnliches eindimensionales Feld:

int[] zeile0 = {64, 82, 240, 240, 94, 189, 73, 69, 100, 76};

zeile0[2] ist 240, zeile0.length ist 10. Mit einer Schleife kommst du an jeden Wert dieser Zeile heran.

Das Gitter hat aber acht Zeilen, das echte Foto sogar 72. Acht Variablen zeile0 bis zeile7 anzulegen wäre umständlich, und über acht einzelne Variablen kann keine Schleife laufen. Dieses Problem kennst du schon: Statt vieler einzelner Zahlen hast du damals ein Feld mit Zahlen angelegt. Hier geht es genauso, nur legst du statt der Zahlen die Zeilen in ein Feld.

Ein Feld von Feldern

Ein kleines Bild mit zwei Zeilen und drei Spalten sieht in Java so aus:

int[][] bild = {
    {20, 80, 240},
    {50, 120, 200}
};

Lies den Typ int[][] von links nach rechts als „ein Feld, dessen Einträge int[] sind“. Das äußere Feld hat zwei Einträge, einen für jede Zeile. Jeder dieser Einträge ist wieder ein Feld, und zwar eines mit drei Zahlen.

Daraus folgt, wie man auf einen einzelnen Wert zugreift, nämlich in zwei Schritten:

Ausdruck Typ Ergebnis
bild[1] int[] die ganze Zeile 1, also {50, 120, 200}
bild[1][2] int in dieser Zeile der Eintrag 2, also 200

Sage vorher, was das Programm ausgibt. Schreibe dir alle sechs Zeilen auf und starte es erst dann.

Ergänze danach eine Ausgabe für bild[2][0]. Was passiert, und warum?

Auflösung
zeile.length   = 3
zeile[2]       = 200
bild[1][2]     = 200
bild[0][1]     = 80
bild.length    = 2
bild[0].length = 3

zeile[2] und bild[1][2] liefern denselben Wert. Das ist kein Zufall: bild[1][2] ist nur die kurze Schreibweise für „hole die Zeile bild[1] und darin den Eintrag [2]“.

bild.length ist 2 und nicht 6. Das äußere Feld zählt nur seine Einträge, und das sind die zwei Zeilen. Wie viele Werte in einer Zeile stehen, verrät erst die Zeile selbst: bild[0].length.

bild[2][0] führt zu einem Laufzeitfehler. Eine Zeile 2 gibt es nicht, weil das äußere Feld nur die Indizes 0 und 1 hat. Dieser Fehler begegnet dir im Projekt noch öfter, meistens dann, wenn Zeile und Spalte vertauscht sind.

Schreibweise Bedeutung
int[][] bild ein zweidimensionales Feld, also ein Feld von Feldern
bild[zeile] eine ganze Zeile, ein int[]
bild[zeile][spalte] ein einzelner Wert: zuerst die Zeile, dann die Spalte
bild.length die Anzahl der Zeilen
bild[zeile].length die Anzahl der Spalten in dieser Zeile

Beide Indizes beginnen bei 0. Zeile 0 ist oben, und die Zeilennummern wachsen nach unten.

Wie das Gitter im Speicher liegt

Die Tabellenform ist eine Vorstellung, die beim Programmieren hilft. Im Speicher liegt das Gitter aber anders: Das äußere Feld enthält keine Zahlen, sondern Verweise auf die Zeilen. Jede Zeile ist ein eigenes Feldobjekt.

Das Diagramm zeigt bild aus dem Programm oben. Bearbeite danach die beiden Schritte:

  1. int[] zeile = bild[1]; Trage ein, worauf zeile danach verweist.
  2. zeile[2] = 0; Ändere den Wert, der sich dadurch ändert.

Nach den beiden Schritten wird bild[1][2] ausgegeben. Kommt 200 heraus oder 0? Begründe mit dem Diagramm und probiere es im Programm oben aus.

Auflösung

Es kommt 0 heraus. int[] zeile = bild[1]; legt keine Kopie der Zeile an. Die Variable zeile bekommt nur denselben Verweis, der auch in bild[1] steht. zeile und bild[1] sind danach zwei Namen für dasselbe Feldobjekt, und eine Änderung über den einen Namen ist auch über den anderen zu sehen.

Das ist später in diesem Projekt wichtig: Ein Filter bekommt als Parameter nur den Verweis auf das Bild. Schreibt er hinein, verändert er das Original.

Startmission: Im Gitter zurechtfinden

Bearbeite jetzt die Stationen. Du brauchst noch keinen Code zu schreiben, nur zu lesen: Du findest Bildpunkte, zählst Zeilen und Spalten, malst aus, was ein Programm in ein Gitter schreibt, und setzt am Ende eine geschachtelte Schleife zusammen. Wenn etwas noch nicht stimmt, bekommst du direkt eine Erklärung und einen neuen Versuch.

Das Foto untersuchen

Das echte Foto ist dasselbe wie das kleine bild, nur größer. Das Objekt foto liefert es dir als Graustufen: eine Zahl je Bildpunkt, 0 ist schwarz und 255 ist weiß.

grau
       Spalte 0    1    2   …   95
Zeile 0  ┌────┬────┬────┬───┬────┐
         │ 64 │ 66 │ 71 │ … │ 76 │   ← grau[0] ist ein int[] der Länge 96
Zeile 1  ├────┼────┼────┼───┼────┤
         │ 64 │ 69 │ 70 │ … │ 76 │
   …     ├────┼────┼────┼───┼────┤
Zeile 71 │101 │100 │100 │ … │ 25 │
         └────┴────┴────┴───┴────┘

a) Das Foto ist 96 Bildpunkte breit und 72 hoch. Warum ist dann grau.length 72 und nicht 96?

b) Vertausche in der Zeile mit „Unten rechts“ die beiden Zahlen, sodass dort grau[95][71] steht. Sage vorher, was passiert, und starte das Programm dann.

c) Gib den Bildpunkt in der Mitte des Fotos aus. Welche Zeile und welche Spalte ist das?

Auflösung

a) grau.length zählt die Einträge des äußeren Feldes, und das sind die Zeilen. Wie hoch ein Bild ist, bestimmt die Zahl seiner Zeilen. Die Breite ist die Länge einer Zeile, also grau[0].length.

b) Das Programm bricht mit einem Laufzeitfehler ab. grau[95] wäre die Zeile 95, aber es gibt nur die Zeilen 0 bis 71. Wer Zeile und Spalte vertauscht, merkt das bei einem quadratischen Bild nicht, bei einem rechteckigen aber sofort.

c) Die Mitte liegt bei Zeile 36 und Spalte 48:

IO.println("Mitte: " + grau[36][48]);

Besser ist es, nicht selbst zu rechnen, sondern grau[grau.length / 2][grau[0].length / 2] zu schreiben. Dann stimmt die Zeile auch für ein anderes Foto.

Jeden Bildpunkt einmal anfassen

Um eine einzelne Zeile zu durchlaufen, brauchst du eine Schleife, so wie bei jedem eindimensionalen Feld. Um alle Zeilen zu durchlaufen, setzt du um diese Schleife eine zweite: Die äußere holt eine Zeile nach der anderen, die innere läuft durch die geholte Zeile.

for (int zeile = 0; zeile < grau.length; zeile++) {
    int[] reihe = grau[zeile];                     // die ganze Zeile, ein int[]
    for (int spalte = 0; spalte < reihe.length; spalte++) {
        // reihe[spalte] ist dasselbe wie grau[zeile][spalte]
    }
}

Die Hilfsvariable reihe zeigt, was passiert. Man lässt sie meistens weg und schreibt gleich grau[zeile][spalte]. Diese Form steht in fast jedem Filter des Projekts:

for (int zeile = 0; zeile < grau.length; zeile++) {
    for (int spalte = 0; spalte < grau[zeile].length; spalte++) {
        // hier steht grau[zeile][spalte] zur Verfügung
    }
}
  • Außen die Zeilen, innen die Spalten. Die äußere Schleife läuft bis grau.length, die innere bis grau[zeile].length.
  • Die innere Schleife läuft für jede Zeile einmal ganz durch. Beim Foto wird der Rumpf also 72 · 96 = 6912-mal ausgeführt.
  • Schreibe innen grau[zeile].length und nicht grau[0].length. Bei einem Foto sind alle Zeilen gleich lang, aber so stimmt die Schleife auch dann noch, wenn sie es einmal nicht sind.

Aufgabe 1: Wie hell ist das Bild?

Ergänze die Methode durchschnitt. Sie soll die mittlere Helligkeit aller Bildpunkte liefern.

Sage vorher: Das Foto zeigt eine überstrahlte LED vor einem dunklen Hintergrund. Liegt der Durchschnitt eher bei 60, bei 128 oder bei 200?

Tipp 1: Wie ging das bei einem eindimensionalen Feld?

Die Summe aller Werte eines int[] werte hast du so gebildet:

int summe = 0;
for (int i = 0; i < werte.length; i++) {
    summe = summe + werte[i];
}

Beim Gitter ist es dasselbe. Nur läuft die Schleife jetzt über die Zeilen, und um sie herum steht die Schleife aus dem Merkkasten oben.

Tipp 2: Was brauchst du?

Zwei Dinge: die Summe aller Werte und die Anzahl der Bildpunkte. Die Summe sammelst du in einer Variablen, die vor den Schleifen angelegt wird. Die Anzahl musst du nicht mitzählen – sie ist pGrau.length * pGrau[0].length.

Tipp 3: Das Gerüst
int summe = 0;
for (int zeile = 0; zeile < pGrau.length; zeile++) {
    for (int spalte = 0; spalte < pGrau[zeile].length; spalte++) {
        summe = summe + pGrau[zeile][spalte];
    }
}
return summe / ???;
Eine mögliche Lösung. Erfrage das Passwort bei deiner Lehrkraft oder sieh auf der Seite Lösungspasswörter nach.

Aufgabe 2: Wo ist der hellste Punkt?

Suche den Bildpunkt mit dem größten Wert und gib seine Zeile und seine Spalte aus. Markiere ihn anschließend: Setze im Gitter ein Kreuz aus schwarzen Punkten (Wert 0) um ihn herum und zeige das Bild noch einmal.

Anders als bei Aufgabe 1 genügt eine Zahl als Ergebnis nicht – du brauchst zwei. Überlege dir, wie du beide aus einer Methode herausbekommst.

In ein Gitter schreibst du genauso, wie du daraus liest: grau[zeile][spalte] = 0; setzt einen einzelnen Bildpunkt auf Schwarz.

Tipp 1: Zwei Ergebnisse

Drei Wege gibt es:

  1. Zwei Attribute der Klasse, die die Methode setzt.
  2. Ein int[] mit zwei Feldern als Rückgabewert.
  3. Gar keine Methode – die Suche steht direkt in main.

Für eine erste Lösung ist der dritte Weg völlig in Ordnung.

Tipp 2: Die Suche

Es ist dieselbe Suche wie beim Maximum eines eindimensionalen Feldes, nur mit einer Schleife mehr. Merke dir nicht nur den Wert, sondern auch, wo er stand:

int besterWert = pGrau[0][0];
int besteZeile = 0;
int besteSpalte = 0;
// … in den Schleifen: ist pGrau[zeile][spalte] größer, alle drei aktualisieren
Eine mögliche Lösung. Erfrage das Passwort bei deiner Lehrkraft oder sieh auf der Seite Lösungspasswörter nach.

Zum Weiterdenken

Das Foto ist 96 × 72 Bildpunkte groß und liegt als int[][] im Speicher.

a) Wie viele Zahlen sind das? Wie viel Speicher braucht das Gitter, wenn ein int vier Byte belegt?

b) Ein Foto aus einer Handykamera hat 4032 × 3024 Bildpunkte, und zwar in drei Farbkanälen. Wie viel wären es dann?

c) Eine JPEG-Datei dieses Fotos ist keine 5 Megabyte groß. Wie kann das sein?

Das Bild als Gitter

Teilbare URL erstellen

Abschnitte auswählen