Spaß mit EXIF und datetime-Objekten in Python3
7. Januar 2022 um 14:13 Uhr von Atari-Frosch
Seit Frühjahr 2009 fotografiere ich regulär mit digitalen Spiegelreflexkameras. Diese Kameras schreiben eine Menge interessante Informationen in die EXIF-Daten der Bilddateien – zum Beispiel den Zeitstempel der Aufnahme. Allerdings geht das so ein bißchen in die Hose, wenn man verpeiltermaßen vergißt, der Kamera zu erklären, wann Sommer- und wann Normalzeit ist. Dann hat man unter Umständen ein Foto, auf dem zufälligerweise eine Kirchturm- oder Bahnhofsuhr abgebildet ist mit einem Zeitstempel, der eine Stunde vorher oder danach anzeigt.
Das ist mir schon einige Male passiert, und ich habe jetzt nicht wenige Fotos, bei denen die EXIF-Zeitstempel halt nicht stimmen. Und so nebenbei dann auch die Dateinamen nicht, denn die kommen zwar als IMG_nnnn.JPG von den Kameras (wobei nnnn einfach eine laufende Nummer ist, die auf 0001 zurückspringt, wenn die 9999 erreicht wurde), aber direkt nach der Übertragung auf den PC lasse ich da bereits ein eigenes Python-Script drüberlaufen, das die Zeitstempel aus den EXIF-Daten ausliest und die Dateinamen ändert in yyyymmdd-hhmmss_nnnn.jpg. Die Aufgabe war also, zum einen die Zeitstempel direkt in den EXIF-Daten der Fotos zu korrigieren und zum anderen auch die Dateinamen anzupassen.
Hinweis: Das ist jetzt nicht so wirklich eine Anleitung, eher mal wieder so ein „Froschs Lernkurve war mal wieder sehr flach“.
In dem Script, dem ich den Namen correcttime.py verpaßt habe, hatte dann gleich mehrere Fehler bzw. Probleme:
1. datetime- und time-Objekte
Ich hatte über längere Zeit schlicht übersehen, daß es im Modul datetime eine Unterklasse strptime gibt und hatte mich schon gewundert, warum da keine ist – denn daß ich einen Zeitstempel als String bekomme und den erst in ein datetime-Objekt wandeln muß, ist ja jetzt nicht sooo selten. Ich hatte nun ersatzweise die gleichnamige Klasse aus dem Modul time verwendet. Dieses Modul wandelt einen String mit Hilfe einer „Vorlage“ in ein time- bzw. datetime-Objekt um. Es wäre aber wohl zu einfach, wenn die beiden gleichnamigen Funktionen auch das gleiche Ergebnis liefern würden. Tun sie nämlich nicht. Und so fiel mir die Verwendung des „falschen“ strptime auf die Füße, als ich mit dem Ergebnis eine datetime-Funktion ausführen wollte, in diesem Fall datetime.datetime.timedelta. Geht nich', sagte mir Python.
Im hybriden Pythonfoo des Chaosdorfs, in das ich mich dann gestern Abend einklinkte, wurde ich also zunächst darüber aufgeklärt, daß datetime halt doch ein eigenes strptime kennt, welches das erwartete Format erzeugt.
Aber warum überhaupt mit datetime-Objekten hantieren, wenn es doch nur darum geht, eine Zeitangabe wahlweise eine Stunde vor- oder zurückzustellen? Klar, könnte ich auch anders lösen, aber es wäre deutlich umständlicher: Da gibt es nämlich noch so lustige Dinge wie Tages-, Monats- und Jahreswechsel sowie Schaltjahre. datetime ist genau dafür da, daß man sich darum nicht kümmern muß. Hier kann ich den ausgelesenen Zeitstempel, wenn er mal zum datetime-Objekt geworden ist, direkt manipulieren und zum Beispiel mit der Funktion timedelta einfach eine Stunde abziehen oder draufsetzen, ohne mich um Tageswechsel etc. kümmern zu müssen. Und das Ergebnis wird dann einfach wieder mit strftime zum String zurückgewandelt und kann wieder in die Datei geschrieben werden. Das ist auch sinnvoll, wenn, wie in diesem Fall, nicht umständlich mit diversen Zeitzonen jongliert werden muß, sondern es aufgrund der Umstände wirklich nur darum geht, den Zeitstempel stur um eine ganze Stunde vor- oder zurückzustellen.
2. EXIF-Daten mit Python3 manipulieren
Es wäre aber zu einfach gewesen, wenn mein Script nach dieser Korrektur sauber durchgelaufen wäre. Ich hatte mit dem Umbenennungs-Script für die Dateinamen der Fotos nur EXIF-Daten ausgelesen und dafür das Modul exifread verwendet, aber bisher keine Methode gefunden, auch wieder welche zu schreiben. Auf Konsolen-Ebene gibt es das Paket exiftools, das ich mit einem subprocess.call aufrufen wollte. Das warf mir aber auch noch eine Fehlermeldung. Zunächst eine ganz einfache, das war ein Syntax-Fehler gewesen. Aber nach dessen Korrektur lief es immer noch nicht.
Das Pythonfoo machte mich auf das Modul exif aufmerksam, und ich stellte fest, daß ich das Debian-Paket python3-exif tatsächlich bereits irgendwann mal installiert hatte. Trotzdem ließ sich das Modul nicht importieren; der Python3-CLI erzählte mir, es sei nicht vorhanden. Ich installierte es dann nochmal mit python3 -m pip install exif, und siehe da, nun wollte es auch importiert werden. Damit wurde die Sache deutlich einfacher.
Ich ersetzte zunächst die Befehle zum Einlesen, die sich auf exifread bezogen, durch die entsprechenden exif-Befehle. Dabei stellte ich fest, daß exif nicht, wie exifread, nur das einliest, was man explizit braucht, sondern daß es das komplette Bild in den Speicher holt. Also nicht nur die EXIF-Daten selbst, sondern auch den ganzen Klopper an Bilddaten, die überhaupt nicht verändert werden sollen. Die EXIF-Daten können dann im RAM manipuliert werden. Danach wird das gesamte Bild wieder in eine (andere) Datei geschrieben. Ich ließ das Script dafür einen Unterordner im jeweiligen Bildverzeichnis anlegen und die editierten Dateien dort hinein schreiben – zur Sicherheit.
Dieses Vorgehen, also das komplette Bild zu laden, wäre verständlich bei Daten, deren Länge sich durch die Manipulation verändert, oder auch dann, wenn zusätzliche EXIF-Datensätze oder ein vorher nicht vorhandener Thumbnail eingefügt oder wenn EXIF-Datensätze gelöscht werden sollen. Aber die Zeitstempel sind hinterher exakt genauso lang wie vorher. Trotzdem ist auch deren Manipulation nur möglich, indem das Bild komplett geladen, editiert und dann wieder abgespeichert wird. Bei ganzen Bilderserien wird dieser Vorgang durch die Zeit zum Lesen und Schreiben natürlich deutlich verlängert. Mit exiftools ist zumindest die Manipulation der Zeitstempel ohne diesen umständlichen Umweg möglich; das hatte ich vorher direkt an der Konsole ausprobiert.
Aber gut, ich brauche das ja „nur“ für einen Teil der bisherigen (digitalen) Fotos, denn mittlerweile schreibe ich mir die Umstellung auf Sommer- und Normalzeit in den Terminkalender, so daß ich sie nicht mehr verpasse. Und dann hoffe ich sehr, daß es das EU-Parlament wirklich endlich mal schafft, diese Uhren-Umstellerei abzuschaffen, wie es 2019 eigentlich schon für 2021 vorgesehen war.
3. Dann waren da noch die Dateinamen …
Ich hatte vereinzelt schon innerhalb von Ordnern die Umbenennung der Dateinamen händisch vorgenommen, ohne die entsprechenden EXIF-Daten zu ändern. Deshalb ist die Anpassung nicht immer zwingend notwendig bzw. sinnvoll. Ich setzte also einen optionalen Parameter, und wenn der angegeben ist, werden die Dateien auch noch umbenannt.
Dabei hatte ich erst noch den Fehler gemacht, nicht die Kopien mit den korrigierten EXIF-Zeitstempeln im Unterverzeichnis, sondern die unveränderten Dateien umzubenennen, so daß ich hinterher eine Serie mit korrekten Zeitstempeln in den EXIF-Daten, aber dem falschen Zeitstempel im Dateinamen und eine zweite mit weiterhin falschen EXIF-Daten, dafür aber korrekten Dateinamen hatte. m)
Und so sieht das Ergebnis derzeit aus:
#!/usr/bin/python3 # -*- coding: utf8 -*- ### bigrenaming/correcttime version 0.1 ### © Atari-Frosch 2022-01-06 import os from sys import argv, exit from datetime import timedelta, datetime from exif import Image, DATETIME_STR_FORMAT if len(argv) == 1: print("No arguments given. Stop.") exit() else: filepath = "" namechange = False for arg in range(len(argv)): if argv[arg] == "-p": filepath = argv[arg + 1] if not filepath.startswith("/"): filepath = "/" + filepath if argv[arg] == "-t": timediff = argv[arg + 1] if not timediff.startswith ("+") and not timediff.startswith("-"): print("Wrong timediff format. Stop.") exit() if argv[arg] == "-n": namechange = True if filepath != "" and os.path.exists(filepath): targetpath = filepath + "tcorr/" if os.path.exists(targetpath) == False: os.mkdir(targetpath) logfile = filepath + "timecorrection.log" log = open(logfile, "w") filelist = os.listdir(filepath) for file in range(len(filelist)): imgname = filelist[file] if imgname.endswith(".jpg"): fullpath = filepath + imgname logtext = "reading " + filepath + imgname + "\n" log.write(logtext) jpgpath = filepath + "/" + imgname with open(jpgpath, 'rb') as jpgfile: img = Image(jpgfile) origtime = img.datetime_original digitime = img.datetime_digitized logtext = "found original timestamp " + origtime + "\n" log.write(logtext) oldtime = datetime.strptime(origtime, "%Y:%m:%d %H:%M:%S") mydelta = int(timediff[1:]) # works only with full hours! if timediff.startswith("+"): newtime = oldtime + timedelta(hours = mydelta) elif timediff.startswith("-"): newtime = oldtime - timedelta(hours = mydelta) replacetime = newtime.strftime('%Y:%m:%d %H:%M:%S') img.datetime_original = replacetime img.datetime_digitized = replacetime newjpg = targetpath + imgname with open(newjpg, 'wb') as newimg: newimg.write(img.get_file()) logtext = "changed original and digitized time to " + replacetime + "\n" log.write(logtext) if namechange == True: # format of imgname: 20130125-000248_0343.jpg h = imgname.split("_") remainpart = str(h[1]) newimgname = newtime.strftime('%Y%m%d-%H%M%S') + "_" + remainpart fulloldimgname = targetpath + imgname fullnewimgname = targetpath + newimgname os.rename(fulloldimgname, fullnewimgname) logtext = "renamed " + imgname + " to " + newimgname + "\n" log.write(logtext) log.close() else: print("File path does not exist or no file path given. Stop.")
Ja, ich weiß, da muß mal noch richtiges Logging rein.
Und dann wären da noch die analogen, gescannten Fotos, Dias und Negative, die einen korrekten Zeitstempel für date_original und date_digitized brauchen, aber das wird nochmal 'ne Ecke umständlicher, weil ich hier nicht einfach beiden EXIF-Einträgen denselben Zeitstempel verpassen kann. Da kommt dann außerdem noch das Problem dazu, daß ich nicht für alle diese Bilder genaue Tagesdaten habe (von der Uhrzeit reden wir da noch gar nicht), sondern oft nur den Monat und das Jahr weiß. Naja, schaumermal.