Linux: Skript zum Einfügen von EXIF-Informationen in gescannte Bilder

Fast jeder, der analog fotografiert, kennt das Problem: Die Kontrolle der Belichtung ist erst möglich, wenn der Film entwickelt wurde, aber mangels akribisch notierter Belichtungsdaten fällt das Lernen aus den eigenen Fehlern schwer. Warum gibt es eigentlich kein Exif für Kleinbildfilme? Gibt es doch! Die Nikon F100 zeichnet auf Wunsch auch Belichtungsdaten in einem internen Speicher auf. Was fehlt, ist ein bequemer Weg, um diese als Exif-Tags in gescannte Bilder zu übertragen.

Die Lösung

Ende der 90er Jahre hat Nikon mit der F100 eine semiprofessionelle Kleinbildkamera auf den Markt gebracht, die zwar auf analogen Film belichtet, aber bereits Eigenschaften einer Digitalkamera aufweist: Sie besitzt einen internen Speicher, in dem die Daten der fotografierten Bilder gespeichert werden können. Mit Hilfe eines speziellen Lesegerätes, dem MV-1, können diese Daten als Textdateien auf einen Computer übertragen werden.
Jedoch ist es reichlich unhandlich, die Bildparameter getrennt von den eigentlichen Bildern aufzubewahren. Praktischer wäre es, wenn man beim Betrachten eines Bildes die entsprechenden Belichtungsdaten sehen könnte.
Zu diesem Zweck gibt es seit Ende der 90er Jahre das "Exchangeable Image File Format" (Exif), mit dessen Hilfe die zu einem Bild gehörenden Metadaten in den entprechenden Bilddateien gespeichert werden können. Mir fehlte bislang die Möglichkeit, die von meiner Kamera übertragenen Daten automatisiert in meine gescannten Bilder zu übertragen.

Das Lesegerät Nikon MV-1 in seiner ganzen Pracht
Für den stolzen Preis erhält man sogar ein modisches Aufbewahrungsbeutelchen

Nikon MV-1

Wo?

Das Skript kann unter dem Link nikexif-0.0.8.tar.bz2 herunter geladen werden.

Voraussetzung: Bash & Co.

Ich habe mich dazu entschlossen, die Aufgabe mit Hilfe eines Shell-Skripts (bash) zu lösen. Hierfür ausschlaggebend war, dass man die kniffligen Teilaspekte (Analyse der Textdateien, Hinzufügen der Exif-Tags etc.) bereits vorhandenen Standard-Tools überlassen kann. Es handelt sich dabei im Wesentlichen um:

  • sed (zur Auswertung regulärer Ausdrücke)
  • grep (um den richtigen Film und das richtige Foto herauszusuchen)
  • basename (liefert einen Dateinamen ohne Pfad und Erweiterung)
  • dirname (liefert den Pfad zu einem Dateinamen)
  • bc (Kommandozeilenrechner)
  • Exiftool (um die entsprechenden Exif-Tags zu schreiben)

Die Bash war für mich das Mittel der Wahl, da sie genügend „Intelligenz“ bietet, um die einzelnen Stücke zusammenzukitten - C++, Java und Konsorten wären hier wohl bereits die sprichwörtlichen auf Spatzen schießenden Kanonen.

Erste Schritte

Bevor ich zu den Innereien komme, zunächst ein paar Erläuterungen zur Verwendung des Skriptes. Die Dateinamen meiner Bilder setzen sich stets nach dem Schema D<Filmnummer>-<Bildnummer>.jpg (Beispiel: D123-25.jpg) zusammen.
Alle meine Parameterdateien liegen in im Verzeichnis „~/bilder/belichtungsdaten“ und werden (vom Nikon MV-1) nach dem Schema n<Filmnummer 5-stellig>.txt (Beispiel: n00123.txt) benannt. Falls sich das Bild (und die 24 vorherigen Bilder des Films) im aktuellen Verzeichnis befinden, kann man das Skript folgendermaßen aufrufen:

user@host:~> nikexif --paramfile=~/bilder/belichtungsdaten --imagefile=D123-25.jpg

Das Skript wählt anhand des Dateinamen-Präfixes automatisch die richtige Parameterdatei und überträgt die Daten des 25. Bildes als Exif-Tags in die Bilddatei. Exiftool legt dabei automatisch eine Backup-Datei an.

