Bildverarbeitung mit Processing.py

Jeder sein kleiner Warhol

Processing und damit auch Processing.py besitzt ein ganzes Arsenal von Filtern zur Bildmanipulation. Davon möchte ich zu Beginn zwei heraussuchen und damit ein kleines Programm erstellen, dessen Ergebnis ein wenig an die berühmten Siebdrucke des Pop-Art-Künstlers Andy Warhol erinnern soll.

Warhol like

Ich habe dazu ein Photo von unserem Sheltie Joey und mir genommen, das Stefanie Radon vor etwa vier Jahren von uns geschossen hatte, und das mein Facebook-Profil ziert und mit dem filter(THRESHOLD, 0.55) in eine reine schwarz-weiß-Zeichnung umgewandelt. THRESHOLD akzeptiert Parameter zwischen 0.0 und 1.0 – je kleiner der Wert, desto weniger wird angezeigt. Nach einigen Experimenten habe ich mich dann auf 0.55 festgelegt, das brachte in meinen Augen das brauchbarste Ergebnis für dieses Photo.

In der draw()-Funktion habe ich dann das Bild acht mal hintereinander in zwei Reihen gezeichnet und mit dem filter tint(color) jeweils in einer anderen Farbe eingefärbt. Ich habe einige Zeit mit den Farben experimentieren müssen, bis ich das oben angezeigt Ergebnis bekam, mit dem ich nun zufrieden bin.

Der Quellcode

Der Quellcode ist einfach und leicht zu verstehen. Im setup() habe ich das Bild geladen und in eine schwarz-weiß-Version umgewandelt, in draw() habe ich dann die acht unterschiedlich eingefärbten Versionen erstellt. Dabei habe ich eine Schleife über die Liste palette[] der von mir ausgewählten Farben laufen lassen:

palette = [color(205, 133, 63), color(124, 205, 124),
           color(255, 140, 0), color(255, 20, 147),
           color(238, 238, 0), color(224, 102, 255),
           color(151, 255, 255), color(205, 200, 177)]

def setup():
    global jojo
    size(640, 320)
    jojo = loadImage("jojo.jpg")
    jojo.filter(THRESHOLD, 0.55)
    noLoop()

def draw():
    global jojo
    background(51)
    for i in range(len(palette)):
        if (i < 4):
            row = 0
            j = i
        else:
            row = 160
            j = i - 4
        tint(palette[i])
        image(jojo, j*160, row)

Ressourcen

Natürlich können Sie für Ihre eigenen Experimente auch das Photo von Joey und mir nutzen – es steht schließlich auf Flickr und im Fratzenbuch, aber es wäre sicher mehr im Sinne von Andy Warhol, wenn Sie sich eigene Bilder (aus-) suchen, die Sie einfärben und serialisieren wollen.

Filter für die Bildverarbeitung

Processing und damit auch Processing.py bringen eine kleine Sammlung vorgefertigter Filter für die Bildmanipulation mit, die auf jedes Bild angewandt werden können. Die Filter haben folgende Syntax: Entweder

filter(MODE)

oder

filter(MODE, param)

Ob ein Filter einen zusätzlichen Paramter mitbekommen kann, hängt vom Filter ab. Wie die Filter wirken und ob und wie sie einen Paramter mitbekommen, können Sie der folgenden Tabelle entnehmen:

Filter Ergebnis
Originalbild (keinen Filter)
THRESHOLD, Parameter (optional) zwischen 0 und 1, Default 0.5
GRAY, keinen Parameter
INVERT, photographisch gesprochen das Negativ, keinen Parameter
POSTERIZE, zwischen 2 und 255, aber einen richtigen Effekt hat man nur mit niedrigen Werten
BLUR, je größer der Wert, desto verschwommener wird das Bild. Der Parameter ist optional, der Default ist 1
ERODE, keinen Parameter
DILATE (das Gegenteil von ERODE), keinen Parameter
Filter können auch kombiniert werden, hier erst GRAY und dann POSTERIZE

Mit folgendem kleinen Sketch können Sie mit den diversen Filtern spielen (die auskommentierten Teile habe ich für die Thumbnails in obiger Tabelle benötigt):

selectFilter = 8

