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.
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:
int[] zeile = bild[1];Trage ein, woraufzeiledanach verweist.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 bisgrau[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].lengthund nichtgrau[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 / ???;
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:
- Zwei Attribute der Klasse, die die Methode setzt.
- Ein
int[]mit zwei Feldern als Rückgabewert. - 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
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?