Man kann auch mehrere Bilder (z. B. alle Bilder des Films 123) auf einmal verarbeiten, indem man zum Beispiel find bemüht:

user@host:~> find D123-*.jpg -exec nikexif --paramfile=~/bilder/belichtungsdaten/ --imagefile={} \;

Die Optionen

Hier die verfügbaren Optionen im Einzelnen:

user@host:~> nikexif --help

nikexif v0.0.8

mandatory options:
--paramfile=PATH or FILE  Location of parameter file(s)
--imagefile=FILE          Image file to process

optional;-) options:
--fileprefix=PREFIX       Part of the filename before the picnumber
                          (default: everything until the first dash)
--rollnumber=NUMBER       Number of film roll to identify the parameter file
                          (default: grabbed from the file prefix)
--rollnumberoffset=NUMBER If provided, NUMBER is added to the provided/detected
                          roll number
--imageindex=INDEX        Index of the image on the film roll
                          (default: index of image file in the list of all
                          files of the same prefix (i. e. fileprefix*))
--imageindexoffset=NUMBER If provided, NUMBER is added to the provided/detected
                          image index.
--keepimageindexformat    Don't add a leading zero to one digit numbers
                          (e. g. 1 -> 01)
--dontignorebackups       Don't ignore backup files (i. e. *_original) when
                          determining index number of image
--overwrite               Overwrite image files instead of saving them to
                          imagefile_original.
--verbose                 Some additional output.


–paramfile
Mit Parameterdatei (Paramfile) ist die aus der Kamera ausgelesene Datei mit den Bildinformationen gemeint. Es kann entweder eine einzelne Datei oder ein Verzeichnis mit mehreren Parameterdateien übergeben werden.
Im letzteren Fall wird das Skript versuchen, die zu verwendenden Datei über die Filmnummer zu ermitteln.

– imagefile
Die Bilddatei (Imagefile), die mit den Exif-Informationen aus der Parameterdatei ergänzt werden soll.

–fileprefix (optional)
Der Dateinamens-Präfix ist der Teil des Dateinamens vor der eigentlichen Bildnummer, aus der sich die Filmnummer ergibt. Wenn diese Option nicht gesetzt ist, wird standardmäßig alles bis zum ersten Bindestrich als Präfix interpretiert.
Beispiel: „D123“ für den Dateinamen „D123-25.jpg“

–rollnumber (optional)
Wenn die Option „–paramfile“ auf ein Verzeichnis weist und die Filmnummer (Rollnumber) nicht aus dem Dateinamens-Präfix ableitbar ist, kann sie mit dieser Option gesetzt werden.
Standardmäßig wird das erste Vorkommen einer Zahl im Dateinamens-Präfix als Filmnummer interpretiert.
Beispiel: „123“ beim Dateinamen „D123-25.jpg“

–rollnumberoffset (optional)
Falls die interne Zählung der Kamera nicht mit der eigenen Zählung der Filme übereinstimmt, kann hier ein (positiver oder negativer) Wert angegeben werden, der bei der Bestimmung der Filmnummer addiert wird.
–imageindex (optional)
Hier kann die Stelle des Bildes auf dem Film (Indexnummer bzw. Imageindex) vorgegeben werden. Standardmäßig sucht das Skript nach allen Dateien mit dem selben Präfix und wertet die Stelle der Bilddatei in der alphabetisch geordneten Dateiliste als Indexnummer.
Damit wird festgelegt, in welcher Zeile der Parameterdatei nach den Bildparametern gesucht wird.

–imageindexoffset (optional)
Falls die automatische Ermittlung der Indexnummer stets um einen gewissen Wert abweicht, z. B. weil Bilder gelöscht wurden, kann hier ein Wert eingegeben werden, um den die Indexnummer erhöht (oder verringert) wird.

–keepimageindexformat (optional)
Wenn diese Option gesetzt wird, dann wird die Indexnummer nicht mit Nullen aufgefüllt, das heißt „1“ wird nicht zu „01“ umgewandelt.