def setup():
    global img
    # Thumbnails
    # size(160, 120)
    # img = loadImage("abendrot-s.jpg")
    # Volle Größe
    size(640, 480)
    img = loadImage("abendrot.jpg")
    noLoop()

def draw():
    global img
    background(255, 127, 36)
    image(img, 0, 0)
    if (selectFilter == 1):
        filter(THRESHOLD, 0.55)
    elif (selectFilter == 2):
        filter(GRAY)
    elif (selectFilter == 3):
        filter(INVERT)
    elif (selectFilter == 4):
        filter(POSTERIZE, 4)
    elif (selectFilter == 5):
        filter(BLUR, 6)
    elif (selectFilter == 6):
        filter(ERODE)
    elif (selectFilter == 7):
        filter(DILATE)
    elif (selectFilter == 8):
        filter(GRAY)
        filter(POSTERIZE, 4)
    # save("filter020" + str(selectFilter) + ".jpg")

Einfach bei selectFilter den gewünschten Wert (zwischen 0 und 8) eingeben und dann den Sketch laufen lassen. Ihr seid natürlich eingeladen, bei den Filtern, die Parameter zulassen, mit diesen zu spielen.

Die letzte (auskommentierte) Zeile zeigt Ihnen, wie Sie das Ergebnis abspeichern können. Das Format des Bildes erkennt Processing an der Endung.

Filter interaktiv

Screenshot

Noch besser können Sie natürlich die Wirkung der diversen Filterparameter interaktiv mit der Maus erkunden. Ich habe als Beispiel dafür zwei kleine Sketche geschrieben, die einmal POSTERIZE und zum anderen THRESHOLD erkunden.

def setup():
    global img
    size(640, 480)
    frame.setTitle("Posterize interaktiv")
    img = loadImage("abendrot.jpg")

def draw():
    v = map(mouseX, 0, width, 2, 64)
    image(img, 0, 0)
    filter(POSTERIZE, v)

Da die hohen Werte bei POSTERIZE keinen interessanten Effekte mehr liefern, habe ich hier mithilfe der map()-Funktion den Parameter auf die Werte zwischen 2 und 64 begrenzt.

def setup():
    global img
    size(640, 480)
    frame.setTitle("Threshold interaktiv")
    img = loadImage("abendrot.jpg")

def draw():
    v = float(mouseX)/width
    image(img, 0, 0)
    filter(THRESHOLD, v)

THRESHOLD erwartet Werte zwischen 0.0 und 1.0. Daher habe ich einfach den mouseX-Wert durch die Breite des Fensters geteilt. Wegen der Integer-Division von Python 2.7 mußte ich einen der Werte explizit zu einem float konvertieren, um das gewünschte Ergebnis zu erhalten (denn sonst bekommt man nur den Wert Null). So aber wird das Bild, wenn die Maus ganz weit links ist, nur weiß, während es bei einer Mausposition ganz rechts im Fenster fast vollständig schwarz wird. Irgendwo dazwischen liegen die interessanten Ergebnisse. Sie sollten dies mit diversen Bildern ausprobieren, um ein Gefühl für die zu erwartenden Effekte zu bekommen.

Pointillismus

Pointillismus bezeichnet eine Stilrichtung der Malerei, die zwsichen 1889 und 1910 ihre Blütezeit hatte. Pointillistische Bilder bestehen aus kleinen regelmäßigen Farbtupfern in reinen Farben. Der Gesamt-Farbeindruck einer Fläche ergibt sich erst im Auge des Betrachters und aus einer gewissen Entfernung.

Als ich ein kleiner Junge war, hatten wir eine Spielart des Pointillismus auch im Kunstunterricht meiner Schule: Mit einem Locher stanzten wir möglichst buntes Konfetti aus den Farbseiten abgelegter Zeitschriften und klebten dann daraus farbige Bilder auf Vorlagen zusammen. Natürlich hatten wir keine Aktphotos als Vorlage – es war schließlich eine katholische Volksschule und die wilden 1968er lagen auch noch in der Zukunft.

So etwas in der Art kann man natürlich auch leicht in Processing.py nachbilden (wobei die möglichst reinen Farben in dem Beispielprogramm nur annähernd getroffen werden, weil es sich bei dem Ausgangsbild um eine handkolorierte Photographie vermutlich ebenfalls aus dem 19. Jahrhundert handelt1).

