Wer es zu mühselig findet, die ins Bild gehaltenen Referenzkarten für die Farbkorrektur in Gimp – wie im vorigen Snapshot beschrieben – manuell auszuwählen, der freut sich jetzt sicherlich über ein Skript, das diesen Vorgang automatisiert.
Im vorigen Monat ging es an gleicher Stelle darum, den von der Kamera erzeugten Farbstich eines Fotos nachträglich mit Hilfe von Referenzkarten (Abbildung 1) zu korrigieren, die der Fotograf probeweise mit aufnimmt [2]. Die Plastikkärtchen aus dem Fotofachhandel mit genormten Schwarz, Weiß- und Grauwerten sollten im Bild keinerlei Farbwerte ergeben. Mitfotografiert bieten sie daher drei Messpunkte für geringe, mittlere und hohe Lichtintensität, dank derer das Fototool Gimp die Farbkurve eines Fotos im Nachhinein 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 und sucht die homogenen Flächen der Karten.
Wie findet ein einfaches Perlskript ohne Einsatz von künstlicher Intelligenz heraus, welche Pixelwerte die drei Karten gegebenenfalls erzeugen? Die Schwierigkeit: Die Lage der Karten im Bild ist nicht genau bekannt und auch nicht immer gleich. Schafft es der Fotograf jedoch, die drei Karten, so wie in Abbildung 1 gezeigt, in der Bildmitte aufgefächert zu halten, kann ein Skript der gedachten horizontalen Mittellinie des Bildes folgen und die Karten anhand der Pixelwerte auf der x-Achse ermitteln.
Entlang dieser Linie bleibt die gemessene Lichtintensität nämlich über weite Strecken relativ konstant, solange die Linie über eine der Referenzkarten verläuft. Schneidet die Linie hingegen den Bildhintergrund, dann schwanken die ermittelten Pixelwerte recht stark.
RGB: Dreimal 0 bis 255
Das Listing 1 (»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 zur Verdeutlichung eingezeichneten horizontalen Linie in ein Koordinatensystem ein, dessen x-Achse den x-Koordinaten im Bild entspricht und dessen y-Wert den jeweiligen Farbanteil im Bereich zwischen 0 und 255 repräsentiert.
|
Listing 1: |
|---|
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 ist ein Multitalent, das alle gängigen Bildformate erkennt, einliest und in das interne Imager-Format zur zügigen Weiterverarbeitung umwandelt. Geht irgendetwas schief, liefern die Imager-Methoden falsche Werte zurück. Um mehr Details über einen Fehler herauszufinden, ruft der sorgfältige Programmierer in diesem Fall die Methode »errstr()« auf, die daraufhin eine Klartextbeschreibung des Fehlers liefert.
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. Es enthält die RGB-Werte des Pixels und gibt sie mit der Methode »rgba()« zusammen mit dem Wert des Alphakanals preis. Es interessieren nur die ersten drei RGB-Werte, die das Skript mit »shift« in Zeile 31 extrahiert.

Abbildung 2: Die Farbwerte des ungefilterten Bildes sind zu zittrig, um die Karten zuverlässig zu erkennen. Die Bereiche, die die Karten repräsentieren, zeichnen sich nicht deutlich genug ab.
Bequem bebildert
Das Modul Imager::Plot stellt öde Zahlenkolonnen in einem ansprechenden Koordinatensystem dar, ohne dass viel Fitzelei mit Skalierung, Achsenbeschriftungen oder grafischem Layout notwendig wäre. Es liefert Bilddateien in allen gängigen Formaten, die der Nutzer danach mit einem Image-Viewer oder einem Webbrowser begutachtet. Der Konstruktor »new()« nimmt die gewünschten Dimensionen der Achsengrafik und den Pfad zu einem installierten Truetype-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 Rotwerte 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 alle drei Graphen in drei verschiedenen Farben malen zu können.
Zeile 55 erzeugt anschließend ein neues Imager-Objekt, das später die gewünschte Grafikdatei erzeugt. Zuerst füllt die Methode »box()« den Bildhintergrund weiß aus, dann zeichnet »Render()« das Koordinatensystem, die Beschriftung und schließlich auch die drei Graphen in einem Rutsch. Die Methode »write()« schreibt die Ausgabedatei im PNG-Format auf die Festplatte.
Unscharf ist sanfter
Bevor ein Skript die drei gesuchten Regionen in der Bildmitte fehlerfrei erkennen kann, sind noch einige Vorbereitungsschritte erforderlich. Abbildung 2 macht deutlich, dass der Graph stark schwankt, was die Erkennung der flachen Stellen erschwert. Das Erkennungsskript »cardfind« (Listing 2) nutzt daher einen Blur-Filter, um das Bild mit dem Verfahren Gaussian Blur und einem Radius von zehn Pixeln unscharf zu machen.
|
Listing 2: |
|---|
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 }
|
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 Bildpunkt zu springen, enthält ein unscharfes Bild an dieser Stelle mehrere Grautönen als Übergang. Entsprechend geglättet stellt sich der Graph in Abbildung 4 dar, der die Pixelwerte entlang der gleichen horizontalen Linie visualisiert. Dieses Verfahren erleichtert die Erfassung der drei gesuchten Regionen erheblich.

