Punktfilter
Der einfachste Filter, den es gibt, schaut sich jeden Bildpunkt einzeln an, rechnet eine neue Zahl aus und schreibt sie ins Ergebnis. Was rundherum liegt, interessiert ihn nicht. Solche Filter heißen Punktoperatoren – und überraschend viel von dem, was eine Kamera-App anbietet, gehört dazu.
Ein neues Gitter anlegen
Auf der letzten Seite hast du Gitter nur gelesen. Ein Filter muss aber ein neues Bild herstellen, und dafür braucht er ein leeres Gitter, in das er schreiben kann. Das legst du so an, wie du es vom eindimensionalen Feld kennst, nur mit zwei Größenangaben:
| eindimensional | zweidimensional |
|---|---|
int[] werte = new int[10]; |
int[][] bild = new int[72][96]; |
| 10 Einträge | 72 Zeilen mit je 96 Spalten |
| überall steht 0 | überall steht 0 |
Die Reihenfolge ist dieselbe wie beim Zugriff: zuerst die Zeilen, dann die
Spalten. new int[72][96] legt also ein Bild an, das 96 Bildpunkte breit und
72 hoch ist.
Das folgende Programm legt ein solches Gitter an, füllt es mit den zwei geschachtelten Schleifen und zeigt es an. Ein Foto braucht es dafür nicht.
a) Sage vorher, wie das Bild aussieht: Wo ist es hell, wo dunkel? Starte das Programm dann.
b) Ändere die eine Zeile im Schleifenrumpf so, dass der Verlauf von oben nach unten geht: oben schwarz, unten hell.
c) Male ein Schachbrett aus Feldern mit einer Größe von 12 × 12 Bildpunkten, abwechselnd schwarz (0) und weiß (255).
d) Male einen weißen Rahmen, der 4 Bildpunkte breit ist, um ein schwarzes
Bild. Lass die Zahlen 72 und 96 dabei nicht im Schleifenrumpf stehen,
sondern benutze verlauf.length und verlauf[zeile].length.
Tipp zu b)
In spalte * 2 steckt, dass die Helligkeit nach rechts zunimmt. Nach unten
nimmt die Zeile zu. Überlege dir, womit du die Zeile malnehmen musst, damit
in der untersten Zeile 71 ein Wert nahe 255 herauskommt.
Tipp zu c)
spalte / 12 sagt dir, im wievielten Feld von links ein Bildpunkt liegt, und
zeile / 12, im wievielten Feld von oben. Ein Feld ist weiß, wenn die Summe
beider Nummern gerade ist. Ob eine Zahl gerade ist, prüfst du mit % 2 == 0.
Tipp zu d)
Ein Bildpunkt gehört zum Rahmen, wenn er nah genug an einem der vier Ränder
liegt. Der obere Rand ist zeile < 4. Der untere Rand ist
zeile >= verlauf.length - 4. Die beiden anderen findest du selbst. Verbinde
die vier Bedingungen mit ||.
Dein erster Filter
Jetzt kommt beides zusammen: ein Gitter lesen, wie auf der letzten Seite, und ein neues Gitter schreiben, wie gerade eben. Das ist schon der ganze Bauplan eines Filters.
Starte das Programm zuerst unverändert: Beide Bilder sehen gleich aus. Ergänze
dann in der markierten Zeile + pStufen und starte es erneut.
Selbstkontrolle
Die markierte Zeile lautet danach:
int wert = pGrau[zeile][spalte] + pStufen;
Das zweite Bild muss nun deutlich heller aussehen. Falls beide Bilder weiterhin gleich aussehen, prüfe, ob du das Programm nach der Änderung erneut gestartet hast.
Ein Filter liefert ein neues Gitter, er ändert das alte nicht.
int[][] neu = new int[pGrau.length][pGrau[0].length];
Diese Zeile steht am Anfang fast jedes Filters. Sie legt ein Gitter mit so vielen Zeilen und Spalten an, wie das Original hat, und das Gitter ist mit Nullen gefüllt, die gleich überschrieben werden. Nur so kannst du das Original hinterher noch danebenstellen.
Danach folgen immer dieselben drei Schritte für jeden Bildpunkt: den alten Wert
aus pGrau[zeile][spalte] lesen, einen neuen ausrechnen und ihn nach
neu[zeile][spalte] schreiben.
Heller und dunkler
Dunkler ist dasselbe wie heller, nur mit einer negativen Zahl. Das Programm zeigt beides neben dem Original.
In heller wird nach oben abgeschnitten, nach unten aber nicht. Trotzdem sieht
das linke Bild richtig aus.
a) Warum stürzt dunkler(grau, 60) nicht ab, obwohl dabei negative Werte
entstehen?
b) Wo genau werden diese negativen Werte abgefangen? Sieh in Leinwand.java nach.
c) Warum ist es trotzdem besser, im Filter selbst zu begrenzen?
Auflösung
a) und b) Die Klasse Leinwand hat eine private Methode begrenzt, die jeden
Wert vor dem Zeichnen auf 0 bis 255 zurechtstutzt. Ein Wert von −40 wird dort
zu 0.
c) Weil das nur beim Zeichnen hilft. Sobald du zwei Filter hintereinander anwendest, rechnet der zweite mit den unbegrenzten Werten weiter – und −40 ist etwas anderes als 0. Auf der Seite Filter als Klassen steckst du Filter zu Ketten zusammen; spätestens dort fällt das auf.
Aufgabe 1: Invertieren und Schwellenwert
Schreibe zwei weitere Punktfilter.
a) invertiert – aus hell wird dunkel und umgekehrt. Ein Wert von 0 wird
zu 255, ein Wert von 200 wird zu 55.
b) schwelle – jeder Bildpunkt wird entweder ganz schwarz oder ganz weiß,
je nachdem, ob er über oder unter einer Grenze liegt. Das Ergebnis hat nur
noch zwei Farben.
Sage vorher: Welche Grenze braucht schwelle, damit von dem Foto ungefähr
die Umrisse der LED übrig bleiben? Du hast auf der vorigen Seite ausgerechnet,
dass die mittlere Helligkeit bei 115 liegt.
Tipp 1: Der Bauplan
Beide Filter haben denselben Rumpf wie heller. Es ändert sich nur die eine
Zeile, in der gerechnet wird:
int[][] neu = new int[pGrau.length][pGrau[0].length];
for (int zeile = 0; zeile < pGrau.length; zeile++) {
for (int spalte = 0; spalte < pGrau[zeile].length; spalte++) {
neu[zeile][spalte] = /* hier */;
}
}
return neu;
Tipp 2: Die beiden Vorschriften
Invertieren: Du suchst eine Rechnung, die 0 auf 255 schickt und 255 auf 0. Probiere sie mit beiden Werten durch, bevor du sie hinschreibst.
Schwellenwert: Hier wird nicht gerechnet, sondern entschieden. Ein if mit
einem else genügt.
Aufgabe 2: Kontrast
Heller machen verschiebt alle Werte. Kontrast dagegen zieht sie auseinander: Was über der Mitte liegt, wird heller, was darunter liegt, wird dunkler.
Die Vorschrift lautet
neu = 128 + (alt - 128) * faktor
a) Rechne von Hand nach, was aus den Werten 40, 128 und 220 bei faktor = 2
wird. Welche davon musst du begrenzen?
b) Schreibe die Methode kontrast(int[][] pGrau, int pFaktor) und probiere die
Faktoren 2, 3 und 4 aus.
c) Was passiert bei faktor = 0? Sage es vorher, dann probiere es.
Tipp: Begrenzen an zwei Enden
Anders als bei heller läuft der Wert hier nach beiden Seiten aus dem
Bereich heraus. Zwei if-Abfragen nacheinander sind völlig in Ordnung:
if (wert < 0) { wert = 0; }
if (wert > 255) { wert = 255; }
Zum Weiterdenken
schwelle ist der Filter, mit dem Scanner-Apps arbeiten, bevor sie Text
erkennen. Eine feste Grenze funktioniert dabei schlecht: Auf einem Foto mit
Schatten ist dieselbe Grenze in der einen Bildhälfte zu hoch und in der anderen
zu niedrig.
Wie könnte ein Schwellenwert aussehen, der sich an die Umgebung anpasst? Skizziere die Idee in Worten – programmieren kannst du sie, sobald du die Seite Weichzeichnen durchhast.