Nachkolorierter Akt

Das Programmfenster zeigt links das Ausgangsbild. Rechts entsteht so langsam das aus Kreisen zufälliger Größe zusammengesetzte Zielbild. Dabei besitzen die Punkte einen Ausganswert (radius) von sechs, der mit einem Zufallsfaktor zwischen 0.2 und 1.5 multipliziert wird. (Ich benutze im Programm die randint()-Funktion von Python und nicht die eingebaute random()-Funktion von Processing. Mir ist die Python-Funktion irgendwie sympathischer, aber das ist vermutlich Geschmackssache.)

Bei jedem Durchlauf der draw()-Schleife wird der Farbwert eines zufälligen Punktes im Ursprungsbild ermittelt und dann als Kreis (Punkt) im Zielbild eingezeichnet. Das Ergebnis gleicht dem Ursprungsbild, nur das es den Anschein erweckt, als würde man es durch eine Scheibe Strukturglas, wie sie manchmal Duschen- oder Badezimmertüren zieren, betrachten.

Der Quellcode

import random as r
radius = 6

def setup():
    global akt
    size(800, 640)
    akt = loadImage("akt.jpg")
    background(255)
    frameRate(600)

def draw():
    global akt
    image(akt, 0, 0)
    x = r.randint(0, akt.width - 1)
    y = r.randint(0, akt.height - 1)
    c = akt.pixels[x + y*akt.width]
    zufall = r.randint(2, 15)/10.0

    alpha = 200
    noStroke()
    fill(c, alpha)
    ellipse(x + 400, y, radius*zufall, radius*zufall)

Der Quellcode ist wieder schön kurz und lädt zum Experimentieren ein. Setzt man zum Beispiel die Konstante radius = 3, dann wirkt das Zielbild bedeutend realistischer. Und ein sehr seltsames Ergebnis bekommt man, wenn man die Zeile mit dem noStroke() auskommentiert.

Man muß natürlich nicht unbedingt Kreise zeichnen. Ein Quadrat oder ein Dreieck ergibt noch ganz andere Effekte. Spielen Sie einfach mal ein wenig damit herum. Processing(.py) ist zum Spielen entworfen.

Noch mehr Pointillismus

Wenn ich ehrlich bin, kann das Ergebnis des Programms aus dem letzten Abschnitt weder ästhetisch noch im Sinne des Pointillismus wirklich überzeugen. Das liegt daran, daß im Programm jedes einzelne Pixel befragt und dann als vergrößerter Punkt wiedergegeben wird. So entsteht im Endeffekt so etwas wie ein verwaschenes Original, aber kein Raster. Daher habe ich – nach einer Idee aus dem wunderbaren Buch »Generative Gestaltung« (derzeit leider nur auf englisch verfügbar) – tatsächlich eine Rasterversion des Aktbildes programmiert und das Ergebnis überzeugt mich mehr:

Screenshot

Dafür habe ich zuerst das Bild, das im Original 400 x 640 Pixel groß war, auf 50 x 80 Pixel verkleinert um dann mit

tileWidth = width/float(akt.width)
tileHeight = height/float(akt.height)
posX = tileWidth*gridX
posY = tileHeight*gridY

ein entsprechendes Raster für das immer noch 400 x 640 Pixel große Ausgabefenster zu schaffen. Mit der Formel

greyscale = round(red(cc)*0.222 + green(cc)*0.707 + blue(cc)*0.071)

habe ich danach die abgetasteten Farben in Graustufen gewandelt, die Gewichtungen habe ich dem oben erwähnten Buch »Generative Gestaltung« entnommen, die Wikipedia zum Beispiel nennt andere Gewichtungen, aber auch gleichverteilte Gewichtungen sind möglich und üblich. Hier gibt es also noch Raum für Experimente.

Mit

w = map(greyscale, 0, 255, 12, 0)

habe ich dann den Radius der Kreise in Abhängigkeit von der Graustufe bestimmt: Je dunkler die Graustufe, desto größer der Kreis. Den Wert 12 habe ich experimentell herausgefunden, auch hier ist ebenfalls noch Raum für Experimente. So bekommt man zum Beispiel auch ein nettes Ergebnis, wenn man die Zeile