–dontignorebackups (optional)
Exiftool erzeugt standardmäßig Backups der veränderten Bilddateien (Dateiname_original.jpg). Diese werden beim Ermitteln der Indexnummer standardmäßig ignoriert. Falls das nicht gewünscht ist, hilft diese Option.

–overwrite (optional)
Diese Option veranlasst Exiftool, auf das Anlegen von Backups zu verzichten.

–verbose (optional)
Mit Hilfe dieser Option können dem Skript zusätzliche Statusmeldungen entlockt werden.

Was läuft ab?

Auswahl des richtigen Bildes

Ich nummeriere meine Filme fortlaufend und die Filmnummer ist stets im Präfix des Dateinamens eines gescannten Bildes enthalten. Somit muss zunächst der Präfix vom Rest des Dateinamens abgetrennt werden.

FILEPREFIX=$(basename $IMAGEFILE | sed -e "s/\(^.*\)-.*/\1/")

Mittels basename werden der Pfadname und die Dateiendung vom Dateinamen getrennt. Das Ergebnis wird sed übergeben. Der reguläre Ausdruck liefert alles bis zum ersten Bindestrich zurück. Nun kann, wieder mittels eines regulären Ausdrucks, die Filmnummer aus dem Präfix extrahiert werden.

ROLLNUMBER=$(echo $FILEPREFIX | sed -e "s/[^[:digit:]]*\([[:digit:]]*\).*/\1/")

Diesmal wird sed angewiesen, das erste Auftreten einer zusammenhängenden Zahl zurückzuliefern. Damit „weiß“ das Skript, um welche Filmnummer es sich handelt. Zu dieser Zahl wird ein eventuell per Kommandozeile übergebener Offset addiert.

Nun ist die Bildnummer an der Reihe. Weil das erste Bild auf einem Filmstreifen (je nach Fingerfertigkeit beim Einlegen des Films) nicht immer die Nummer 1 trägt, kann die Bildnummer nicht direkt aus dem Dateinamen abgeleitet werden. Stattdessen wird die Stelle des Bildes innerhalb einer alphabetisch geordneten Liste aller Dateien mit dem selben Präfix als Bildnummer interpretiert.

IMAGEINDEX=$(ls $(dirname $IMAGEFILE)"/$FILEPREFIX"* | sort | grep -n -e "^$(dirname $IMAGEFILE)/$(basename $IMAGEFILE)\$" | sed -e "s/^\([[:digit:]]*\).*/\1/")

Die Ausgabe des Befehls ls wird sort übergeben. Aus der so sortierten Liste wird unsere Bilddatei mittels grep herausgefiltert. Dabei bewirkt der Schalter „-n“, dass das Ergebnis inklusive einer Zeilennummer ausgegeben wird. Diese Zeilennummer wird dann mit dem regulären Ausdruck „liefere alle Zahlen am Zeilenanfang“ als Bildnummer verwendet.
Es ist übrigens zu beachten, dass Exiftool standardmäßig Backupdateien anlegt, die den selben Dateipräfix aufweisen wie die Originaldatei und deshalb die Zählweise durcheinander bringen. Aus diesem Grund muss die oben genannte Befehlskette noch etwas erweitert werden.

IMAGEINDEX=$(ls $(dirname $IMAGEFILE)"/$FILEPREFIX"* | sort | grep -v -e "_original\$" | grep -n -e "^$(dirname $IMAGEFILE)/$(basename $IMAGEFILE)\$" | sed -e "s/^\([[:digit:]]*\).*/\1/")

Hinzugekommen ist ein weiterer Aufruf von grep, das nach „_original“ am Ende eines Dateinamens sucht und Dank des Schalters „-v“ alle Zeilen zurückliefert, die diesen Ausdruck nicht enthalten.
Falls per Kommandozeile übergeben, wird zu der ermittelten Bildnummer zu guter Letzt ebenfalls noch ein Offset addiert.

Wenn keine einzelne Parameterdatei, sondern ein ganzes Verzeichnis übergeben wurde, so muss nun noch die auszuwertende Datei herausgefunden werden.

PARAMFILE="${par_paramfile}n$(printf "%05d" $ROLLNUMBER).txt"

