Der Schuhkarton als Archivbox für die Fotosammlung hat ausgedient: Bilder sind heute digital und landen auf der Platte. Ein kommandozeilenbasiertes Perl-Frontend kategorisiert und archiviert die Schnappschüsse und stöbert in der privaten Bildersammlung.
Neulich entdeckte ich, wie leicht Linux per USB die Bilder von einer Digitalkamera liest. Bei der Gelegenheit nahm ich mir vor, endlich Ordnung in alle neuen Schnappschüsse sowie die 5000 privaten Fotos zu bringen, die seit einigen Jahren als JPEG-Bilder auf meiner Festplatte herumlungern. Das Problem ist nur, die Files konsequent zu beschriften.
Wer aus dem Urlaub mit 500 Fotos heimkommt, setzt sich nicht hin, um alle mit Texten wie “Ich mit Blumenkette neben dem Mietauto auf Hawaii 2002” zu beschreiben. Meist landen alle Bilder unkommentiert in einem Verzeichnis. Vier Wochen später sind sie vergessen. Will man nach Jahren die Hawaii-Fotos von 2002 ansehen, geht die Sucherei los.
Programme wie iPhoto auf dem Mac oder das brandaktuelle Adobe Photoshop Album erfüllen zwar die meisten Aufgaben, aber grafische Oberflächen von proprietären Produkten sind schnell ausgereizt. Üblicherweise dauert es nur wenige Tage, bis ich an ihre Grenzen stoße und lautstark eine programmierbare Schnittstelle fordere.
Diese und die nächste Ausgabe des Perl-Snapshots zeigen eine programmierbare Bilderverwaltung mit allen Schikanen, die sich auch – aber nicht nur – von der Kommandozeile aus bedienen lässt. So kann man Abfragen in alter Unix-Tradition mit Pipes verknüpfen und filtern, dass es nur so schnackelt.
Die Namensfrage
Als Erstes war zu klären, wie die Bilder eindeutig zu bezeichnen sind, also welche ID jedes Foto erhält. Nach einigem Herumprobieren fand ich es am einfachsten, das genaue Datum und die Uhrzeit zu verwenden, an dem ein Bild aufgenommen wurde:
2002-03-01_22:13:01 2002-03-01_23:22:17 ... 2003-02-13_11:07:13
Voraussetzung dafür ist allerdings, dass das Datum in der Kamera richtig eingestellt ist und man nicht mehr als ein Bild pro Sekunde schießt. Digicams speichern die Sekunden-genauen Informationen als Metadaten in der Bilddatei; Perl-Module wie »Image::Info« extrahieren die Daten wieder. So lassen sich auch Fotos richtig einordnen, die aus Versehen in falscher Reihenfolge archiviert wurden oder in einem alten Unterverzeichnis verstauben. Außerdem kann man blitzschnell feststellen, ob ein unbekannt aussehendes Bild schon in der Datenbank liegt oder nicht: Das Datum dient dabei als eindeutiger Stempel.
Es wäre mühsam, jede Datei einzeln zu beschreiben, gerade wenn sich viele Bilder stark ähneln. Praktischer sind daher Text-Tags, die auf einen Schlag gleich einem Schuhkarton voller Bilder zugeordnet sind. Ein Bild kann beliebig viele Tags erhalten, ein Tag kann beliebig vielen Bildern zugeordnet sein.
Die Bilder liegen – nach Datum sortiert – in Unterverzeichnissen auf der Festplatte, die beschreibenden Tags in einer MySQL-Datenbank. Abbildung 1 zeigt die Umsetzung. Das Beispiel verwendet Ober- und Unterkategorien: Alle Bilder, die Freunde zeigen, erhalten das Freunde-Tag; für Max und Schorsch gibt es zusätzlich ein eigenes Tag.

