Fabian Wohlgemuth

digital creative. tool wizard.

AI

  • Generative Art mit Python Pillow

    Heute hat der Hyperfokus mich gepackt und ich hab meine allerersten Versuche mit generativer Kunst gemacht. Was soll das sein? Bei generative art geht es darum, mit Hilfe von Code Dinge zu krerieren. Das können Melodien, Videos oder ganz klassisch 2D Grafiken sein. Für den Anfang ist letzteres meine Wahl gewesen.

    Den Einstieg mache ich mit Python pillow. Einer Library zur Bildmanipulation.

    Den gesamten Code gibt es zur Zeit noch nicht öffentlich zugänglich. Dafür bin ich noch zu sehr im Experimentieren und der Code entsprechend unaufgeräumt. Teile des Codes erkläre ich aber hier.

    Was kreiere ich?

    Spannend ist was anderes, oder? Nun ja, ich fange ja gerade erst an. Von daher gibt’s ersteinmal die Grundlagen.

    Ich habe mir erst überlegt, wie man mit sehr wenigen Elementen zumindest ein stimmiges Bild erschaffen kann. Also habe ich die benötigten Python-Module installiert und losgelegt.

    import random
    from PIL import Image, ImageDraw, ImageColor, ImageFont

    Das pillow Modul ist für das Erschaffen der Grafik verantwortlich. Wir erstellen ein canvas, geben ihm eine zufällige Hintergrundfarbe (Format RGB, mit R, G, B Werten von 0 bis 255) und fangen an zu malen.

    canvas = Image.new('RGB', (256, 256))
    
    random_color = ((random.randint(0, 255), 
                     random.randint(0, 255), 
                     random.randint(0, 255)))
    
    canvas.paste(random_color, [0, 0, 256, 256])

    Jetzt haben wir sogar schon den ersten generativen Part eingebaut. Die Hintergrundfarbe wird per Zufallsgenerator festgelegt. Weil ich die Farben etwas “Pastell”-artig haben möchte, mische ich sie mit reinem Weiß (RGB(255, 255, 255)).

    color_pastel = tuple([round((x + 255) / 2) for x in random_color])

    Als nächstes möchte ich das Bild mit Elementen füllen. Diese sollen eine andere Farbe haben als der Hintergrund. Damit sie erkennbar sind, stelle ich sicher, dass ein bestimmter Kontrast vorherrscht. Dazu bediene ich mich an den gängigen Luminanz-Formeln für Kontraste im Kontext der Barrierefreiheit (Accessibility).

    def color_contrast(color1, color2):
        
        def color_luminance(color):
            rsrgb = color[0] / 255.0
            gsrgb = color[1] / 255.0
            bsrgb = color[2] / 255.0
            r_l = 0.2126 * ((rsrgb + 0.055) / 1.055) ** 2.4 if rsrgb > 0.04045 else rsrgb / 12.92
            g_l = 0.7152 * ((gsrgb + 0.055) / 1.055) ** 2.4 if gsrgb > 0.04045 else gsrgb / 12.92
            b_l = 0.0722 * ((bsrgb + 0.055) / 1.055) ** 2.4 if bsrgb > 0.04045 else bsrgb / 12.92
            return r_l + g_l + b_l
        
        l1, l2 = color_luminance(color1), color_luminance(color2)
        
        return (max(l1, l2) + 0.05) / (min(l1, l2) + 0.05)

    Für meine Elemente hole ich mir also eine weitere random_color und schaue dann, ob der Kontrast hoch genug ist. Laut WCAG2.0 Richtlinie benötigt man einen solchen Kontrast für dekorative Elemente zwar nicht, aber allein aus ästhetischen Gründen, sortiere ich hier nach Kontrast > 2. Ist die generierte Farbe nicht kontrastreich genug zum Hintergrund, wird eine neue Farbe generiert und das Spiel wiederholt.

    Wenn ich eine passende Farbe gefunden habe, was gerne mal einige tausend Iterationen lang dauern kann, beginne ich mit meinen Elementen. Hier habe ich mich für ein simples Quadrat als Frame und ein Dreieck als Hauptakzent entschieden. Zuerst wird der Frame gemalt.

    draw_image.rectangle((padding, padding, size - padding, size - padding), fill=None, outline=accent_color, width=1)

    padding gibt hier an, wie viel Platz der Frame nach außen zum Bildrand haben soll. In meinem Code ist dieser prozentual an der Gesamtgröße festgelegt. Das Rechteck bekommt keine Fill-Color sondern nur die vorhin randomisiert erstellte accent_color als Outline.

    Und schon geht es weiter zum Dreieck. Ein Dreieck zu erstellen, ist ersteinmal gar nicht schwierig. Wir brauchen drei Koordinatenpunkte und sind fertig. Diese sollen natürlich in einem gewissen Rahmen auf dem Bild platziert werden, damit sie nicht zu nah am Bildrand sind. Wir generieren also die Koordinaten mit entsprechenden Parametern. Zustäzlich gehen wir noch sicher, dass die Dreiecks-Fläche groß genug ist, damit sie erkennbar ist. Somit verhindern wir sehr spitze Dreiecke, die eventuell sogar nur ausssehen, wie Linien. Mit all diesen Anforderungen, wird unser Dreiecks-Generator schnell komplex.

    def random_triangle(seed=None, canvas_size=IMAGE_SIZE, canvas_padding=PADDING, min_area=None):
        
        def random_position(seed=seed, canvas_size=canvas_size, canvas_padding=canvas_padding):
            if seed: random.seed(seed)
            return random.randint(canvas_padding, canvas_size - canvas_padding)
        
        if not min_area: min_area = canvas_size * 25
        position_iterations = 0
        min_area_reduced = False
        while True:
            x1, y1 = random_position(), random_position()
            x2, y2 = random_position(), random_position()
            x3, y3 = random_position(), random_position()
    
            area = abs((x2 - x1) * (y3 - y1) - (x3 - x1) * (y2 - y1)) / 2
            
            position_iterations += 1
            
            if position_iterations >= MAX_ITERATIONS_TRIANGLE:
                min_area_reduced = MIN_AREA
                      
                if area >= min_area_reduced:
                    break  
    
            if area >= min_area:
                break
        
        triangle = ((x1, y1), (x2, y2), (x3, y3))
        
        return triangle, position_iterations

    Mit der selben Farbe wie der Frame, wird nun auch das Dreieck gemalt. triangle ist hier mit der obigen Funktion generiert.

    draw_image.polygon(triangle, fill=accent_color, outline=None)

    Und schon sind wir fertig. Die Datei kann nun gespeichert werden.

    canvas.save(f'./{OUTPUT_FOLDER}/{seed}.{output_format}')

    seed wird später noch spannend. Hier ersteinmal, was wir gespeichert haben:

    Okay, wie oben gesagt. Doch gar nicht so spannend. Und das kann ich in Figma in wenigen Sekunden selbst erstellen und hab dazu auch noch Kontrolle über Farben und Element-Größen. Warum also Generative Art? Nun, ich kann meine Funktion in einem Loop laufen lassen und mehrere Grafiken auf einmal generieren.

    for amount in range(25):
        create_canvas()[0].save(f"./tmp/{amount}.jpg")

    Und das mache ich in Bruchteilen von Sekunden. Da kann Figma natürlich nicht mehr mithalen.

    Und das ist schon die Kunst hinter Generative Art. Ich bin echt begeistert, wie schnell so etwas geht und unglaublich gespannt, was ich noch so hin bekommen kann.

    Was es mit seed auf sich hat, und wie ich beim Bild vom Anfang gelandet bin, kommt dann in Bälde als Teil 2 in diesen Blog. Und ein Behind The Scenes (BTS) Beitrag mit technischen Details zu meinem Python JupyterLab Server gibt’s bestimmt auch in Zukunft noch.

    Wenn Du Dich mit mir über Code im Allgemeinen oder Generative Art im Speziellen austauschen möchtest, schreib mir gerne. Zum Beispiel per Mail.