log-in_small.gif (418 Byte)

LOG IN 21 (2001) Heft 5/6

 

Unterrichtsideen für Pixelgrafik


Eine Einführung in PYTHON

von Werner Arnhold


Erste Ideensammlung

   
Der folgende Beitrag ist nicht im eigentlichen Sinne ein Unterrichtsvorschlag, eher eine Sammlung von Ideen zum Thema „Pixelgrafik im Informatikunterricht“. Er enthält Beispiele für Vorgehensweisen, zu vermittelnde Inhalte, programmtechnische Realisierungen (in PYTHON) und einige kleine Hilfen. Die Ideen sind von sehr stark unterschiedlichem Schwierigkeitsgrad, sodass sie nicht direkt einer Schul- oder Klassenstufe zuzuordnen sind. Insoweit richtet sich dieser Beitrag an alle Kolleginnen und Kollegen, die am besten wissen, was sie davon ihren Schülerinnen und Schülern zumuten wollen oder können.
    Tragende Idee der ganzen Sammlung ist das Bemühen, Schülern die Magie der Bildbearbeitungssoftware zu entschleiern und sie verstehen zu lassen, was da im Hintergrund abläuft, wenn Pixelgrafik bearbeitet, verändert, verfälscht wird. Sie erfahren durch eigenes Tun, dass trotz aller Farbe und Form im Hintergrund eine Rechenmaschine arbeitet, die weder Bilder noch Farben kennt, sondern nur Zahlen und Rechenoperationen. Sie erleben durch selbstgeschriebene kleine Programmstücke, wie damit dennoch Bildbearbeitung möglich wird.




Standards

   
Aus der Menge der Dateiformate wurde das PNM-Format (Erläuterung s. u.) ausgewählt, weil es

    PNM ist ein relativ altes Format. Es stammt aus Zeiten, als man erstmals versuchte, Bilder per E-Mail zu verschicken. Da die Mail-Transportwege als nicht 8-Bit-sicher gelten, mussten die Daten vor dem Transport in ASCII-Text umgewandelt werden. Später kam dann aus Gründen der zu transportierenden Datenmengen noch ein Rohformat hinzu.
    PNM-Dateien haben prinzipiell immer den folgenden Aufbau:

1. Dateitypkennung,
2. Breite und Höhe des Bildes in Pixel,
3. bei Graustufenbildern den maximalen Grauwert, bei Farbbildern den maximalen Farbwert je  
      Grundfarbe,
