Wem das manuelle Erfassen der Referenzkarten zur Farbkorrektur eines digitalen Fotos im letzten Snapshot zu mühselig war, der wird sich dieses Mal an einem Skript erfreuen, das den Vorgang automatisiert.
Letzten Monat ging es darum, den von der Kamera erzeugten Farbstich eines Fotos mit Hilfe von ins Bild gehaltenen Referenzkarten (Abbildung 1) nachträglich zu korrigieren. Diese im Fotofachhandel erhältlichen Plastikkärtchen mit genormten Schwarz, Weiß- und Grauwerten sollten im Bild keinerlei Farbwerte erzeugen, und bieten daher drei Messpunkte für geringe, mittlere und hohe Lichtintensität, an denen man mit dem Fototool Gimp die Farbkurve eines Fotos korrigieren kann.
Abbildung 1: Es gilt, die Farbwerte der drei ins Bild gehaltenen Karten zu ermitteln. Hierzu führt das Skript die Farbwerte entlang der horizontalen Mittelinie ab. |
Wie kann nun ein einfaches Perlskript ohne Einsatz von künstlicher Intelligenz herausfinden, welche Pixelwerte die drei Karten erzeugen, deren Lage im Bild nicht genau bekannt ist? Wenn man es schafft, die drei Karten so wie in Abbildung 1 gezeigt in der Bildmitte aufgefächert zu halten, kann ein Skript auf der (gedachten) horizontalen Mittellinie entlangwandern und die Karten anhand der Pixelwerte entlang der x-Achse ermitteln. Entlang der zu Illustrationszwecken eingezeichnete Linie bleibt die gemessene Lichtintensität relativ weite Strecken konstant, solange die Linie über einer Referenzkarte verweilt. Streift die Linie hingegen den den Bildhintergrund, schwanken die Pixelwerte relativ stark.
Listing graphdraw
erzeugt mit Hilfe des Imager-Moduls vom CPAN den
in Abbildung 2 gezeigten Kurvenverlauf.
Die drei Graphen bilden die Rot-, Grün- und Blauwerte entlang
der in Abbildung 1 eingezeichneten horizontalen Linie in ein
Koordinatensystem ab, dessen x-Achse den X-Koordinaten im Bild entspricht
und dessen y-Wert den jeweiligen Farbanteil von 0 bis 255 repräsentiert.
Abbildung 2: Die Farbwerte des ungefilterten Bildes sind zu zittrig, um die Karten zuverlässig zu erkennen. |
Abbildung 3: Der Blur-Filter mit der Einstellung "Gaussian Blur" und einem Radius von 5 Pixeln bringt Unschärfe und glättet die Wogen der Pixelwerte. |
Abbildung 4: Das mit dem Blur-Filter unscharf gemachte Bild weist glattere Kurvenverläufe auf, an denen man die ins Bild gehaltenen Karten an den flachen Stellen erkennt. |
01 #!/usr/local/bin/perl -w 02 use strict; 03 use Imager; 04 use Imager::Plot; 05 use Log::Log4perl; 06 07 my($file) = @ARGV; 08 die "No file given" unless defined $file; 09 10 my $img = Imager->new(); 11 $img->read( file => $file ) or 12 die $img->errstr(); 13 14 $img->filter( 15 type => "gaussian", 16 stddev => 10 ) or die $img->errstr; 17 18 my $y = int( $img->getheight() / 2 ); 19 my $width = $img->getwidth(); 20 21 my $data = {}; 22 23 for my $x (0..$width-1) { 24 push @{ $data->{ x } }, $x; 25 26 my $color = $img->getpixel( x => $x, 27 y => $y ); 28 my @components = $color->rgba(); 29 for my $color_name (qw(red green blue)) { 30 push @{ $data->{ $color_name } }, 31 shift @components; 32 } 33 } 34 35 my $plot = Imager::Plot->new( 36 Width => 550, 37 Height => 350, 38 GlobalFont => 39 '/usr/share/fonts/truetype/msttcorefonts/Verdana.ttf'); 40 41 for my $color_name (qw(red green blue)) { 42 $plot->AddDataSet( 43 X => $data->{x}, 44 Y => $data->{$color_name}, 45 style => { 46 marker => { 47 size => 2, 48 symbol => 'circle', 49 color => Imager::Color->new($color_name), 50 } 51 } 52 ); 53 } 54 55 my $graph = Imager->new( 56 xsize => 600, 57 ysize => 400); 58 59 $graph->box(filled => 1, color => 'white'); 60 61 # Add text 62 $plot->{'Ylabel'} = 'RGB Values'; 63 $plot->{'Xlabel'} = 'X-Pixel'; 64 $plot->{'Title'} = 'RGB-Distribution'; 65 66 $plot->Render( 67 Image => $graph, 68 Xoff => 40, 69 Yoff => 370); 70 71 $graph->write(file => "graph.png") or die $graph->errstr();
Die Methode read()
des Imager-Moduls vom CPAN ist ein Multitalent,
das alle gängigen Bildformate erkennt, einliest und in das interne
Imager-Format zur zügigen Weiterverarbeitung umwandelt. Geht irgend
etwas schief, liefern die Imager-Methoden falsche Werte zurück. Um
mehr Details über einen aufgetretenen Fehler herauszufinden, ruft der
sorgfältige Programmierer in derartigen Fällen
die Methode errstr()
auf, die eine
Klartextbeschreibung des Fehlers zurückliefert.
Die Methode getpixel()
untersucht die RGB-Werte eines per X- und
Y-Koordinate festgelegten Pixels im Bild. Sie gibt
ein Objekt vom Typ Imager::Color zurück. Dieses enthält die RGB-Werte des
Pixels und gibt sie mit der Methode rgba()
samt dem Wert des Alpha-Kanals
preis. Es interessieren nur die ersten drei RGB-Werte, die das Skript mit
shift
in Zeile 31 extrahiert.
Das Modul Imager::Plot
stellt öde Zahlenkolonnen in ansprechend
gestalteten Koordinatensystem dar ohne dass hierfür viel Fitzelei mit
Skalierung, Achsenbeschriftungen oder graphischem Layout notwendig wäre.
Es liefert Bilddateien in allen gängigen Formaten, die der erfreute
Nutzer anschließend mit einem Image-Viewer oder einem Webbrowser begutachtet.
Der Konstruktor new()
nimmt die gewünschten Dimensionen der
Achsengrafik und den Pfad zu einem installierten True-Type Font
für die Achsenbeschriftung entgegen.
Das Skript sammelt alle Koordinatendaten in einem Hash von Hashes, auf
den die Referenz $data
zeigt. Es legt
alle X-Koordinaten in $data->{x}
und alle Rot-Werte
in $data->{red}
ab. Analoges gilt für die Grün- und Blauwerte entlang
der X-Achse. Die Methode AddDataSet
bestimmt jeweils die Daten für einen
der drei Graphen. Das Skript ruft sie dreimal auf, um die Daten
für die drei Graphen in drei verschiedenen Farben einzupauken.
Zeile 55 erzeugt anschließend ein neues Imager-Objekt, das später die
gewünschte Grafikdatei erzeugen wird. Erst füllt die Methode box()
den Bildhintergrund weiß aus, dann malt Render()
das Koordinatensystem,
die Beschriftung und schließlich die drei Graphen in einem Rutsch.
Die Methode write()
schreibt schließlich die Ausgabedatei im
PNG-Format auf die Festplatte.
Bevor ein Skript die drei gesuchten Regionen in der Bildmitte fehlerfrei
erkennen kann, sind noch einige Vorbereitungsschritte erforderlich.
Aus Abbildung 2 wird deutlich, dass der Graph stark schwankt und deswegen
die Erkennung der flachen Stellen erschwert. Das Erkennungsskript
cardfind
nutzt deswegen einen Blur-Filter, um das Bild mit dem
Verfahren ``Gaussian Blur'' und dem Radius 10 unscharf zu machen.
In einem unscharfen Bild (Abbildung 3) sind die Farbübergänge zwischen
den einzelnen Pixeln weniger abrupt. Statt zum Beispiel direkt von einem
weißen Pixel auf einen schwarzen zu springen, bringt eine unscharfes Bild
mehrere Grautönen als Übergang. Entsprechend geglättet stellt sich der Graph
in Abbildung 4 dar, der die Pixelwerte entlang der gleichen horizontalen
Linie darstellt. Dies erleichtert die Erfassung der drei gesuchten
Regionen.
In diesen Bereichen verläuft die Kurve über hunderte von Pixeln weit recht flach dahin. Wer sich noch an die Schulmathematik erinnert, dem fällt vielleicht ein, dass die erste Ableitung eines solchen Graphen an flachen Stellen konstant und etwa gleich Null ist, während sie sonst deutlich höhere Werte aufweist und stark schwankt. Abbildung 5 zeigt die erste Ableitung der Intensitätswerte, die sich aus der Addition der Pixelwerte für den roten, den grünen und den blauen Kanal ergeben. Die aufgezeichneten Werte stellen ein Maß für die Schwankungen der ursprünglichen Kurve dar und bewegen sich über lange Strecken nahe des Nullpunkts. Dies sind die Stellen, an denen im ursprünglichen Bild die homogen belichteten Karten liegen. Das Skript also muss nur an diesem Graphen entlangwandern, einen Ringpuffer von etwa 50 durchwanderten Werten anlegen und Alarm schlagen, falls die dort liegenden Werte durchschnittlich etwa gleich Null sind. Dann befindet es sich über einer Karte.
Fangen in diesem Zustand die Pufferwerte plötzlich wieder zu holpern an,
wurde der der Bereich einer Karte verlassen und das Skript geht wieder
in den Zustand ``suche nach der nächsten homogenen Stelle'' über. So sollte
es alle drei gesuchten Regionen finden und die gefundenen RGB-Werte im
YAML-Format ausspucken. Damit kann das im letzten Snapshot vorgestellte
Skript picfix
die White-Balance weiterer Bilder mit denselben
Lichtverhältnissen korrgieren. Wendet sich der Fotograf also einer neuen
Szene zu, zieht er die drei Kärtchen aus der Hosentasche, hält sie ins
Bild und knipst ein Referenzfoto. Alle anschließend geschossenen Fotos können
dann später daheim mit Hilfe des Gimp und dem
letztens vorgestellten picfix
-Skript korrigiert werden.
Damit das Verfahren bei einem homogenen Bildhintergrund nicht ins Schleudern kommt, prüfen die Zeilen 48 bis 50 nicht nur ab, ob der Durchschnittswert im Puffer kleiner als 3 ist, sondern auch ob sich der Algorithmus gerade im mittleren Bilddrittel aufhält. Die äußeren Bilddrittel ignoriert es schlicht.
Abbildung 5: Die erste Ableitung des Intensitätsgraphen weist für die flachen (also: homogenen) Bildstellen Werte nahe Null auf. |
Als Ringpuffer verwendet das Skript normale Perl-Arrays. Neue Werte hängt es
mit push()
hintenan und prüft anschließend, ob der Array damit die
Maximallänge des Ringpuffers überschritten hat. Ist dies der Fall, löscht
es das erste Arrayelement mit shift()
. Anschließend ist der Array nicht
nur um eins kürzer, sondern das zweite Element ist die neue Nummer eins.
Um die erste Ableitung der recht komplexen Pixelfunktion zu ermitteln, kommt ein
vereinfachtes numerisches Verfahren zum Einsatz. Im Ringpuffer
@intens_ring
liegen die Intensitätswerte der letzten 50 Pixel, die durch
Addition der Rot-, Grün- und Blauwerte an den bereits verarbeiteten
x-Koordinaten entstanden sind. Zur Extraktion der drei Werte aus dem
von der Methode rgba()
zurückgegebenen 4-Teiler nutzt das
ein sogenanntes Hash-Slice mit der Notation @components[0,1,2]
.
Der Wert der ersten Ableitung, also die
Steigung des Graphen an dieser Stelle ermittelt sich anschließend
näherungsweise aus der Differenz des ersten und des letzten
Ringpufferelements. Positive oder negative Steigungsraten interessieren
nicht, also egalisiert die Funktion abs()
diese zu positiven Werten.
Um festzustellen, ob der Algorithmus sich gerade in einem der gesuchten
flachen Teile der Kurve befindet oder in einem eher welligen Bereich,
unterhält das Skript einen zweiten Ringpuffer @diff_ring
, der die
letzten 50 ermittelten Werte für die erste Ableitung des Graphen enthält.
Die ab Zeile 76 definierte Funktion avg()
rechnet den Mittelwert aller 50
Intensitätswerte aus. Ist der Algorithmus gerade in einem welligen Teil,
reicht ein Mittelwert unterhalb der Schwelle von 3, damit ein flacher Teil
erkannt wird. Einmal in diesem Modus angelangt ist allerdings eine
mittlere Steigung von mehr als 10 erforderlich, damit der Zustandsautomat
sich wieder in einem welligen Bereich wähnt.
Jedes Mal, wenn ein flacher Bereich erkannt wurde, speichert Zeile 52
die RGB-Werte des ersten dort gefundenen Pixels im Array @ctl_points
.
Nur drei flache Bereiche werden gesucht, etwaigen weiteren bereitet die
last
-Anweisung in Zeile 70 den Garaus. Die Funktion Dump
aus dem
YAML-Modul vom CPAN schließlich gibt das Ergebnis nach Abbildung 6
aus. In einer .yml-Datei sample.yml
gespeichert und mit -c sample.yml
an das
im letzten Snapshot vorgestellte Skript picfix überreicht, und schon
farbkorrigiert es nicht nur das Bild mit den abgebildeten Karten, sondern
auch noch beliebig viele weitere, die in den gleichen Lichtverhältnissen
geschossen wurden. Doch nicht vergessen: Die Karten müssen mittig ins
Bild gehalten werden, damit der einfach Algorithmus sie findet. Sonst
muss tatsächlich ein ausgefuchsteres Verfahren ran, aber wie immer sind
in Perl mit der reichen Modulauswahl auf dem CPAN
der Fantasie keine Grenzen gesetzt.
Abbildung 6: Das Skript cardfind nimmt den Namen einer Bilddatei entgegen, führt die Berechnungen eigenständig durch und gibt die Farbwerte der gesuchten Messwerte auf den Referenz-Karten im YAML-Format aus. |
01 #!/usr/local/bin/perl -w 02 use strict; 03 use Imager; 04 use YAML qw(Dump); 05 06 my($file) = @ARGV; 07 die "No file given" unless defined $file; 08 09 my $img = Imager->new(); 10 $img->read(file => $file) or 11 die "Can't read $file"; 12 13 # Blur 14 $img->filter( 15 type => "gaussian", 16 stddev => 10 ) or die $img->errstr; 17 18 my $y = int( $img->getheight() / 2 ); 19 my $width = $img->getwidth(); 20 21 my @intens_ring = (); 22 my @diff_ring = (); 23 my $found = 0; 24 my @ctl_points = (); 25 26 for my $x (0..$width-1) { 27 my $color = $img->getpixel( x => $x, 28 y => $y ); 29 my @components = $color->rgba(); 30 31 # Save current intensity in ring buffer 32 my $intens = @components[0,1,2]; 33 push @intens_ring, $intens; 34 shift @intens_ring if @intens_ring > 50; 35 36 # Store slope between x and x-50 37 push @diff_ring, 38 abs($intens - $intens_ring[0]); 39 shift @diff_ring if @diff_ring > 50; 40 41 if($found) { 42 # Inside flat region 43 if(avg(\@diff_ring) > 10) { 44 $found = 0; 45 } 46 } else { 47 # Outside flat region 48 if($x > $width/3 and 49 $x < 2/3*$width and 50 avg(\@diff_ring) < 3) { 51 $found = 1; 52 push @ctl_points, 53 [@components[0,1,2]]; 54 } 55 } 56 } 57 58 my $out = {}; 59 my @labels = qw(low medium high); 60 61 # Sort by intensity 62 for my $ctl_point (sort { 63 $a->[0] + $a->[1] + $a->[2] <=> 64 $b->[0] + $b->[1] + $b->[2] } 65 @ctl_points) { 66 my $label = shift @labels; 67 $out->{$label}->{red} = $ctl_point->[0]; 68 $out->{$label}->{green}= $ctl_point->[1]; 69 $out->{$label}->{blue} = $ctl_point->[2]; 70 last unless @labels; 71 } 72 73 print Dump($out); 74 75 ########################################### 76 sub avg { 77 ########################################### 78 my($arr) = @_; 79 80 my $sum = 0; 81 $sum += $_ for @$arr; 82 return $sum/@$arr; 83 }
Michael Schilliarbeitet als Software-Engineer bei Yahoo! in Sunnyvale, Kalifornien. Er hat "Goto Perl 5" (deutsch) und "Perl Power" (englisch) für Addison-Wesley geschrieben und ist unter mschilli@perlmeister.com zu erreichen. Seine Homepage: http://perlmeister.com. |