Wie können wir Zeit in einem Bild darstellen ?
Man könnte Uhren zerfließen lassen oder einfach doch ein Zeitraffer-video machen. Aber das geht noch besser in einem einzelnen Bild.

Dazu nehmen wir viele Bilder des selben Motivs, die zu verschiedenen Zeiten aufgenommen wurden.

Ziel ist dieses Bild:

Dazu brauchen wir nur ein kleines Python-Script:

Was sollte unser Skript können?

  • JPG Dateien verarbeiten
  • Spaltenbreiten berechnen
    • Warnung, wenn es mehr Bilder als Pixel gibt
    • Sollte sich die Bildbreite nicht ganzzahlig durch die Anzahl der Bilder teilen lassen, verteilen wir die übrigen Pixel gleichmäßig.
    • Rückmelden, wie breit die einzelnen Spalten werden.
  • (optional) Unterverzeichnisse rekursiv verarbeiten
  • Input und Output bild haben die selbe Größe
    • Warnung, wenn nicht alle Bilder gleich groß sind.

timeslice-helper.py

Parameter:

  • Pfad zum Import-Ordner
    • In diesem Verzeichnis wird auch unser fertiges Bild gespeichert.
  • Boolean, ob Unterordner verarbeitet werden sollen.
    • default: nicht verarbeiten
  • (optional) Dateiname des fertigen Bilds.

Imports

  • glob
  • os
  • sys

Zuerst überprüfen wir ob der Pfad, den wir als Parameter bekommen haben auch ein Verzeichnis ist, das existiert. Falls nicht, wars das hier schon. Als nächsten Parameter bekommen wir einen Boolean, der entscheidet ob wir direkt ein Verzeichnis verarbeiten oder uns zuerst die Verzeichnisse zusammensuchen.

# Main
if __name__ == "__main__":

    path = sys.argv[1]
    path = os.path.abspath(path)
    if not os.path.isdir(path):
        sys.exit("Path is not a directory or does not exist.")
    rec = sys.argv[2]
    name = ""
    #
    if len(sys.argv) > 3:
        name = sys.argv[3]
    if rec == "True":
        get_dir(path)
    else:
        process_dir(path, name)

Ein einzelner Ordner wird immer mit process_dir() verarbeitet.

Suchen wir uns also zuerst die Ordner zusammen:

def get_dir(path):
    dir = []
    for currentpath, folders, files in os.walk(path):
        for folder in folders:
            print(os.path.join(currentpath, folder))
            dir.append(os.path.join(currentpath, folder))

    for d in dir:
        print("Processing " + str(d))
        process_dir(d, "")

Dazu wird ein Array angelegt, das wir als Ordner-ToDo-Liste nutzen.
Angefangen beim gegebenen Ordner fügen wir so jeden einzelnen Unterordner hinzu. Außerdem geben wir aus, welche Ordner gefunden wurden. So kann man noch abbrechen, wenn man doch den falschen Pfad angegeben hat, bevor die eigentliche Bildbearbeitung beginnt.

Wenn die Liste fertig ist, geben wir jeden Ordner an process_dir() weiter und geben aus, welcher gerade verarbeitet wird.

def process_dir(d, name):
    files = []
    images = []

    # Load Image (JPEG/JPG needs libjpeg to load)
    for file_name in sorted(glob.iglob(d + '/*.jpg', recursive=True)):
        files.append(file_name)
    # check if folder contains no images
    if not files:
        print("This folder contains no images.")
        return

    if check_files(files):
        print("All images have the same size.")
    else:
        print("Not all images have the same size.")
        return

    print("Processing " + str(len(files)) + " images.")

    # Slice and save
    new = slice(files)
    print("Saving image...")
    filename = ""
    if name == "":
        filename = str(d).replace("/", '_')
    else:
        filename = name.replace("/", '_')
    p = "output/" + filename.replace("\\", '_')
    save_image(new, p + '.png')
    print(p + ".png is ready.")