Abbildung 1: Bild »2002-03-01_22:13:01« (rot) gehört zu den Kategorien »Freunde« und »Max«. Dem zweiten Bild (grün) ist ebenfalls das »Freunde«-Tag zugeordnet, es gehört aber zusätzlich zur Kategorie »Schorsch«.
Ab in die Datenbank
Image Database (kurz »idb«) heißt das im Folgenden vorgestellte Kommandozeilen-Frontend für das neue System. Gerade von den Bahamas zurückgekommen hänge ich die Kamera an meine Linux-Kiste und tippe:
$ idb --import --tag "Bahamas 2003" /mnt/cam/dcim/100_fuji
Schon wandern alle JPEGs aus dem Kameraverzeichnis »100_fuji« in die Datenbank. Liegen die Bilder verstreut über einige Verzeichnisse auf dem Laptop, ist auch das kein Problem. Sogar die länglichen Optionen lassen sich abkürzen:
$ idb -i -t "Tirol 2002" /samba/laptop/fotos/*
Die Shell ersetzt das Sternchen durch alle Dateien und Unterverzeichnisse in »/samba/laptop/fotos/«; das »idb«-Kommando liest daraus alle Bilder und beschriftet sie mit dem Tag »Tirol 2002«.
Manchmal tauchen beim Durchforsten des Verzeichnis-Waldes alte JPEGs auf, die man nicht mühsam durchsehen und mit Tags beschreiben will. Obwohl Tags später hilfreich sind, ist niemand gezwungen gleich eins anzugeben. Also erst mal nur archivieren:
$ idb -i uralt.jpg 2002-03-01_23:22:17
Jeder »–import«-Aufruf gibt die neuen ID(s) der archivierten Fotos aus. Auch nachträglich lässt sich noch ein Tag an ein Bild hängen, wenn man die ID angibt. Statt der ID genügt auch der Name der Datei, denn »idb« wird den Zeitstempel auslesen, zur Datenbank gehen, die ID ermitteln und das Tag anhängen:
$ idb -t "Freunde" 2002-03-01_23:22:17 $ idb -t "Schorsch" uralt.jpg
Pro Bild sind beliebig viele Tags erlaubt, im vorliegenden Fall ist das Bild sowohl mit »Freunde« als auch mit »Schorsch« verknüpft. Die List-Funktion zeigt nun für »uralt.jpg« zwei Tags an:
$ idb -l uralt.jpg uralt.jpg: Freunde, Schorsch
Bilder wiederfinden
Um die Bilder zu einem gegebenen Tag aus der Datenbank zu holen, kommt die Option »–search« zum Einsatz, abgekürzt »-s«:
$ idb -s "Schorsch" 2002-03-01_23:22:17
Wer statt der ID den Pfad zur Bilddatei benötigt, setzt zusätzlich die Option »-p« ein. Eine exakte Tag-Angabe ist nicht nötig: Die SQL-Wildcard »%« ersetzt beliebig viele beliebige Zeichen.
$ idb -s "Freu%" -p /pics/2002/03/01/22:13:01.jpg /pics/2002/03/01/23:22:17.jpg
Wenn es zu viele Treffer hagelt, kommt »idb -g« (Grep) als Filter zum Einsatz. Der folgende Aufruf holt aus allen mit »Italien« gekennzeichneten Bildern nur jene heraus, die ein Tag führen, in dem »Meer« vorkommt:
$ idb -s "%Italien%" | idb -g "%Meer%" -p /pics/2001/07/2001-07-07_12:21:15.jpg
Das erste »idb« schickt zeilenweise passende IDs durch die Pipe, die der zweite Aufruf am anderen Ende aufschnappt und weiteren Filterkriterien unterwirft. Die »-g«-Option bewirkt, dass »idb« nicht alle Bilder in der Datenbank durchsucht, sondern nur die, deren IDs durch Stdin hereinkommen.
Natürlich kann man ein getaggtes Bild wieder von seiner Kategorie befreien:
$ idb -u "Schorsch" 2002-03-01_23:22:17
Der Aufruf entfernt das »Schorsch«-Tag von Bild »2002-03-01_23:22:17«, es belässt aber die »Freunde«-Kategorie.
Module, Module
Zu Beginn zieht »idb« (siehe Listing 1) eine Reihe von Zusatzmodulen herein, die allesamt leicht vom CPAN zu holen und zu installieren sind. »Log::Log4perl« gibt Warn- und Fehlermeldungen aus, »Image::Info« extrahiert Metadaten aus JPEG-Bildern, »File::Path« erzeugt mit »mkpath()« beliebig tiefe Verzeichnisse und »File::Copy« kopiert Dateien.
Das später besprochene »Getopt::Long« schlägt über »Pod::Usage« dem Benutzer die Anleitung um die Ohren, falls er ungültige Optionen eingibt. Das Programm nutzt auch das Modul »CameraStore«: Diese Datenbankschnittstelle wird im nächsten Snapshot besprochen.
Die Image-Datenbank speichert Informationen auf zweierlei Art: Bilddateien wandern in Jahr/Monat/Tag-spezifische Verzeichnisse unter »$IMG_FILE_DIR« (Zeile 8 in »idb«). Ist es auf »/pics« gesetzt, kopiert »idb« ein Bild mit der ID »2002-03-01_23:22:17« nach »/pics/2002 /03/01/23:22:17.jpg« und erzeugt die dafür notwendigen Unterverzeichnisse automatisch.
Listing 1: Image Database »idb«
001 #!/usr/bin/perl
002 ###########################################
003 # Mike Schilli, 2003 (m@perlmeister.com)
004 ###########################################
005 use warnings;
006 use strict;
007
008 my $IMG_FILE_DIR = "/pics";
009
010 use CameraStore;
011 use Log::Log4perl qw(:easy);
012 use Image::Info qw(image_info);
013 use File::Path;
014 use File::Copy;
015 use Getopt::Long;
016 use Pod::Usage;
017
018 my $ID_REGEX = qr#^(d{4})-(dd)-(dd)
019 _(dd):(dd):(dd)$#x;
020 my $TS_REGEX = qr#^(d{4}):(dd):(dd)s
021 (dd):(dd):(dd)$#x;
022 my $PIC_REGEX = qr#(.jpg)$#i;
023
024 Log::Log4perl->easy_init(
025 { file => 'stderr',
026 level => $WARN,
027 layout => "%p %m%n"});
028
029 GetOptions(
030 "import" => my $import,
031 "tag=s" => my $tag,
032 "untag=s" => my $untag,
033 "filter" => my $filter,
034 "list" => my $list,
035 "grep=s" => my $grep,
036 "search=s" => my $search,
037 "paths" => my $paths) or
038 pod2usage();
039
040 my $db = CameraStore->new();
041
042 if($search) {
043 for($db->search_tag($search, $paths)) {
044 print "$_n";
045 }
046 exit 0;
047 }
048
049 my $in = get_input_sub();
050
051 while($_ = $in->()) {
052
053 if($import) {
054 add_file($db, $_, $tag);
055 next;
056 }
057
058 my($id) = (-f) ? file_info($_) : $_;
059 unless(defined $id and
060 $id =~ /$ID_REGEX/) {
061 ERROR "Image $_ not in DB";
062 next;
063 }
064
065 if($tag) {
066 $db->add_tag($tag, $id);
067 } elsif($untag) {
068 $db->delete_tag($untag, $id);
069 } elsif($grep) {
070 print "$_n" for
071 $db->search_tag($grep,
072 $paths, $id);
073 } elsif($list) {
074 print "$_: ", join(', ',
075 $db->list_tags($id)), "n";
076 } else {
077 pod2usage("Options error");
078 }
079 }
080
081 ###########################################
082 sub add_file {
083 ###########################################
084 my($db, $ofile, $tag) = @_;
085
086 my($stamp, $dir, $file) =
087 file_info($ofile);
088
089 return undef unless defined $file;
090
091 if(!-d $dir) {
092 mkpath($dir) or
093 LOGDIE "Cannot mkpath $dir";
094 }
095
096 copy($ofile, "$dir/$file") or
097 LOGDIE "$ofile > $dir/$file failed";
098
099 $db->add_image($stamp,
100 "$dir/$file", $tag);
101 print "$stampn";
102 }
103
104 ###########################################
105 sub file_info {
106 ###########################################
107 my($ofile) = @_;
108
109 my ($suffix) = ($ofile =~ $PIC_REGEX);
110
111 unless($suffix) {
112 ERROR "Unknown image type: $ofile";
113 return undef;
114 }
115
116 my($y, $m, $d, $h, $mi, $s) =
117 image_date($ofile);
118 return undef unless defined $s;
119
120 my $stamp = "$y-$m-${d}_$h:$mi:$s";
121 my $dir = "$IMG_FILE_DIR/$y/$m/$d";
122 my $file = "$h:$mi:$s$suffix";
123
124 return($stamp, $dir, $file);
125 }
126
127 ###########################################
128 sub image_date {
129 ###########################################
130 my($file) = @_;
131
132 my $info = image_info($file);
133
134 if ($info->{error} or
135 ! exists $info->{DateTime} or
136 $info->{DateTime} !~ $TS_REGEX) {
137 WARN "No timestamp from $file";
138 return undef;
139 }
140 return($1, $2, $3, $4, $5, $6);
141 }
142
143 ###########################################
144 sub get_input_sub {
145 ###########################################
146 my @items = ();
147
148 if(@ARGV) {
149 push @items, @ARGV;
150 } else {
151 while(<STDIN>) {
152 chomp;
153 push @items, $_;
154 }
155 }
156
157 return sub {
158 if(@items and -d $items[0]) {
159 my $dir = shift @items;
160 unshift @items,
161 grep /$PIC_REGEX/, <$dir/*>;
162 }
163 return shift @items;
164 };
165 }
166
167 __END__
168
169 =head1 NAME
170
171 idb - Image database client
172
173 =head1 SYNOPSIS
174
175 # Import and tag
176 idb -i -t tag [file|dir] ...
177 # Filter files and tag
178 ls *.jpg | idb -t tag
179 # DB search for tags, print paths
180 idb -s search_pattern -p
181 # Grep for tags in files
182 idb -g search_pattern [file|dir] ...
183 # List files/tags
184 idb -l [file|dir] ...
Datenbank und Directory
Die Datenbank erhält hierüber folgende Informationen: die ID des Bildes, den Pfad, unter dem es abgespeichert wurde, und etwaige Tags, die am Bild kleben (Option »-t«). Kommen später weitere Bilder hinzu, sortiert sie das Verfahren automatisch in die richtige Aufnahme-Reihenfolge. So kann man gemütlich per Filebrowser oder Imageviewer durch die Verzeichnisse streunen und das Geschehen verfolgen.
Ein vielseitiges Programm wie »idb« versteht viele Optionen. Zum Glück erleichtert »Getopt::Long« die Arbeit (Zeile 29 bis 37): Man gibt die ausgeschriebenen Optionen an (»list« für »–list«), definiert, ob sie allein stehen oder ein String-Argument erwarten (»=s«), und weist Referenzen auf Skalare zu. Die Skalare erhalten bei Flags einen binären Wert (wahr: Option ist gesetzt). Bei String-Optionen erhalten sie den Wert, den der Benutzer per Kommandozeile angegeben hat. Ist eine eindeutige Zuordnung möglich, versteht »Getopt::Long« auch Abkürzungen, etwa »-l« statt »–list«, wenn keine andere Option mit »l« anfängt.
Die Zeilen 18, 20 und 22 kompilieren als Vorbereitung die regulären Ausdrücke »$ID_REGEX«, »$TS_REGEX« und »$PIC_REGEX«. Sie dienen dazu, richtige IDs, Datumsstempel und Dateinamen von JPEG-Bildern zu erkennen. Durch die Klammern extrahieren sie die interessanten Informationen, die später als »$1«, »$2« und so weiter vorliegen.
Das Skript nutzt »Log::Log4perl« für Warn- und Fehlermeldungen. Zeile 24 initialisiert es auf den Warn-Level, leitet Meldungen nach Stderr um und legt das Format auf »%p%m%n« fest. Das bedeutet: erst die Priorität (»%p«: Warn, Error …), dann die Meldung »%m« und am Ende ein Newline (»%n«). Zeile 40 initialisiert ein »CameraStore«-Objekt. Die Methoden dieser Schnittstelle zur Datenbank nutzt das Skript später, um den Inhalt zu manipulieren.
Wenn der Benutzer »–search« wählt, möchte er nach Tags in der Datenbank stöbern. Dann ist »$search« gesetzt und Zeile 43 führt eine Datenbankmethode aus, die eine Liste mit den passenden Image-IDs zurückliefert. Die nachfolgende »print«-Funktion schreibt die IDs einfach zeilenweise auf Stdout. So können auf der Kommandozeile nachgeschaltete Filter die IDs nutzen, um das Ergebnis zu verfeinern. Zeile 46 bricht nach getaner Arbeit das Programm ab.
Der Rest von »idb« beschäftigt sich mit Dateien und IDs, die via Kommandozeile oder Stdin hereinkommen. Hierzu holt sich Zeile 49 einen Funktionszeiger, den sie von »get_input_sub()« erhält (ab Zeile 144 definiert). Dieser Pointer zeigt auf eine Funktion, die bei jedem Aufruf die nächste zu bearbeitende Datei (oder ID) liefert, unabhängig davon, wie der Benutzer dies auf der Kommandozeile festlegt. Egal, ob Dateien oder Verzeichnisse hereinkommen (Letztere werden transparent ausgelesen), und unabhängig davon, ob via »@ARGV« oder durch Stdin: Die von »get_input_sub()« als Referenz zurückgegebene Funktion gibt bei jedem Aufruf die nächste zu bearbeitende Datei (oder ID) zurück.
Geschlossene Gesellschaft
Diese etwas unkonventionelle Technik lässt sich durch eine Closure[2] realisieren. Das ist eine anonyme Funktion (sie hat keinen Namen) und lässt sich daher nur über einen Funktionspointer ansprechen. Sie läuft in dem Kontext, in dem sie erzeugt wurde, egal von wo aus man sie aufruft – eine Closure schließt dazu alle lexikalischen Variablen ein, auf die sie zugreift. Das macht sich »get_input_sub()« zu Nutze:
- Zeile 146 definiert das lokale Array »@items«.
- Die Zeilen 148 bis 155 füllen das Array mit Dateien oder
Verzeichnissen, ob von der Kommandozeile oder von Stdin. - Zeile 157 gibt eine Referenz auf eine namenlose Funktion
zurück, die in den Zeilen 158 bis 163 definiert ist. - Diese Funktion (die Closure) nutzt das eben erzeugte Array
»@items« und hat bei jedem Aufruf Zugriff auf dessen
Werte. - Das Array ist in der Closure also eine Art Instanzvariable
für Arme.
Ist der nächste Wert ein Verzeichnis, entfernen die Zeilen 160 und 161 das Element aus dem Array und schaufeln stattdessen (unter Umständen viele) in ihm liegende Bilddateien nach – vorausgesetzt die Files passen zum regulären Ausdruck »$PIC_REGEX«.
Die While-Schleife in Zeile 51 ruft die Closure-Funktion über ihre Referenz auf: »$in->()« liefert mit dem eben beschriebenen Mechanismus eine Bilddatei nach der anderen. Ist das Import-Flag gesetzt, ruft Zeile 54 die ab Zeile 82 definierte Funktion »add_file()« auf und übergibt ihr eine Referenz auf das »CameraStore«-Objekt, den Dateinamen und ein eventuell gesetztes Tag.
Die Funktion »add_file()« nimmt in Zeile 86 die Dienste von »file_info()« zu Hilfe. Diese Info-Funktion öffnet das Bild, extrahiert das Datum und gibt die folgenden Werte als Liste zurück: »$stamp« (ID), »$dir« (das Bildverzeichnis »…/JJ /MM/DD«) und »$file« (Bildname »SS :MM:ss.jpg«). »file_info()« nutzt hierzu »image_date()« (sie ist ab Zeile 128 definiert), die ihrerseits das CPAN-Modul »Image::Info« verwendet, um die Datumsinformation aus dem JPEG-Bild herauszuholen. In »$TS_REGEX« liegt der reguläre Ausdruck, der auf den Zeitstempel in meiner Fuji Finepix passt (Format »JJ:MM:TT HH:mm:ss«). Für andere Kameras ist der Ausdruck eventuell anzupassen. Da er Klammern enthält, kann Zeile 140 auf die in Zeile 136 ermittelten Teilkomponenten per »$1«, »$2« und so weiter zugreifen.
Wieder zurück zum Hauptprogramm: Zeile 58 ermittelt zu jedem Bild, das noch als Datei vorliegt, die entsprechende ID. Dazu greift sie ebenfalls auf »file_info()« zurück, fängt aber nur das erste Argument auf und ignoriert den Rest. Falls von dort nichts Gescheites zurückkommt, liegt das Bild offensichtlich nicht in der Datenbank, woraufhin das Programm in Zeile 61 einen Fehler meldet und mit »next« zum nächsten Bild weiterspringt.
Tags zu Bildern hinzufügen und wieder entfernen ist Aufgabe der Datenbankschnittstelle, die Zeilen 66 und 68 rufen einfach die entsprechenden Methoden auf und übergeben ihnen die ID des entsprechenden Bildes und den Tag-Wert. Ist die Grep-Option aktiv, gibt Zeile 70 die ID des gerade untersuchten Bildes aus – aber nur, wenn die Datenbankschnittstelle mittels »search_tag()« bestätigt, dass ihm das Tag tatsächlich anhaftet. Falls nicht, gibt sie nichts aus und der nächste Filter wird die ID nie zu Gesicht bekommen.
Die List-Option schließlich lässt das Skript in den Elsif-Block ab Zeile 73 springen, den Namen des Bildes ausgeben und die von der Datenbank mit »list_tags()« ermittelten Tags kommasepariert daneben stellen. Fertig!
Kamera läuft
Um die gerade auf der Kamera gespeicherten Bilder in die Image-Datenbank zu importieren, tippt man (am Beispiel der Fuji Finepix) einfach Folgendes ein:
$ idb -i -t "Erste Bilder" /mnt/cam/dcim/100_fuji
Mit dem im nächsten Shop vorgestellten »CameraStore.pm« kann\’s anschließend losgehen. Bis dann! (fjl)
|
USB-Kamera |
|---|
|
Folgende Root-Kommandos schalten unter einem standardmäßig ausgelieferten Red-Hat-8.0-System das USB-Modul zu und mounten die Digitalkamera ins Verzeichnis »/mnt/cam«: # modprobe usbcore # modprobe usb-uhci # modprobe usb-storage # mount -t auto /dev/sda1 /mnt/cam |
|
Infos |
|---|
|
[1] Listings: [ftp://www.linux-magazin.de/pub/listings/magazin/2003/04/Perl] [2] Information zu Closures: [http://www.perl.com/pub/a/2002/05/29/closure.html] |
|
Der |
|---|
|
|
Copyright © 2002 Linux New Media AG