Die Filmnummer wird in eine fünfstellige Zahl mit führenden Nullen umgewandelt und nach dem Muster <Pfad>n<fünfstellige Filmnummer>.txt zum Dateinamen zusammen gesetzt. Nun sind alle Informationen verfügbar, um mit der eigentlichen Auswertung beginnen zu können.

Behandlung der Exif-Daten

Eine aus der Kamera ausgelesene Parameterdatei sieht folgendermaßen aus:

Film speed,Film number,Camera ID
100,170,___
Frame number,Shutter speed,Aperture,Focal length,Lens maximum aperture,Metering system,Exposure mode,Flash sync mode,Exposure compensation value,EV difference in Manual,Flash exposure compensation value,Speedlight setting,Multiple exposure
01,60,F4,35(28-70),F2.8,Matrix,P,Front curtain sync,0.0,-3.0>,-0.7,TTL auto flash,None
02,60,F4,35(28-70),F2.8,Matrix,P,Front curtain sync,0.0,-3.0>,-0.7,TTL auto flash,None
03,60,F4,35(28-70),F2.8,Matrix,P,Front curtain sync,0.0,-3.0>,0.0,TTL auto flash,None
.
.
38,60,F4,28(28-70),F2.8,Matrix,P,Front curtain sync,0.0,-2.8,0.0,TTL auto flash,None

Wie man erkennen kann, wird ab der vierten Zeile je ein Bild pro Zeile beschrieben. Die Werte sind stets mit einem Komma getrennt. Deren Bedeutung wird netterweise in der zweiten Zeile erläutert; an erster Stelle steht immer die zweistellige Bildnummer. Das Skript filtert mit Hilfe von grep die entsprechende Zeile heraus und spaltet sie anhand der Kommas (Setzen der Array-Trennzeichen-Variable $IFS) ganz unbürokratisch in den Array $params auf.

IFS=$',' params=($(grep -e "^$IMAGEINDEX," "$PARAMFILE"))

Nun werden die einzelnen Werte der Reihe nach ausgewertet und es wird je ein Array mit Tag-Namen und Tag-Werten aufgebaut. Die verwendeten Tag-Namen und -Werte habe ich der Exiftool-Homepage entnommen.

#######################################
#now define the list of tags and values
#######################################

#----------------------------------------------------------------------------------------------
#(skip 1st column as it only contains the picture number. therefore:) 2nd column: Exposure time
#----------------------------------------------------------------------------------------------
addtagname "Exif:ExposureTime"
if [[ "${params[1]:${#params[1]}-1}" != "\"" ]]; then
  addtagvalue "$(echo "scale=8; 1/${params[1]}" | bc)"
else
  addtagvalue "${params[1]:0:${#params[1]}-1}"
fi

Wurde mindestens eine Sekunde belichtet, dann wird die Belichtungszeit in Sekunden angegeben (z. B. 1.5„) und das letzte Zeichen sind Anführungsstriche. Wenn dies nicht der Fall ist, dann ist der angegebene Wert der Nenner des Bruches 1/Nenner. Die Umrechnung in einen Dezimalbruch übernimmt der Kommandozeilenrechner bc.

#-------------------------------------------------
#3rd column: Aperture (omit leading character "F")
#-------------------------------------------------
addtagname "Exif:FNumber"
addtagvalue "${params[2]:1}"

Die Blende kann fast unverändert übernommen werden. Lediglich der führende Buchstabe „F“ wird entfernt.

#------------------------
#4th column: Focal length
#------------------------
addtagname "Exif:FocalLength"
addtagvalue "$(echo ${params[3]} | sed -e "s/^\([[:digit:]]*\)[^[:digit:]].*/\1/")"