4. mit einem „Gartenzaun“ (#) beginnende Zeilen als Kommentar und
5. je nach Dateityp die Daten als ASCII-Text oder als Binärdaten.

    Es ergeben sich somit 2ö3 Formatvarianten, und zwar:


    Zum Einlesen und formatrichtigen Wegschreiben von Dateien werden im PYTHON-Modul pnmbild.py drei Klassen PBMBild, PGMBild und PPMBild angeboten. Jede Datei enthält im Kopf ein wenig Dokumentation.




Ein Einstieg mit Text

   
Am Anfang steht die Frage, ob man überhaupt mit einem Rechner Bilder malen kann und wenn ja, wie. Ein möglicher Versuch mit einem normalen Texteditor liefert das in Bild 1 wiedergegebene Ergebnis.
    Nun ist aber bekannt, dass der Rechner nur Nullen und Einsen kennt. Wenn man also mit der Suchen- bzw. Ersetzen-Funktion des Editors aus jeder Folge von einem „X“ und einem Leerzeichen eine aus einer „1“ und einem Leerzeichen macht, anschließend aus jeder Folge von zwei Leerzeichen eine aus einer „0“ und einem Leerzeichen, so erhält man Bild 2.
    Wenn man vor diesen Text noch einen Kopf setzt, der das Dateiformat angibt (P1 = ASCII-Bitmap), einen kleinen Kommentar und die Anzahl der Pixel in der Waagerechten und Senkrechten, also etwa so:

P1
# smilie.pbm
23 23

dann kann man das Ganze schon mit einem Grafik-Programm betrachten (Bild 3).
    Es ist leicht erkennbar, dass jede „1“ ein geschwärztes Pixel bedeutet, jede „0“ ein weiß gelassenes.
    Mögliche Aufgabenstellung: Ein Bildformat festlegen (nicht mehr als 40 Spalten wegen der Darstellbarkeit im Editor), die Zeilen mit Nullen füllen (mit je einem folgenden Leerzeichen) und dann im Überschreibmodus gezielt Einsen eintragen. Das Ergebnis im Bildbetrachter und ein Beispiel aus dem Editor zeigen die Bilder 4 (unten) und 5 (nächste Seite).


Bilder errechnen

Schwarz-Weiß-Bilder

   
Ein Rechner kann Bilder nicht malen, er muss sie errechnen. Wählen wir ein einfaches Beispiel, das leicht einsichtig ist. Wir importieren die Klasse PBMBild, bauen eine doppelten Schleife über Zeilen und Spalten und setzten bei jedem Punkt, bei dem die x-Koordidnate durch die y-Koordinate teilbar ist, ein Pixel. Die y-Koorddinaten lassen wir erst bei 1 beginnen. Dann wird das Bild gespeichert.

from pnmbild import PBMBild

dateiname = ‘bild6.pbm’

print ‘Jetzt wird ein Bild erzeugt!’
bild = PBMBild(dateiname, breite = 200, hoehe = 200)
print ‘Fertig!’

print ‘Jetzt werden die Punkte gesetzt.’
for x in range(bild.breite()):
        for y in range(1, bild.hoehe()):
                if x % y == 0:
                        bild.setzen(x, y)
print ‘Fertig!’

print ‘Jetzt wird gespeichert.’
bild.speichern()
print ‘Fertig.’

# Und nun wollen wir das Ergebnis sehen.

import bildbetrachter

print ‘Jetzt wollen wir das Ergebnis sehen.’
print ‘Bitte <Return> drücken!’
raw_input ()

bildbetrachter.zeigen(dateiname)

Das Ergebnis (Bild 6 im Heft S. 78) ist eine grafische Darstellung des Einmaleins.


Graustufenbilder

   
Wir nehmen uns nun des zweiten PNM-Formats an, des Graustufenbildes. Dabei wählen wir nur einen leicht veränderten Algorithmus: Wir schreiben an die Stelle (x|y) nicht eine Null oder Eins, sondern den Rest, der beim Teilen von x durch y entsteht, und zwar als Grauwert:

from pnmbild import PGMBild # PGM = Portable Greymap

dateiname = ‘bild7.pgm’

print ‘Jetzt wird ein Bild erzeugt!’
bild = PGMBild(dateiname, breite = 200, hoehe = 200, maxfarben = 200)
print ‘Fertig!’

print ‘Jetzt werden die Punkte gesetzt.’
for x in range(bild.breite()):
        for y in range(1, bild.hoehe()):
                bild.setzen(x, y, x % y)
print ‘Fertig!’

print ‘Jetzt wird gespeichert.’
bild.speichern()
print ‘Fertig.’

# Und nun wollen wir das Ergebnis sehen.

import bildbetrachter

print ‘Jetzt wollen wir das Ergebnis sehen.’
print ‘Bitte <Return> drücken!’
raw_input ()

bildbetrachter.zeigen(dateiname)


    Das erzeugte Bild (Bild 7 im Heft S. 79) sieht recht dunkel aus, da die Zahlen beim PGM-Format den Weißanteil bedeuten. Eine „0“ bedeutet, dass der Bildschirm an der Stelle des Pixels ganz dunkel ist, „maxfarben – 1“ ist die maximale Helligkeit in weiß, zu der der Monitor fähig ist. Der Parameter „maxfarben“ gibt an, in wieviel Stufen diese Skala unterteilt ist. Ein Wert von „2“ liefert ein Schwarz-Weiß-Bild, mehr als 256 Stufen sind nicht drin.
    Sollte ein Schüler hier den Eindruck von räumlicher Tiefe haben, so kann man ihm ruhig sagen, dass dieser Eindruck durch Rechnung erzeugt wurde und dass dies für alle mit Rechnern generierten Bilder gilt, die den gleichen Eindruck vermitteln.


Farbbilder

    Wir gehen nun zu Farbbildern über. PNM-Bilder arbeiten nach dem RGB-Modell, stellen Bilder also durch ihre
additiven Grundfarben Rot, Grün und Blau dar. Dabei wird ein Bildpunkt jetzt nicht mehr nur durch eine Zahl dargestellt, sondern durch ein Zahlentripel, das den Farbstufenwert des Rot-, Grün- und Blauanteils angibt. Die Bedeutung ist ähnlich wie bei den Graustufen, d. h. der Parameter maxfarben gibt die Abstufung an.
    Wir wählen also der Einfachheit halber als Rotwert die x- und als Grünwert die y-Koordinate, der Blauwert wird durch den Rest bei Division gebildet.
    Alternativ wählen wir diesmal eine Prozedur der Klasse PPMBild. Sie heißt gezielt_anwenden und bekommt einen Prozedurnamen als Parameter. Diese Prozedur muss man selber schreiben. Sie bekommt drei Parameter, nämlich die jeweiligen x- und y-Koordinaten und die Daten des Punktes an der Stelle (x|y). Sie muss als Wert die neuen Daten des Punktes liefern. Damit sieht unser Programm dann so aus (das Bild ist in Bild 8 wiedergegeben):

from pnmbild import PPMBild # PPM = Portable Pixmap

dateiname = ‘bild8.ppm’

print ‘Jetzt wird ein Bild erzeugt!’
bild = PPMBild(dateiname, breite = 200, hoehe = 200, maxfarben = 200)
print ‘Fertig!’

def bunt_malen(x, y, punkt):
            if y == 0:
                    return x, y, 0
            return x, y, x % y

print ‘Jetzt werden die Punkte gesetzt.’
bild.gezielt_anwenden(bunt_malen) # Prozedurname ohne ()!
print ‘Fertig!’

print ‘Jetzt wird gespeichert.’
bild.speichern()
print ‘Fertig.’

# Und nun wollen wir das Ergebnis sehen.

import bildbetrachter

print ‘Jetzt wollen wir das Ergebnis sehen.’
print ‘Bitte <Return> drücken!’
raw_input ()

bildbetrachter.zeigen(dateiname)


Nationales

    Beim Malen der Teilbarkeitsbilder hat sozusagen die Mathematik den Pinsel geführt. Wir wollen das etwas mehr steuern. Dazu werden wir kleine Nationalflaggen erzeugen. Vernünftig ist es, ein Maß zu definierten (hier die Flaggenbreite mit 80 Pixeln) und die restlichen Werte als relative Anteile davon. Der Sinn ist sehr leicht einsichtig, wenn man die Bilder skalieren möchte.
Fangen wir ganz einfach an und definieren ein paar Farben und Zahlen:

# Die Farben

weiss = (255, 255, 255)
rot = (255, 0, 0)
gruen = ( 0, 255, 0)
blau = ( 0, 0, 255)
schwarz = ( 0, 0, 0)
gelb = (255, 255, 0)
purpur = (255, 0, 255)
blaugruen = ( 0, 255, 255)

# Die Flaggenmaße

flaggenbreite = 80
flaggenhoehe = int (0.625 * flaggenbreite)

# Die Flaggenkoordinaten

f0x = 0
f0y = 0
f1x = flaggenbreite - 1
f1y = flaggenhoehe - 1


Dann geht es mit der deutschen Flagge los. Es muss lediglich etwas formuliert werden wie: „Wenn der Punkt im oberen Drittel liegt, wird er schwarz, im zweiten Drittel rot und im unteren Drittel gelb.“ Hier die zwei Prozeduren, welche die Flagge in Bild 9 liefern:

def deutschland_malen(x, y, punkt):
        if y < flaggenhoehe / 3:
                return schwarz
        if y < flaggenhoehe * 2 / 3:
                return rot
        return gelb

def deutschland():
        flagge = PPMBild(‘deutschland.ppm’, flaggenbreite, flaggenhoehe, 256)
        flagge.gezielt_anwenden(deutschland_malen)
        flagge.speichern()

    Frankreich ist (rein programmtechnisch natürlich) das Gleiche wie Deutschland, es muss lediglich statt der y-Koordinate die x-Koordinate untersucht werden. Weitere Länder sind in der Datei flaggen.py zu betrachten (siehe Hinweis „Internetquellen“, S.93); im Bild 10 sind einige Ergebnisse wiedergegeben.
    Ganz nebenbei haben wir noch aus der Datei pnmtools.py einige kleine geometrische Prozeduren benutzt wie z.B. abstand für die japanische Flagge.
    Hier bieten sich gute Möglichkeiten, dass Schülerinnen und Schüler sich (Staaten gibt es ja genug!) eine Flagge mit
einem ihnen angemessenen Schwierigkeitsgrad heraussuchen. Es ergeben sich auch interessante und Erkenntnis bringende Diskussionen, wenn Schüler sich überlegen, ob sie die Flagge der EU oder z.B. die von Mexiko (Bild 11, im Heft S. 80) erzeugen können.



Arbeit mit Farbmodellen

   
Nun geht es um echte Bilder. Woher bekommt man sie? Entweder nimmt man seine Schülerinnen und Schüler mit einer Digitalkamera auf oder man legt Bilder auf den Scanner. In jedem Fall erhält man ein Farbbild, das dann erst ins PNM-Format umgewandelt werden muss. Manche Programme erzeugen eine Endung .pnm, manche .ppm. In jedem Fall wird aber die komprimierte Version verwendet. Sie hat auch den Vorteil, dass unser PYTHON-Modul sie viel schneller einliest und wegschreibt, weil eine schnelle C-Implementierung benutzt werden kann.


Farbmodelle additiv und subtraktiv 

    Damit man mit den Bildern aber so richtig etwas machen kann, müssen die zugrunde liegenden Farbmodelle verstanden sein. Sie sollen hier allerdings nicht erklärt werden. Als kleines Beispiel zum Erforschen soll das Programm rgbwuerfel.py dienen, das nach Auswahl eine interaktive Darstellung des gewählten Farbmodells zeigt (Bilder 12 und 13, im Heft S. 81).
    Die Farbmodelle RGB und YMC (oder auch CMY) sind von den Bezeichnungen für Monitore bzw. Farbdrucker bekannt (RGB = Rot, Grün, Blau; YMC = Yellow, Magenta, Cyan, bzw. mit den aus der Farbfotografie bekannten Namen Gelb, Purpur und Blaugrün). Auch die entsprechenden Farbkreise kennen die Schüler aus dem Physikbuch. Aber das Verstehen ist eine andere Sache. Ein kleines Programm kann da unter Umständen helfen, Erkenntnisse zu festigen. Hier die Erstellung der Kreise für das RGB-Bild:

def rgbbild():
        bild = PPMBild(breite = br, hoehe = hoe)
        bild.gezielt_anwenden(plus_rot_in_1)
        bild.gezielt_anwenden(plus_gruen_in_2)
        bild.gezielt_anwenden(plus_blau_in_3)
        bild.speichern(‘rgbkreise.ppm’)


    Ein neues PPMBild wird immer mit Schwarz initialisiert. Man muss also nur noch die Farben hinzufügen (nicht eintragen). Es kommt also auf die Formulierung für die Farbmischung an. Das sieht so aus:

def plus_rot_in_1(x, y, rgb):
        if in_kreis1(x, y):
                return (255, rgb[1], rgb[2])
        else:
                return rgb

def plus_gruen_in_2(x, y, rgb):
        if in_kreis2(x, y):
                return (rgb[0], 255, rgb[2])
        else:
                return rgb

def plus_blau_in_3(x, y, rgb):
        if in_kreis3(x, y):
                return (rgb[0], rgb[1], 255)
        else:
                return rgb


    Man sieht, wie z. B. beim Einmischen von Rot nur der Rotanteil gesetzt wird, die anderen beiden Farbwerte
bleiben. Auf diese Weise entsteht die Farbmischung (Bild 14). Für das YMC-Modell gilt:

def ymcbild():
        bild = PPMBild(breite = br, hoehe = hoe)
        bild.anwenden(weissen)
        bild.gezielt_anwenden(minus_blau_in_1)
        bild.gezielt_anwenden(minus_gruen_in_2)
        bild.gezielt_anwenden(minus_rot_in_3)
        bild.speichern(‘ymckreise.ppm’)


    Im subtraktiven Modell werden die Farben aus dem Weiß (z.B. des Papiers) herausgezogen. Daher muss unser Bild zuerst einmal vom voreingestellten Wert „Schwarz“ auf „Weiss“ gesetzt werden. Nun die Prozeduren (siehe auch Bild 15, im Heft S. 82):

def minus_blau_in_1(x, y, rgb):
        if in_kreis1(x, y):
                return (rgb[0], rgb[1], 0)
        else:
                return rgb

def minus_gruen_in_2(x, y, rgb):
        if in_kreis2(x, y):
                return (rgb[0], 0, rgb[2])
        else:
                return rgb

def minus_rot_in_3(x, y, rgb):
        if in_kreis3(x, y):
                return (0, rgb[1], rgb[2])
        else:
                return rgb


    Auch hier geschieht, was das Entfernen einer Farbe bedeutet, nämlich das Null-Setzen nur einer Komponente, die restlichen Bereiche bleiben, wie sie sind. Die subtraktiven Grundfarben bleiben automatisch übrig.

Farbauszüge – RGB

    Als erste Übung kann man die Farben nach ihren RGB-Bestandteilen trennen. Wie sieht ein Bild aus, wenn es nur von dem für die Rotanteile zuständigen Elektronenstrahl
gemalt wird? Die entsprechenden gezielt_anwenden-Prozeduren lauten wie folgt:

def RGBrotauszug(rgb):
        return (rgb[0], 0, 0)

def RGBgruenauszug(rgb):
        return (0, rgb[1], 0)

def RGBblauauszug(rgb):
        return (0, 0, rgb[2])

    Wie man sieht, wird von jedem Bildpunkt nur der jeweilige Wert einer Grundfarbe zurückgegeben, die anderen auf Null gesetzt. Wie sehen die Bilder dazu aus? Wir wählen als Original einen Blick auf die Gondelanlegestelle am Markusplatz in Venedig (Bild 16, im Heft S. 82).
Die dazugehörigen Farbauszüge folgen in den Bildern 17a-17c (im Heft S. 83).
    Die Farben der Auszüge wirken zwar eher etwas dunkel, also intensiv, da die Farbmischung aber additiv ist, bedeutet es, dass eine hohe Intensität der beteiligten Grundfarben im Gesamtbild zu einer eher hellen Farbe führt.

Farbauszüge – YMC

    Die subtraktive Farbtrennung nach dem YMC-Modell ist interessant, da sie insbesondere bei Druckern, aber auch im gesamten Druckereiwesen verwendet wird. (Dass in den Druckereien häufig mehr als drei oder vier Farben benutzt werden, liegt daran, dass es sehr schwer ist, preiswerte Farbsubstanzen in der notwendigen Qualität herzustellen, um mit nur drei oder vier Farben auszukommen.)


def YMCgelbauszug(rgb256):
        y, m, c = YMC_aus_RGB( RGB_aus_RGB256(rgb256))
        return RGB256_aus_RGB( RGB_aus_YMC((y, 0.0, 0.0)))

def YMCpurpurauszug(rgb256):
        y, m, c = YMC_aus_RGB( RGB_aus_RGB256(rgb256))
        return RGB256_aus_RGB( RGB_aus_YMC((0.0, m, 0.0)))

def YMCblaugruenauszug(rgb256):
        y, m, c = YMC_aus_RGB( RGB_aus_RGB256(rgb256))
        return RGB256_aus_RGB( RGB_aus_YMC((0.0, 0.0, c)))


    Hier sehen wir eine kleine Besonderheit des Farbenmoduls: Standard sind Farbwerte im Bereich 0 bis 1. Farben aus einem Bild im Bereich 0 bis 255 müssen erst umgerechnet, dann in ein anderes Modell verwandelt, bearbeitet und anschließend wieder zurückgerechnet werden. Die Farbauszüge folgen in in den Bildern 18a-18c (vorige Seite).

Zusammenhang RGB – YMC
    
   
Wie man an den Farbkreisbildern sieht (vgl. Bilder 14 und 15), ergeben die drei additiv gemischten Farben Rot, Grün und Blau die Farbe Weiß. In dem Bereich, wo sich nur Grün und Blau mischen, ergibt sich Blaugrün. Man kann also sagen: Rot und Blaugrün ergeben Weiß oder Rot und Blaugrün sind Komplementärfarben. Das Gleiche gilt für Blau und Gelb bzw. für Grün und Purpur.
    Entsprechend kann man RGB in YMC umrechnen, indem man für die subtraktiven Farben die Werte einträgt, die ihrer additiven Komplementärfarbe noch an Weiß (=1,0) fehlen. Das Bild 19 verdeutlicht den Zusammenhang an einer pastellblauen Mischfarbe und ihren Bestandteilen.

Farbauszüge – YMCK

    Die heutigen Farbdrucker arbeiten mit vier Farben. Sie verwenden zusätzlich Schwarz, und das nicht nur zum Ausdruck von Texten, sondern auch in farbigen Bildern. Wie geht das?
    Schauen wir uns unseren Mischton noch einmal an, diesmal mit den YMC-Werten. Dabei sind die Farbanteile entsprechend ihrer Intensität in der Mischfarbe eingefärbt (Bild 20, im Heft S. 84).
    Wenn man am Farbwähler oder dem RGB-Würfel ausprobiert, was beim Mischen von Farbwerten passiert, so stellt man fest, dass immer dann, wenn die RGB- (und damit auch die YMC-) Werte gleich sind, sich ein Grauton ergibt. Man kann also alle drei YMC-Farben in einen gemeinsamen Anteil – den des geringsten Wertes, hier also Gelb – und die darüber hinaus gehenden Anteile der jeweiligen Farbe aufteilen. Den gemeinsamen Anteil kann man als Grauwert drucken, von den anderen Farben muss man dann nur noch den Überschuss des Farbwertes auf das Papier bringen. Dies verdeutlicht Bild 21 (auch hier in den der Intensität des Beispiel entsprechenden Farbtönen eingefärbt). Das „K“ in YMCK bedeutet übrigens Schwarz und kommt vom Wort Karbon; wobei „K“ deshalb für die „Skelettfarbe“ Schwarz gewählt wurde, um Verwechslungen mit dem „S“ für „Sättigung“ bzw. „Saturation“ auszuschließen. Die Bilder 22a bis 22d (vorige Seite) zeigen die entsprechenden Farbauszüge.
    Die Farbanteile im Vier-Farben-System erscheinen seltsam blass. Im ersten Moment glaubt man an einen Rechen- oder Programmierfehler. Verständlich wird dies, wenn man sich überlegt, dass sie wirklich nur die Farbüberschüsse enthalten.

Farbmodelle – HSV

   
Das RGB-Modell scheint zwar in der Praxis häufig vorzukommen, ist aber gefühlsmäßig schwerer zu manipulieren, da das menschliche Farbempfinden sich nicht an den additiven Grundfarben orientiert. Ähnliches gilt für das subtraktive Modell. Die Parameter Farbton, Sättigung und Helligkeit (hue, saturation, value = HSV) hingegen sind leichter einzuschätzen und zu verändern. Auch hierbei liegen die Werte von Sättigung und Helligkeit wieder im Bereich zwischen 0 und 1. Für den Farbton geht man allerdings von
einem Farbkreis mit einer Parametrisierung in Grad (0 bis 360) aus. Im Bild 23 wird gezeigt, wie die Farben liegen.




Kleines Fotolabor

   
Dinge, die früher im heimischen Fotolabor mit viel Chemie und Wasser zuwege gebracht wurden, gehen mit dem PNM-Bild ganz leicht. Wir machen aus einem Farbbild ein Schwarz-Weiß-Bild. Dazu müssen wir nur den RGB-Wert in einen HSV-Wert umschreiben, davon den Helligkeitswert herausschneiden und in eine entsprechende Grautondatei schreiben. Kleine Besonderheit: Die Anwendungsprozedur wird auf das neu zu erstellende Bild angewandt, dessen Farbwerte aber ignoriert werden. Stattdessen wird die Pixelinformation aus dem alten Bild geholt. Hier das Programm (das Ergebnis ist in Bild 24 wiedergegeben, im Heft S. 86):

from pnmtools import *
from pnmbild import *
from farbe import *
from bildbetrachter import zeigen

bildname = ‘venezia’
sw_bildname = ‘%s_sw.pgm’ % bildname

def vergrauen(x, y, dummy):
        rgb256 = bild.wert(x, y)
        rgb = RGB_aus_RGB256(rgb256)
        hsv = HSV_angepasst(HSV_aus_RGB(rgb))
        return abs_aus_rel(hsv[2])
    
bild = PPMBild(bildname + ‘.ppm’)
sw_bild = PGMBild(sw_bildname, hoehe = bild.hoehe(), breite = bild.breite(), komprimiert = 1)
sw_bild.gezielt_anwenden(vergrauen)
sw_bild.speichern()
zeigen(sw_bildname)

   
Ebenso einfach ist es, ein Negativ zu erzeugen (im Fotolabor sagte man „umkopieren“). Es muss lediglich auf der Ebene des HSV-Wertes der komplementäre Helligkeitswert zurückgeschrieben werden. Hier die anwenden-Prozedur, Ergebnis in Bild 25 (nächste Seite):

def negativ_vergrauen(x, y, dummy):
    rgb256 = bild.wert(x, y)
    rgb = RGB_aus_RGB256(rgb256)
    hsv = HSV_angepasst(HSV_aus_RGB(rgb))
    return abs_aus_rel(1 - hsv[2])

    Zu den gehobeneren Anwendungen im Fotolabor zählten die Arbeiten mit anderen Filmmaterialien, z.B. Strich- oder Dokumentenfilm. Er wird für fotografische Reproduktionen von Text gebraucht und zeigt einen hohen Kontrast, um die fotografierten Texte besser lesbar zu machen. Das können wir in unserem Software-Labor auch. Hier ist nun ein Programmstück zu sehen, das sozusagen eine Belichtungsreihe erzeugt. Im Bild 26 wird ein Ergebnis dieser Reihe gezeigt, bei der die Grenze zwischen Schwarz und Weiß bei 30% liegt.

from pnmtools import *
from pnmbild import *
from farbe import *
from bildbetrachter import zeigen

bildname = ‘venezia’

def trennen(x, y, dummy):
        global schwelle
        rgb256 = bild.wert(x, y)
        rgb = RGB_aus_RGB256(rgb256)
        hsv = HSV_angepasst(HSV_aus_RGB(rgb))
        grauwert = hsv[2]
        return grauwert > schwelle

bild = PPMBild(bildname + ‘.ppm’)
for i in range(1, 10):
        schwelle = i / 10.0
        strichbildname = ‘%s_strich_%.1f.pbm’%\ (bildname, schwelle)
        strichbild = PBMBild(strichbildname, hoehe = bild.hoehe(), breite = bild.breite(), komprimiert = 1)

        strichbild.gezielt_anwenden(trennen)
        strichbild.speichern()
        zeigen(strichbildname)

    Eine Besonderheit ist hier noch anzumerken: Bei völliger Dunkelheit ist es unsinnig, von einem Farbton zu sprechen. Auch ein Grauton, also das gleichmäßige Vorhandensein aller drei Grundfarben hat sinnvollerweise keinen Farbton. Folgerichtig ist in diesem Fall der Farbwinkel im HSV-Modell mit der Konstanten FARBE_UNDEFINIERT festgelegt. Die Umwandlungsprozeduren von HSV in RGB und zurück berücksichtigen diese Besonderheit. Wenn man jedoch von Hand an den Werten etwas ändert, kann es sinnvoll sein, für die Grautonfälle eine Korrektur einzubauen. Die Prozedur HSV_korrigiert setzt den Wert FARBE_UNDEFINIERT, wenn die Farbsättigung 0 ist, die Prozedur HSV_angepasst setzt den Farbwinkel auf 0, wenn er vorher undefiniert war. Einen Fall, in dem das sinnvoll sein kann, zeigt das nächste Beispiel.

Falschfarbenbilder 

    Wir verfälschen die venezianische Schönheit. Unsere anwenden-Prozedur bekommt den Farbwert des bestehenden Bildes, wandelt ihn in RGB, dann in HSV, addiert dann den Drehwinkel, hier 90 Grad, auf den bestehenden Farbwinkel, gleicht das Ganze noch mit der 360-Grad-Grenze ab und wandelt anschließend alles wieder zurück. (Nicht vergessen, das Bild 27 unter einem anderen Dateinamen zu sichern!)

def farbe_drehen(x, y, rgb256):
        rgb = RGB_aus_RGB256(rgb256)
        hsv = HSV_angepasst(HSV_aus_RGB(rgb))
        hsv = ((hsv[0] + delta_farbwinkel)%360, hsv[1], hsv[2])
        hsv = HSV_korrigiert(hsv)
        return RGB256_aus_RGB(RGB_aus_HSV(hsv))


Gezielte Fälschungen 
    
   
Aber es lässt sich noch etwas mehr damit machen: Statt pauschal alle Farbtöne zu verändern, kann man sich einen bestimmten Farbton heraussuchen und alle Farben im gesamten Bild, die ihm gleichen, umfärben. Dafür ist das HSV-Modell besonders geeignet, da man dabei zwar den Farbwinkel verändern, Sättigung und Helligkeit jedoch beibehalten kann, wodurch die Lebendigkeit und Struktur einer Bildfläche erhalten bleibt – anders, als wenn einfach eine feste Ersatzfarbe darübergeschrieben würde. Wichtig ist es, bei der Frage, ob ein Farbton zu verändern ist oder nicht, mit Toleranzen zu arbeiten, deren Feineinstellung man ausprobieren muss, um das gewünschte Ergebnis zu erzielen.
Im Bild 28 wird ein Beispiel gezeigt, das auch der Redaktion eines Boulevard-Blattes entstammen könnte, dessen Namen wir aus Gründen der Diskretion lieber verschweigen!




Farbtiefe und Kompression 

   
Komprimierte Datenformate sind häufig und oft auch Voraussetzung für die Nutzung von Mulitmedia-Inhalten überhaupt. Wie geht das Komprimieren vor sich?
Eine erste Variante ist die Untersuchung des so genannten „rohen“, also des Nicht-ASCII-Formates von PNM. Dabei werden Bitmap-Bilder nicht als „0“ und „1“ abgespeichert, sondern mit je einem Bit pro Pixel. Grau- und Farbbilder speichern ihre Werte als Zahlen, was die Leerzeichen einspart und außerdem einen RGB-Wert immer mit einem Byte verschlüsselt, statt mit ein bis drei (je nach Zahl) in der ASCII-Version.
    Wer mag, kann Versuche mit weiteren verlustlosen Kompressionsverfahren anstellen. Eine einfache Möglichkeit ist es aber auch, die Farbtiefe zu verringern, d.h. pro Grundfarbe nicht mehr 256, sondern weniger Abstufungen zuzulassen. Bei 16 Stufen lässt sich der Farbwert in 4 Bit packen und der Platzbedarf damit gegenüber der „rohen“ Version der Grau- und Farbbilder noch einmal um die Hälfte reduzieren.
    Auf das Programmieren der entsprechenden Packprozeduren wurde hier verzichtet. Wir wollen eher der Frage nachgehen, wie weit die Farbtiefe verringert werden kann, bevor der Informationsverlust wirklich störend wird.
    Das folgende Programm (farbtiefe.py) erstellt eine Reihe von abgestuften Varianten der Lagunenstadt. Ab wann wird der Informationsverlust störend, ab wann wieder interessant? Der Leserin bzw. dem Leser wird hier das Urteil selbst überlassen (Bild 29a, unten, und Bilder 29b bis 29g, nächste Seite).

from pnmbild import PPMBild
from bildbetrachter import zeigen

endung = ‘.ppm’
bildname = ‘venezia’

# Jetzt die Anzahl der Farbstufen festlegen:

stufungen = [32, 16, 8, 6, 4, 3, 2]


def venezia_reduziert(x, y, punkt):
        w = originalbild.wert(x, y)
        return w[0] * farbstufen / 256,\
                 w[1] * farbstufen / 256,\
                 w[2] * farbstufen / 256

originalbild = PPMBild(bildname + endung)
for farbstufen in stufungen:
        neubildname = ‘venezia_%03d’ % farbstufen
        neubild = PPMBild(neubildname + endung, 
   
                    breite = originalbild.breite(),
                       hoehe = originalbild.hoehe(), 
                       maxfarben = farbstufen,
                       komprimiert = 1)

        neubild.gezielt_anwenden(venezia_re
   
     neubild.speichern()

# Und begucken!
        zeigen(neubildname + endung)



Rasterungen
    
   
Raster sind notwendig, damit z.B. Drucker oder Druckmaschinen uns vortäuschen können, sie könnten (z.B. Farbdrucker) ein zartes Pastellgrün oder (z.B. Laserdrucker) ein sanftes Grau produzieren. Nehmen Sie eine starke Lupe oder ein schwaches Mikroskop, und Sie sehen den Betrug sofort. Ein Laserdrucker hat nur eine Farbe, nämlich Schwarz! Wollte er eine graue Fläche wirklich als grau drucken, müsste er Toner in dem entsprechenden Grauton haben. Auch ein Farbdrucker, der ein freundliches Lindgrün produzieren soll, müsste Tinte in genau dieser Farbe besitzen. Stattdessen setzen die Maschinen kleine Punkte ihrer vorhandenen Farben nebeneinander auf das Papier und vertrauen auf die mangelnde Auflösung des Auges und die Farbmischung im Gehirn.
    Vernünftige Druckraster sind ein (zu) umfangreiches Thema. Wir können aber das Prinzip mit einem stark vereinfachten Verfahren deutlich machen. Dazu sind einige Vorüberlegungen notwendig:


Der Bau des Rasters 
    
   
Wir bauen das Raster von Hand. Wir wählen ein Muster von 5ö5 Punkten aus, was stark vergrößert im Bild 31 zu sehen ist.
    Diese werden dann zur Herstellung von 10 verschiedenen Schwärzungs-Intensitäten in einem 3ö3-Muster angeordnet, wie Bild 32 ebenfalls vergrößert zeigt. Das Programm raster.py leistet die Definition dieser Arbeit, das Programm raster_erzeugen.py schreibt das Raster in eine Bilddatei zur späteren Verwendung.


Das Bild als Großpixelmenge 

   
Als nächstes wird das Originalbild in eine Schwarz-Weiß-Version mit nur 10 Graustufen verwandelt (Bild 33, Programm in graustufenbild.py).
Nun geht es los: Aus dem Graubild werden kleine Stücke von ausschnittmass = 4 Pixel Breite und Höhe herausgeschnitten. Ihre durchschnittliche Schwärzung wird berechnet und auf einen ganzzahligen Wert gerundet. Das Komplement zu 10 ergibt dann den Index für das als Ersatz zu verwendende Stück aus der Reihe der 10 Rasterquadrate. Hier die entscheidende Prozedur aus rasterbild.py:


def rasterbild_herstellen():
        y_raster = 0
        for y in range(0, rb_hoehe, ausschnittmass):
                x_raster = 0
        for x in range(0, rb_breite, ausschnittmass):
                teil = graubild.ausschnitt( x, y, ausschnittmass, ausschnittmass)
                if teil == 0:
                            break
                d = graustufen - int(teil.durchschnitt() + 0.5) - 1
                rasterbild. ausschnitt_einfuegen( x_raster, y_raster, rpunkte[d])
                x_raster = x_raster + \ rastermass
                y_raster = y_raster + rastermass

rasterbild.speichern()



    Das Ergebnis wird in Bild 34 gezeigt. Die Darstellung ist verkleinert. Machen Sie sich den Spaß und drucken Sie Ihre Porträts in Originalgröße aus. Sie werden den verblüffenden Effekt feststellen, dass man in normalem Leseabstand nur Punkte, von weitem aber deutlich und meist sogar erkennbar die abgebildete Person sieht.




Ein Bild von Omas Hochzeitsreise 

    Zum Abschluss soll eine historische Aufnahme hergestellt werden, und zwar ein Bild, das unsere Großmutter bei ihrer Hochzeitsreise zu Beginn des vorigen Jahrhunderts in Venedig mit ihrer alten Plattenkamera aufgenommen hat. (Wenn man ein geeignetes Porträt hat, kann sich ein Schüler auch ein Bild seines Großvaters oder seiner Großmutter herstellen, das ihm selbst sehr ähnlich sieht.) Entsprechend der damaligen Mode bekommt unser Bild einen oval ausgefransten Rahmen, der durch eine Maske beim Umkompieren erzeugt wurde. Außerdem wird das Bild durch eine Substanz im Entwicklerbad mit einer leichten Sepia-Tonung eingefärbt, auch das war damals modern.
    Gehen wir zuerst die Sache mit dem Oval an. Die Oval-Linie (Ellipse) ist die Menge aller Punkte, bei denen die Summe der Abstände zu zwei Punkten gleich ist. Bei den Punkten innerhalb des Ovals ist die Abstandssumme kleiner; diese interessieren uns. Um das Oval etwas besser handhaben zu können, definieren wir seine Eigenschaften separat:


x_verschiebung = 0
y_verschiebung = -15
zentrum = bild.breite() / 2 + x_verschiebung, \
                bild.hoehe() / 2 + y_verschiebung

m_abstand = 100             
                                                                        # Abstand der Brennpunkte
m1 = zentrum[0] - m_abstand, zentrum[1] 
                                                                        # Brennpunkt 1
m2 = zentrum[0] + m_abstand, zentrum[1] 
                                                                        # Brennpunkt 2
abstandssumme = 320


    Zu verändernde Werte sind hierbei x_verschiebung, y_verschiebung, m_abstand und abstandssumme. An diesen Werten basteln wir so lange herum, bis das Ergebnis unseren Wünschen entspricht. In einem ersten Durchlauf erzeugen wir zunächst nur die Maske. Hier folgt die anwenden-Prozedur, das Ergebnis sehen Sie im Bild 35 (nächste Seite, im Heft S. 92):


def ovalpunkt(x, y, punkt):
        sum = abstand(x, y, m1[0], m1[1]) + abstand(x, y, m2[0], m2[1])
        if sum <= abstandssumme:
                    return punkt
        return weiss


    Als nächstes soll der ausgefranste Ovalrand erzeugt werden. Das bedeutet für Punkte, die nicht zu weit vom
Rand weg sind, dass sie allmählich in Weiß übergehen sollen. Was heißt aber „allmählich übergehen“?
    Hier hilft uns wieder das HSV-Modell. Ein Farbton bewegt sich auf Weiß zu, wenn sich seine Farbsättigung gegen 0 und seine Helligkeit gegen 1 bewegt. Wir definieren also einen Wert für den Rand, in unserem Beispiel 40 Pixel, innerhalb dessen sich dieser Übergang vollziehen soll. In einer groben Näherung bestimmen wir den Abstand eines Punktes von der Ovallinie als den halben Überschuss der Summe der Abstände zu den Brennpunkten. Die Stufung ist von der Randbreite abhängig, hier also ein Vierzigstel. Um diesen Anteil verringern wir die Sättigung und erhöhen die Helligkeit, aber nicht über die Maximal- bzw. Minimalwerte hinaus. Der Farbwinkel bleibt unverändert. Das Ergebnis wird ins RGB-Modell zurückverwandelt und in das Bild zurückgeschrieben. Hier die Prozedur, das Ergebnis findet sich in Bild 36:

def ovalpunkt_ausgefranst(x, y, punkt):
        sum = abstand(x, y, m1[0], m1[1]) + \ abstand(x, y, m2[0], m2[1])
        if sum <= abstandssumme:
                    return punkt
        rgb = RGB_aus_RGB256(punkt)
        h, s, v = HSV_angepasst(HSV_aus_RGB(rgb))
        distanz = (sum - abstandssumme) / 2.0
        stufung = 1.0 / randbreite
        s = max(0.0, s - distanz * stufung)
        v = min(1.0, v + distanz * stufung)
        rgb = RGB_aus_HSV(HSV_korrigiert((h, s, v)))
        return RGB256_aus_RGB(rgb)

   
Nun fehlt nur noch die Sepia-Tonung. Dabei wird das Farbbild zunächst in ein Schwarz-Weiß-Bild gewandelt. Doch das geht so nicht, da wir ja den Toner-Effekt nur in einem Farbbild erreichen können. Wir legen also für das Pseudo-Schwarz-Weiß-Bild einen Farbwinkel von 25 Grad (in swbild_h) und eine Sättigung von 0.2 (in swbild_s) fest. Dann bleibt als variabler Parameter noch die Helligkeit. Sie wird entweder unverändert übernommen, wenn die Punkte innerhalb der Ovallinie liegen, oder nach dem obigen Muster abgeschwächt. Hier die Prozedur (das Ergebnis in Bild 37, im Heft S. 92):

def ovalpunkt_sw(x, y, punkt):
        sum = abstand(x, y, m1[0], m1[1]) + \ abstand(x, y, m2[0], m2[1])
        rgb = RGB_aus_RGB256(punkt)
        h, s, v = HSV_angepasst(HSV_aus_RGB(rgb))
        h = swbild_h
        s = swbild_s
        if sum > abstandssumme:
                distanz = (sum - abstandssumme) / 2.0
                stufung = 1.0 / randbreite
                v = min(1.0, v + distanz * stufung)
        rgb = RGB_aus_HSV(HSV_korrigiert((h, s, v)))
        return RGB256_aus_RGB(rgb)



Informatische Bildungund Multimedia

    Zum Abschluss dieses Colleg-Beitrags seien dem Autor noch ein paar Bemerkungen zum Verhältnis gestattet, das zwischen informatischer Bildung einerseits und dem aktuellen Trend andererseits besteht, in alle Unterrichtsfächer Themen zu integrieren, die irgend etwas mit Multimedia zu tun haben.
    Wie mit diesem Beitrag gezeigt werden soll, kann durch „Multimedia“ die informatische Bildung in der Schule nicht ersetzt werden. Im Gegenteil: Beide ergänzen sich in wichtiger Funktion. Erst mit informatischer Bildung können Schülerinnen und Schüler die Hintergründe verstehen, die dazu führen, wie „Multimedia“ mit Computern funktioniert. Und sie können damit die kritische Distanz gewinnen, die von allen Medienpädagogen gefordert wird, ohne dass dies nur aus einem Lippenbekenntnis besteht. Wer „hinter die Kulissen“ geblickt hat, wird sich nicht mehr so leicht in die Irre führen lassen.
    Die Basis eines solchen – hier vorgestellten – Exkurses, ist daher ein nachhaltiger Baustein für informatisch und damit zukunftsorientiert gebildete Menschen.


Werner Arnhold
Freie Universität Berlin
Fachbereich Erziehungswissenschaft und Psychologie
GEDIB
Arbeitsbereich „Lehrerfortbildung“
Habelschwerdter Allee 45
14195 Berlin

E-Mail: warnhold@zedat.fu-berlin.de

 



Internetquellen

Quellen zu diesem Beitrag:
http://160.45.133.240/pixelgrafik/index.html


Arnhold, W.: Kleines Vokabelheft Python.
http://160.45.133.240/vok/vokabelheft.html (Stand: Mai 2002).