Abbildung 3: Der Blur-Filter mit der Einstellung Gaussian Blur und einem Radius von zehn Pixeln erzeugt Unschärfe und glättet die Wogen der Pixelwerte. Die Farbflächen der Karten sind damit leichter auszumachen.

Abbildung 4: Das mit dem Blur-Filter unscharf gemachte Bild weist glattere Kurvenverläufe auf, an denen sich die ins Bild gehaltenen Karten an den flachen Stellen erkennen lassen.
In der Schule aufgepasst?
In diesen Bereichen verläuft die Kurve über Hunderte von Pixeln recht flach. 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, grünen und blauen Kanal ergeben.

Abbildung 5: Die erste Ableitung des Intensitätsgraphen weist für die flachen (also homogenen) Bildstellen Werte nahe null auf.
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 Karten mit ihren homogenen Grauwerten liegen. Das Skript muss nur an diesem Graphen entlangwandern, einen Ringpuffer von etwa 50 beobachteten Werten anlegen und Alarm schlagen, falls die dort liegenden Werte durchschnittlich etwa gleich null sind. Dann befindet es sich über einer Karte.
Farbkorrektur
Fangen in diesem Zustand die Pufferwerte plötzlich wieder zu holpern an, hat das Skript den Bereich einer Karte verlassen und geht anschließend in den Zustand “Suche nach der nächsten homogenen Stelle” über. Auf diese Weise sollte es alle drei gesuchten Regionen finden und die gefundenen RGB-Werte im Yaml-Format ausgeben. Damit kann das im vorigen Snapshot-Artikel vorgestellte Skript »picfix« die White-Balance weiterer Bilder mit denselben Lichtverhältnissen korrigieren [2].
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 lassen sich später daheim mit Hilfe von Gimp und dem bereits vorgestellten »picfix«-Skript korrigieren.
Damit das Verfahren bei einem homogenen Bildhintergrund nicht ins Schleudern kommt, prüfen die Zeilen 48 bis 50 in Listing 2 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.
Arrays als Puffer
Als Ringpuffer verwendet das Skript normale Perl-Arrays. Neue Werte hängt es mit »push()« einfach hinten an und prüft, ob das 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 ein Element kürzer, sondern das zweite 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 Vierteiler nutzt das Skript einen so genannten Array-Slice mit der Notation »@components[0,1,2]«.
Ableitung, vereinfacht
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. Ob die Steigungsraten positiv oder negativ ausfallen, interessiert in diesem Zusammenhang nicht, deshalb egalisiert die Funktion »abs()« jede Steigung zu einem positiven Wert.
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, um einen flachen Teil zu erkennen. 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.
Werteübergabe
Jedes Mal, wenn ein flacher Bereich gefunden ist, speichert Zeile 52 die RGB-Werte des ersten dort gefundenen Pixels im Array »@ctl_points«. Nur drei flache Bereiche sind von Interesse, eventuell vorhandenen weiteren bereitet die »last«-Anweisung in Zeile 70 den Garaus. Die Funktion »Dump()« aus dem Yaml-Modul vom CPAN schließlich gibt das Ergebnis aus (Abbildung 6).

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 Referenzkarten im Yaml-Format aus.
In einer »yml«-Datei »sample.yml« gespeichert und mit »-c sample.yml« an das im vorigen Snapshot erläuterte Skript »picfix« überreicht, korrigiert es die Farben nicht nur des Bildes mit den abgebildeten Karten, sondern auch noch beliebig viele weitere, die bei den gleichen oder zumindest sehr ähnlichen Lichtverhältnissen zustande kamen.
Doch nicht vergessen: Die Karten sind mittig ins Bild zu halten, damit der einfache Algorithmus sie auch finden kann. Andernfalls müsste tatsächlich ein ausgefuchsteres Verfahren her. Das wäre natürlich auch möglich – wie immer sind in Perl mit der reichen Modulauswahl auf dem CPAN der Fantasie keine Grenzen gesetzt. (jcb)
|
Infos |
|---|
|
[1] Listings zu diesem Artikel: [ftp://www.linux-magazin.de/pub/listings/magazin/2008/08/Perl] [2] Michael Schilli, “Farbenspiel”: Linux-Magazin 07/08: [https://www.linux-magazin.de/Artikel/ausgabe/2008/07/perl/perl.html] |
|
Der Autor |
|---|
|
|