fill(cc)

durch

fill(greyscale)

ersetzt. Der Processing-Quellcode aus »Generative Gestaltung« zeigt ebenfalls noch ein paar wirklich nette Möglichkeiten, was man mit so einem Grid alles anstellen kann.

Der Quellcode

Hier nun der vollständige Quellcode, er ist – wie fast immer – erfrischend kurz:

def setup():
    global akt
    size(400, 640)
    akt = loadImage("akt50x80.jpg")
    background(255)
    noLoop()

def draw():
    global akt
    for gridX in range(akt.width):
        for gridY in range(akt.height):
            # grid position and tile size
            tileWidth = width/float(akt.width)
            tileHeight = height/float(akt.height)
            posX = tileWidth*gridX
            posY = tileHeight*gridY
            # get current color
            cc = akt.pixels[gridY*akt.width + gridX]
            # greyscale conversion
            greyscale = round(red(cc)*0.222 + green(cc)*0.707 + blue(cc)*0.071)
            # pixel color to fill, greyscale to ellipse size
            noStroke()
            fill(cc)
            w = map(greyscale, 0, 255, 12, 0)
            ellipse(posX, posY, w, w)

Videos sind auch Bilder

Bisher habe ich wenig bis gar nichts zu den Video-Fähigkeiten von Processing geschrieben. Vermutlich lag es daran, daß bei den aktuellen Versionen von Processing die Video-Bibliothek nicht mehr Bestandteil der Standard-Distribution ist, sondern daß man diese gesondert herunterladen muß. Und hier lag auch schon der erste Hase im Pfeffer.

Timeout!!

Denn meine Versuche, die Bibliothek über das Tools-Menü zu installieren, endeten jedesmal mit einem Timeout: Gefühlt einhundert Mal hatte ich es probiert und jedesmal endete der Versuch mit der Fehlermeldung: »Verbindungs-Wartezeit beim Download von Video überschritten«, also einem Timeout. Nachdem ich es beinahe aufgegeben hatte, klappte es beim Versuch 101 dann doch – die Bibliothek war endlich installiert.

Der Rest war einfach (dabei half mir auch eine schöne Video-Tutorial-Reihe von Daniel Shiffman):

Ein Videoplayer im Python-Mode von Processing

Wie hier schon einmal beschrieben, bindet man die Bibliothek in seinen Sketch ein und kann dann einfach loslegen:

add_library('video')

def setup():
    global movie
    size(560, 315)
    movie = Movie(this, "confettisystem.mp4")
    movie.loop()

def movieEvent(movie):
    movie.read()

def draw():
    global movie
    image(movie, 0, 0)

Diese paar Zeilen reichen wirklich aus, um einen Video-Player in Processing.py zu schreiben. Natürlich hat die Video-Bibliothek noch ein paar weitere Methoden, die am häufigsten benötigten sind (in meinen Augen):

  • play() – spielt das Video nur einmal ab (statt loop())
  • pause() – stoppt das Video an der aktuellen Stelle
  • jump() – springt zu einer bestimmten Stelle im Video (Angaben in Sekunden (als Fließkommazahl – also auch 3,57 Sekunden geht))
  • duration() – gibt die Länge des Films zurück (ebenfalls in Sekunden).

Wichtig ist auch noch die Funktion movieEvent() im obigen Sketch. Sie setzt erst die Event-Schleife in Gang, mit der das Video jedesmal, wenn ein neuer Frame bereit ist, diesenim Processing-Fenster anzeigt. Ohne diese seht Ihr nichts.

Aber am Interessantesten ist: Ist der Frame einmal geladen, ist er ein Bild (image). Alle Filter und Bildverarbeitungsfuntkionen, die es in Processing gibt, könnt Ihr daher auch auf Videos anwenden. Ich werde – sobald ich auf Archive.org ein nettes Video gefunden habe – in den nächsten Tagen damit ein wenig experimentieren und dann hier berichten. Still digging!