Als Brennweite werden alle Zahlen bis zu einer Nichtzahl interpretiert. Grund hierfür ist, dass an dieser Stelle auch der Brennweitenbereich von Zoom-Objektiven in Klammern notiert wird (z. B. 35(28-70).

#------------------------
#5th column: Max aperture
#------------------------
addtagname "Exif:MaxApertureValue"
addtagvalue "$(echo ${params[4]} | sed -e "s/F\{0,1\}\([[:digit:]]*\.\{0,1\}[[:digit:]]*\)[^[:digit:]]*.*/\1/")"

Die größte Blende wird mittels dem regulären Ausdruck „überspringe ein eventuell führendes F, nimm alle Zahlen bis zu einem Punkt, den Punkt und alle Zahlen nach dem Punkt“ ermittelt.

#-------------------------
#6th column: Metering mode
#-------------------------
tmp=""
if [[ "${params[5]}" = "Matrix" ]]; then
  tmp=5
fi
if [[ "${params[5]}" = "Center weighted" ]]; then
  tmp=2
fi
if [[ "${params[5]}" = "Spot" ]]; then
  tmp=3
fi
if [[ -n "$tmp" ]]; then
  addtagname "Exif:MeteringMode"
  addtagvalue "$tmp"
else
  echo "Warning: Unknown value for metering mode."
  exitwarning="true"
fi
unset tmp

Der Belichtungsmodus wird von einer Zeichenkette (Matrix, Center weighted, Spot) in die entsprechenden numerischen Exif-Werte (5, 2, 3) umgewandelt.

#----------------------------
#7th column: Exposure program
#----------------------------
tmp=""
if [[ "${params[6]}" = "P" ]]; then
  tmp=2
fi
if [[ "${params[6]}" = "S" ]]; then
  tmp=4
fi
if [[ "${params[6]}" = "A" ]]; then
  tmp=3
fi
if [[ -n "$tmp" ]]; then
  addtagname "Exif:ExposureProgram"
  addtagvalue "$tmp"
else
  echo "Warning: Unknown value for exposure program."
  exitwarning="true"
fi
unset tmp

Auch das Belichtungsprogramm wird von einer Zeichenkette in die entsprechenden numerischen Werte überführt.

#-------------------------------------
#8th+10th+11th+12th column: Flash mode
#-------------------------------------
tmp=""
if [[ "${params[11]}" != "none" ]]; then
  tmp="Flash setting? "${params[11]}"; Flash sync mode? "${params[7]}"; Flash exposure Compensation? "${params[10]}"; Flash EV difference? "${params[9]}
else
  tmp="none"
fi
addtagname "Exif:Exposure"
addtagvalue "$tmp"
unset tmp

Falls verwendet, wird der Blitzmodus direkt aus den Spalten 8 und 10-12 zusammen gesetzt.

#----------------------------------
#9th column: Manual EV compensation
#----------------------------------
addtagname "Exif:ExposureCompensation"
addtagvalue "${params[8]}"

Die manuelle Belichtungskorrektur kann direkt übertragen werden.

#--------------------------
#12th column: Flash setting
#--------------------------
addtagname "Exif:Flash"
if [[ "${params[11]}" != "None" ]]; then
  addtagvalue "1"
else
  addtagvalue "0"
fi

Wenn der Blitzmodus gesetzt wurde, kann auch dieser Exif-Tag gesetzt werden.

#---------------------------
#13th column: Multi exposure
#---------------------------
#ToDo: currently not handled

Mehrfachbelichtungen kommen in der (Wald-und-Wiesen-) Digitalfotografie nicht so häufig vor, deshalb fehlen mir hier noch die Informationen.

Am Ende wird aus den Arrays eine Zeichenkette mit Exiftool-Parametern zusammen gesetzt, mit der Exiftool dann aufgerufen wird. Voila - das war es auch schon!

Zu Guter Letzt

Einige Scanprogramme (z. B. Vuescan) bieten die Möglichkeit, jedes Bild nach dem Stapelscannen mit einem externen Programm aufzurufen. Auf diese Weise kann man die Exif-Daten direkt nach dem Scannen automatisch eintragen.

Leider speichert die Kamera nicht Datum und Uhrzeit der Belichtung, so dass das Skript diese Information nicht in die Bilddaten übertragen kann. Hier ist ein weiterer Bearbeitungsschritt (recht komfortabel zum Beispiel mit F-Spot) notwendig.

Das Skript ist auf die Verwendung der F100-Daten (ausführliches Datenformat) abgestimmt. Man kann das MV-1, soweit ich weiß, auch an eine F5 oder F6 anschließen, die übertragenen Bildparameter unterscheiden sich jedoch von denen der F100. Falls also jemand das Skript mit einer solchen Kamera einsetzen möchte, so wären entsprechende Anpassungen notwendig. Über Hinweise und Rückmeldungen würde ich mich freuen!