Zuerst brauchen wir 2 Arrays, einmal für die Dateien und dann für die geladenen Bilder.

Aus dem gegebenen Ordner suchen wir uns alle JPG-Dateien und speichern diese. Wenn es keine Bilder gibt, geben wir eine Warnung aus. Das Skript läuft aber weiter, da wir noch andere Ordner in der Warteschange haben könnten.

Dann geht es zu check_files():

def check_files(files):
    width, height = open_image(files[0]).size
    if len(files) > width:
        print("To many images. Each slice would be smaller than 1px.")
        return
    print("The output image will be " + str(width) + "x" + str(height))
    for x in files:
        w, h = open_image(x).size
        if w != width or h != height:
            return False
    return True

Hier überprüfen wir ob alle Bilder das selbe Seitenverhältnis haben. Wenn nicht, können wir mit diesem Ordner direkt aufhören.
Gleichzeitig geben wir aus, welche Maße das Bild haben wird (die selben, wie alle Input-Bilder). Gibt es aber mehr Bilder, als Das Bild Pixel breit ist, stoppen wir auch, da jede „Scheibe“ schmaller als 1px wäre.

Wieder zurück in process_dir() teilen wir noch mit, dass alle Bilder erfolgreich überprüft wurden und beginnen mit der eigentlichen Bildverarbeitung.

Bevor wir uns aber mit diesem Teil beschäftigen, gehen wir noch die letzten Zeilen von process_dir() durch.

Wurde kein Dateiname für den Output angegeben, nehmen wir den Ordnerpfad und ersetzen alle Sonderzeichen durch Unterstriche. Das fertige Bild wird dann im angegebenen Output-Verzeichnis gespeichert. Zudem teilen wir mit, dass das Bild jetzt verfügbar ist.

slice() – Die eigentliche Verarbeitung

def slice(files):
    # Get size
    width, height = open_image(files[0]).size

    # calculate slices
    s = int((width / len(files)))
    print("Each slice will be " + str(s) + "pixel wide.")
    slices = []
    for x in files:
        slices.append(s)
    if (s * len(files)) < width:
        print("Distributing lextover columns.")
        leftover = width - (s * len(files))
        print(str(leftover))
        for i in range((len(files) - 1), 0, -1):
            if leftover > 0:
                slices[i] = slices[i] + 1
                leftover = leftover - 1
                print("Image " + str(i) + " will be 1px bigger.")
        # slices[len(files) - 1] = slices[len(files) - 1] + (width - (s * len(files)))
        print("Leftover pixel-columns were distributed evenly among the leftmost slices.")

    # Create new Image and a Pixel Map
    new = create_image(width, height)
    pixels = new.load()

    current = 0
    # column
    c = 0
    for row in slices:
        pic = open_image(files
) print("Slicing image " + str(c + 1) + " - " + str(slices
) + " Pixels wide.") for i in range(current, current + row): for j in range(height): # print(str(c+1) + " - " + str(i) + " - " + str(j)) # Set Pixel in new image pixels[i, j] = get_pixel(pic, i, j) c = c + 1 current = current + row # Return new image return

Am Anfang berechnen wir, we breit die „Scheiben“ werden. Falls es nicht genau aufgeht, gleichen wir die verbleibenden Pixel noch aus, indem wir allen Spalten von links weg einen Pixel mehr geben bis alle Übrigen aufgebraucht sind.

Anschließend brauchen wir ein neues Bild, dessen Maße ja schon bekannt sind.

Jetzt gehen wir von links nach rechts alle Spalten durch und prüfen jeweils woher (aus welchem Bild) diese Pixel kommel sollen.

Diese Spalten nehmen wir dann aus dem entsprechenden Quell-Bild heraus und schreiben Sie in das neue Bild.

Hier könne wir nun schon unser fertiges Bild zurückgeben.

Dieses Bild besteht aus ca 2000 Einzelbildern, die über 13 Stunden lang von einem RaspberryPi aufgenommen wurden.

Das ganze Skript zum selbst ausprobieren liegt unter hier auf Github.