Die Video-Bibliothek besitzt zusätzlich noch Klassen und Methoden, um auch Live-Videos von einer Kamera direkt zu verarbeiten (Capture()). Aber da die integrierte Kamera meines betagten MacBook Pro nicht funktioniert (sie hatte nie wirklich funktioniert, aber ich hatte sie auch nie benötigt (ich skype aus Datenschutzgründen nie)), konnte ich diese nicht testen. Aber sie funktionieren im Prinzip genauso wie die Video-Funktionen mit gespeicherten Videos. Was die Unterschiede sind, kann man auch in der oben verlinkten Video-Playlist von Daniel Shiffman sehen, der intensiv damit experimentierte.

OpenCV und Processing.py

OpenCV ist eine freie Programmbibliothek mit Algorithmen für die Bildverarbeitung und maschinelles Sehen. Sie ist für die Programmiersprachen C, C++, Python und Java geschrieben und steht als freie Software unter den Bedingungen der BSD-Lizenz. Das »CV« im Namen steht für englisch »Computer Vision«. Und nachdem ich mir kürzlich einige Videos angesehen hatte, in denen Daniel Shiffman Computer-Vision-Algorithmen in Processing (Java) per Fuß implementiert hatte, dachte ich mir, dies müßte doch auch einfacher gehen. Denn immerhin steht OpenCV als Bibliothek für Processing zur Verfügung und diese basiert auf der »offiziellen« OpenCV-Java-API.

OpenCV-Test

Und wirklich, das Schwierigste an dem ganzen Unterfangen war die Installation der Bibliothek. Wie schon hier war das Repositorium für die Processing-Libraries wohl zu stark beansprucht und so bekam ich die Bibliothek erst nach mehrmaligen Versuchen, die jeweils mit einem Timeout abbrachen, heruntergeladen.

Der Rest war dann einfach: Ich habe mich an dieses Beispielprogramm in Processing (Java) von der GitHub-Seite des Projekts gehalten und es nach Python portiert. Das sah dann so aus:

add_library('opencv_processing')

contours = []

def setup():
    global jojosrc, jojodst, contours
    size(840, 420)
    jojosrc = loadImage("jojo2.jpg")
    opencv = OpenCV(this, jojosrc)

    opencv.gray()
    opencv.threshold(120)
    jojodst = opencv.getOutput()

    contours = opencv.findContours()
    print(contours.size())

def draw():
    global jojosrc, jojodst, contours
    image(jojosrc, 0, 0)
    image(jojodst, jojosrc.width, 0)

    noFill()
    strokeWeight(1)

    for contour in contours:
        stroke(0, 255, 0)
        contour.draw()

        stroke(255, 0, 0)
        point = PVector()
        beginShape()
        for point in contour.getPolygonApproximation().getPoints():
            vertex(point.x, point.y)
        endShape()

Das Programm konvertiert ein Farbphoto zu einem Schwarz-Weiß-Bild und zeigt, wo die Konturlinien liegen, nach denen OpenCV entscheidet, was schwarz und was weiß dargestellt wird. Sie können (und sollten – vor allem, wenn Sie ein anderes Photo verwenden) mit dem threshold-Wert herumspielen, damit Sie sehen, was da genau passiert.

Das von mir verwendete Photo (© 2012 by Stefanie Radon) hatte ich auf 420 x 420 Pixel zurechtgeschnitten. Wenn Sie ein anderes Photo mit einer anderen Größe verwenden, müssen Sie natürlich die Größe des Ausgabefensters an dieses Photo anpassen.

Gesichtserkennung mit OpenCV und Processing.py

Eine der meist zitierten Anwendungen von OpenCV ist ja die Gesichtserkennung und da wollte ich mal testen, wie gut dies mit Processing.py und OpenCV funktioniert:

Gesichtserkennung und Processing (Python

OpenCV besitzt mehrere Bibliotheken zur Gesichtserkennung, eine davon ist der Haar Cascade Classifier, der auf ein Paper von Viola und Jones aus dem Jahr 2000 zurückgeht. OpenCV bringt bereits einige vortrainierte Haar Cascade Classifier mit – unter anderem um Gesichter von Menschen oder Katzen zu erkennen. Der Algorithmus ist ziemlich schnell, allerdings – wie Sie sehen werden – nicht ganz fehlerfrei.

In der setup()-Funktion habe ich diesen Classifier initialisiert,

add_library('opencv_processing')

faces = []

def setup():
    global opencv, faces
    size(640, 480)
    opencv = OpenCV(this, "puppen.jpg")

    opencv.loadCascade(OpenCV.CASCADE_FRONTALFACE)
    faces = opencv.detect()
    # print(len(faces))

und zwar den, der Gesichter von vorne erkennen soll. Als Testbild habe ich dieses Photo mit Schaufensterpuppen genommen, denn Schaufensterpuppen sind vermutlich die einzige Möglichkeit, Gesichtserkennungsalgorithmen zu testen, ohne Probleme mit dem Datenschutz zu bekommen. (Das Photo hat Gabi geschossen, ein paar andere Photos mit Schaufensterpuppen habe ich ebenfalls für Tests genutzt – siehe unten)

Der Rest ist straightforward, zuerst wird die OpenCV-Bibliothek geladen und das Array mit den Gesichtern (faces[]) initialisiert. In der setup()-Funktion werden dann alle Gesichter, die der Haar Cascade Classifier erkennt, abgespeichert.

Wenn Sie kontrollieren wollen, ob überhaupt Gesichter erkannt wurden, können Sie sich die Anzahl der erkannten Gesichter mit der auskommentierten print()-Anweisung anzeigen lassen.

Die draw()-Funktion zeigt das Photo und platziert um jedes erkannte Gesicht ein lindgrünes Viereck:

def draw():
    global opencv, faces
    image(opencv.getInput(), 0, 0)
    noFill()
    stroke(0, 255, 0)
    strokeWeight(2)
    for i in range(len(faces)):
        rect(faces[i].x, faces[i].y, faces[i].width, faces[i].height)

Das ist schon alles. Wie man dem Screenshot entnehmen kann, werden zwar die beiden Gesichter der Schaufensterpuppen erkannt, aber mit dem Batik-Muster der Puppe rechts hat der Classifier so seine Probleme. Und das ist kein Einzelfall: Wie Olver Moser in seiner schönen »Einführung in Computer Vision mit OpenCV und Python« berichtet, erkennt der Classifier auch regelmäßig die Rückenlehne seines Stuhls als Gesicht. Hier muß man also entweder einen anderen, rechenintensiveren Classifier, wie zum Beispiel den »HOG Detector« verwenden, oder versuchen, den Haar Cascade Classifier weiter trainiern. Beides ist sehr rechenaufwendig, daher habe ich mich mit dem Ergebnis erst einmal abgefunden.

Hier sind noch ein paar Bilder aus Neuköllner Schaufenstern, mit denen sich der Classifier mal mehr und mal weniger gut geschlagen hat:

Dieses halbverschleierte Gesicht wurde  gut erkannt.

Auch beim Gesicht von dieser Puppe zeigte der Classifier keine Probleme.

Hier will ich nicht meckern, die Gesichter der zwei links stehenden Schaufensterpuppen sind durch Spiegelungen auch kaum zu erkennen.

Nicht nur die Nofretete hat ein Gesicht, sondern auch die Rückenlehen des Sessels im Hintergrund (mit den drei Knöpfen ist sie aber auch ziemlich gesichtsähnlich).

Hier hat der Algorithmus kläglch versagt. Gerade mal das Gesicht eines der Gartenzwerge und die Putte wurden erkannt.

Nun ja, Elvis muß natürlich jeder Classifier erkennen, sonst gibt es Ärger mit den Fans.

Der Quellcode

Hier noch einmal für Neugierige der komplette Quellcode zum Nachprogrammieren:

add_library('opencv_processing')

faces = []

def setup():
    global opencv, faces
    size(640, 480)
    opencv = OpenCV(this, "elvis.jpg")

    opencv.loadCascade(OpenCV.CASCADE_FRONTALFACE)
    faces = opencv.detect()
    # print(len(faces))

def draw():
    global opencv, faces
    image(opencv.getInput(), 0, 0)
    noFill()
    stroke(0, 255, 0)
    strokeWeight(2)
    for i in range(len(faces)):
        rect(faces[i].x, faces[i].y, faces[i].width, faces[i].height)

  1. Fragen Sie nicht, wo ich diese Photographie gefunden habe. Ich habe sie einfach aus den unendlichen Tiefen des WWW gefischt.