Pixelgespür (Linux-Magazin, Mai 2007)

SecurID-Tokens helfen bei der Zugangskontrolle, denn sie zeigen Ziffernfolgen an, die jeweils nur eine Minute lang zum Einloggen auf einem Zielsystem gültig sind. Eine mit Perl gestrickte Zeichenerkennung legt offen, was der Token den ganzen Tag so treibt.

Neulich berichtete mein Arbeitskollege Fergus, dass sein SecurID-Token gerade ``000000'' angezeigt hätte und postete prompt ein Foto auf Flickr [2]. SecurID-Anhänger von der Firma RSA geben alle 60 Sekunden eine unterschiedliche 6-stellige Zahl aus. Wenn alle Ziffernfolgen mit der gleichen Wahrscheinlichkeit auftreten, ist die Chance, zu einem zufälligen Zeitpunkt auf der Anzeige ``000000'' zu sehen, eins zu einer Million. Das kommt fast einem Lottotreffer gleich!

Abbildung 1: Der 'Keyfob' von RSA Security zeigt alle 60 Sekunden eine neue sechsstellige Zahl an.

Totpatentiert

Dieser Zufall weckte meine Neugier: Was zeigt eigentlich mein ``Keyfob'' (amerikanisch für Schlüsselanhänger) an, wenn ich gerade nicht hinsehe? Mit einer Webcam oder einem Scanner lässt sich die Anzeige leicht als Bild digitalisieren. Und aus den Pixeln ermittelt man dann per Optical Character Recognition (OCR) die tatsächlichen Ziffernwerte. Da der Bereich aber leider totpatentiert wurde, gibt es dafür kaum funktionsfähige freie Software.

Abbildung 2: Der Versuchsaufbau zur Fobcam: Eine Lampe sorgt für gleichmässig verteiltes Licht und die Webcam zeigt senkrecht auf den SecurID-Token.

Im vorliegen Fall ist das Problem jedoch einfacher zu lösen, denn der vom Token dargestellte Zeichensatz ist auf die Ziffern 0-9 beschränkt. Sie sind grob aus nur sieben LCD-Segmenten zusammengesetzt, und die Erkennung ist tatsächlich relativ leicht mit einem Perlskript und den richtigen CPAN-Modulen zu bewerkstelligen.

Aber vorher noch ein Warnhinweis: Der heutige Snapshot dient nur zu Forschungszwecken. Auf keinen Fall sollten die Keyfob-Daten auf elektronischem Wege übermittelt werden. Die so genannte ``Fobcam'' [3] eines Computernutzers, der die aktuelle Ziffernfolge seines SecurID-Token aus Bequemlichkeit mittels Webcam auf seiner Website anzeigte, ist zwar immer wieder für einen Lacher gut, wird aber nicht zur Nachahmung empfohlen.

Abbildung 3: Das OCR-System hat die dargestellte Zahl korrekt erkannt. An den beiden grünen Referenzpunkten hängt die OCR-Maske, an den roten Stellen sind Sensoren platziert, die die dargestellte Zahl abtasten.

Ins rechte Licht gerückt

Um ein Bild des Tokens aufzunehmen, bietet sich eine Webcam oder ein Scanner an. In einem frühreren Snapshot [4] wurde schon mal gezeigt, wie man unter Linux mittels Video::Capture::V4l eine Webcam ansteuert, mittlerweile habe ich das Modul Video::Capture::V4l::Imager aufs CPAN gestellt, das die Sache noch weiter vereinfacht.

Listing 1: fobcam

    01 #!/usr/bin/perl -w
    02 use strict;
    03 use Video::Capture::V4l::Imager;
    04 use Log::Log4perl qw(:easy);
    05 
    06 Log::Log4perl->easy_init($DEBUG);
    07 
    08 my $v = Video::Capture::V4l::Imager->new(
    09     width   => 640,
    10     height  => 480,
    11 );
    12 
    13 $v->brightness($ARGV[0] || 27_900);
    14 my $img = $v->capture();
    15 
    16 $img->write(file => 'fob.jpg') 
    17   or die "Can't write: $!";

Listing fobcam zeigt, wie die Kamera angesteuert und zunächst die gewünschte Helligkeit mit der Methode brightness() eingestellt wird. Der beste Wert hängt vom Kamertyp und von den aktuell herrschenden Lichtverhältnissen ab. Nach einigem Experimentieren findet sich meistens ein akzeptabler Wert. Das CPAN-Modul bietet auch die Methode calibrate() an, die solange mit verschiedenen Einstellungen für brightness() herumprobiert, bis das aufgenommene Bild eine vorgegebene mittlere Helligkeit zeigt.

Das Capture-Objekt $v liefert im Erfolgsfall als Ergebnis ein Objekt vom Typ Imager zurück, an dem man entweder Bildmanipulationen vornehmen oder die Daten in einem gängigen Format wie JPEG oder PNG auf der Festplatte abspeichern kann.

OCR für Arme

Für den OCR-Vorgang wird zunächst die Lage des Keyfob-Displays im Bild ermittelt und die Position jeder einzelnen Ziffer berechnet. Steht das Rechteck fest, in dem die aus sieben LCD-Segmenten zusammengesetzte Zahl liegt, definiert das Erkennungsskript über jedem Segment einen Fühler, der die dort vorhandenen Pixelwerte misst.

Liefert der Fühler einen hellen Pixelwert, ist das dort vermutete Segment inaktiv. Kommt allerdings ein niedriger RGB-Wert zurück, weist das Bild dort eine dunkle Stelle auf und die ist bei ordnungsgemäßer Belichtung auf ein aktives LCD-Segment zurückzuführen.

Abbildung 4: Die Segmente des LCD-Displays werden zur einfachen Zeichenerkennung durchnumeriert.

Abbildung 4 zeigt eine einzelne LCD-Ziffer, mit denen der Keyfob die Zahlen von 0-9 anzeigt. Der Übersicht halber wurden ihre Segmente willkürlich durchnumeriert. Zeigt das Display die Zahl 8 an, sind alle Segmente von 1-7 aktiv, während bei der Zahl 1 nur die Segmente 2 und 3 aufleuchten.

Soweit die Theorie. In der Praxis stellt sich heraus, dass der Token manchmal schief im Bild liegt, die Billig-Webcam im Nahbereich erschreckend schlechte Bildqualität liefert und die Ausleuchtung der Szene mit einer Schreibtischlampe alles andere als homogen ist. Es müssen also einige Tricks ran.

Wer suchet, der findet

Um die Lage der Einzelsegmente berechnen zu können, muss sich das Erkennungsskript zunächst im Bild orientieren. Dies geschieht mittels zweier Referenzpunkte, deren Pixelkoordinaten (x1_ref, y1_ref) und (x2_ref, y2_ref) dem Skript bekannt sind. Für den verwendeten RSA-Token wurden hierfür die beiden äußeren oberen Eckpunkte der blauen Fläche (Abb. 1) ausgewählt.

Zu Experimentierzwecken ist unter den Listings zu diesem Artikel [1] noch ein Modul Blue.pm erhältlich, das die beiden Referenzpunkte durch ein ausgefuchstes Verfahren ermittelt, dessen Funktion allerdings den Rahmen dieses Artikels sprengen würde. Man kann sie allerdings genausogut mit Gimp ermitteln, in dem man eine Testaufnahme lädt und mit der Maus an die beiden Referenzpunkte heranfährt. Gimp zeigt die jeweiligen X/Y-Koordinaten dann in der linken unteren Ecke an.

Mit den beiden Referenzpunkten lässt sich der Token im Bild eindeutig lokalisieren. Jeder Keyfob hat jedoch andere Abmessungen, und auch für den gleichen Token beeinflusst die eingestellte Auflösung bzw. Bildvergrößerung die Werte für die Entfernungen von den Referenzpunkten zu den Segmenten.

Maßgeschneidert für jeden Token

Aus diesem Grund werden die Maße nicht fest in das Skript einkodiert sondern in der Konfigurationsdatei fobs.yml verwaltet.

Listing 2: fobs.yml

    01 # Key Fob Characteristics
    02 RSA1:
    03   x1_ref:       176
    04   y1_ref:       232
    05   x2_ref:       422
    06   y2_ref:       155
    07   x_off:         99 
    08   y_off:          9
    09   digit_width:   12
    10   digit_height:  27.5
    11   digit_dist:    23
    12   digits:         6

Abbildung 5: Die zwei Referenzpunkte (x1_ref, y1_ref) und (x2_ref, y2_ref) an den oberen Eckpunkten der blauen Fläche legen das Koordinatensystem fest, in denen später die Pixelwerte einzelner Ziffern abgegriffen werden.

Den horizontalen Abstand zwischen dem ersten Referenzpunkt eines zu Testzwecken kerzengerade im Bild liegenden Tokens und dem Mittelpunkt der ersten Ziffer der Anzeige legt x_off fest. y_off ist hingegen der vertikale Abstand zwischen der obersten Segmentreihe und einer gedachten horizontalen Linie, die die beiden Referenzpunkte verbindet (Abbildung 5).

Die Breite einer LCD-Ziffer wird mit digit_width definiert und digit_height gibt deren Höhe an. digit_dist ist der horizontale Abstand vom Anfang einer Ziffer zum Anfang der nächsten. digits schließlich gibt die Anzahl der auf dem Display dargestellten Zeichen an.

Die Maßangaben in fobs.yml werden in Pixeln gemessen, sind aber unabhängig von einer bestimmen Bildauflösung. Denn bevor das Skript die Maße verwendet, berechnet es den Abstand der beiden Referenzpunkte im wirklichen Bild, vergleicht ihn mit dem willkürlich in fobs.yml gewählten und passt alle Werte entsprechend an.

Und Action!

Das Skript in Listing reco nimmt eine Bilddatei entgegen und die vier Koordinaten der zwei Referenzpunkte des Tokens im Bild:

    $ reco fob.jpg 160 193 425 218
    372394

Es gibt die erkannte sechsstellige Anzeige aus. Es bedient sich dabei des weiter unten besprochenen Moduls LCDOCR.pm, dessen Methode reco() eine Referenz auf einen Array zurückliefert, der die erkannten Ziffern beinhaltet.

Da das Skript im Verbose-Modus (-v) zusätzlich die Referenzpunkte, die Fühler und die gefundenen Ziffern ins Bild einblendet und in der Datei out1.jpg abspeichert (wie in Abbildung 3 dargestellt), lässt es sich zur Feinjustierung der Konfigurationsparameter nutzen. Mit der Option -d aufgerufen startet es nach dem Erkennungslauf zusätzlich den schnellen Image-Viewer xv mit dem durch die eingeblendeten Informationen angereicherten Bild.

Listing 3: reco

    01 #!/usr/bin/perl -w
    02 use strict;
    03 use Log::Log4perl qw(:easy);
    04 use LCDOCR;
    05 use Getopt::Std;
    06 
    07 getopts("vd", \my %opts);
    08 Log::Log4perl->easy_init($opts{v} ? 
    09                          $DEBUG : $ERROR);
    10 my($file, $x1, $y1, $x2, $y2) = @ARGV;
    11 die "usage: $0 file x1 y1 x2 y2\n" unless
    12    defined $y2;
    13 
    14 my $i = Imager->new();
    15 $i->read(file => $file, type => "jpeg") or 
    16     die "Can't read $file";
    17 
    18 my $gr = Imager::Color->new(0, 255,0);
    19 $i->circle(color=>$gr,r=>1,x=>$x1, y=>$y1);
    20 $i->circle(color=>$gr,r=>1,x=>$x2, y=>$y2);
    21 
    22 my $ocr = LCDOCR->new(
    23   name   => 'RSA1',
    24   x1_ref => $x1, y1_ref => $y1,
    25   x2_ref => $x2, y2_ref => $y2,
    26   image  => $i,
    27   debug  => ($opts{v}||0));
    28 
    29 my $digits = $ocr->reco();
    30 
    31 if($opts{v}) {
    32   my $font = Imager::Font->new(file =>
    33   "/usr/X11R6/lib/X11/fonts/TTF/Vera.ttf");
    34 
    35   $i->string(x => 50, y => 50,
    36     string => "Reco: @$digits",
    37     font   => $font, color => "white",
    38     size => 30);
    39   $i->write(file => "out1.jpg", 
    40             type => "jpeg");
    41   system("xv out1.jpg") if $opts{d};
    42 }
    43 print join('', @$digits), "\n";

On-the-fly compiliert

Das Modul LCDOCR.pm implementiert den OCR-Vorgang auf das LCD-Display. Es steigt zum Herumorgeln auf den Bilddaten in die C-Welt des Imager-Moduls hinunter. Dies könnte man wie in [4] mit einer .xs-Datei bewerkstelligen, doch mit dem Modul Inline::C vom CPAN lässt sich C-Code auch direkt in das Perlskript einbinden.

Dieser wird dann beim ersten Aufruf für den User unsichtbar übersetzt und die Objekt- und Shared-Library-Dateien im Unterverzeichnis _Inline aufgehoben. Beim nächsten Aufruf wird der Compilierschritt übersprungen und das Skript startet mit voller Geschwindigkeit. Ändert sich der C-Code im Skript, merkt Inline::C das und leitet wieder eine Neukompilation ein.

Listing 4: LCDOCR.pm

    001 use strict;
    002 use Imager;
    003 use Log::Log4perl qw(:easy);
    004 use YAML qw(LoadFile);
    005 
    006 ###########################################
    007 sub new {
    008 ###########################################
    009   my($class, %options) = @_;
    010 
    011   my $refd = LoadFile("/etc/fobs.yml")->
    012              {$options{name}};
    013   my $self = {
    014       name           => "RSA1",
    015       threshold      => 0.85,
    016       debug          => 0,
    017       digits         => $refd->{digits},
    018       %options,
    019   };
    020     # Adapt coordinates to real image
    021   my $stretch = ref_dist($self) /
    022                 ref_dist($refd);
    023   for (qw(x_off y_off digit_width 
    024           digit_height digit_dist)) {     
    025       $self->{$_} = $refd->{$_} * $stretch;
    026   }
    027 
    028   $self->{angle} = atan2 (
    029     $self->{y2_ref}-$self->{y1_ref}, 
    030     $self->{x2_ref}-$self->{x1_ref});
    031 
    032   bless $self, $class;
    033 }
    034 
    035 ###########################################
    036 sub ref_dist {
    037 ###########################################
    038   my($h) = @_;
    039   return sqrt(
    040     ($h->{x2_ref} - $h->{x1_ref})**2 +
    041     ($h->{y2_ref} - $h->{y1_ref})**2)
    042 }
    043 
    044 ###########################################
    045 sub reco {
    046 ###########################################
    047   my($self) = @_;
    048 
    049   my @digits;
    050   my %seg_orient = qw(
    051           1 h 2 v 3 v 4 h 5 v 6 v 7 h);
    052 
    053   for (1..$self->{digits}) {
    054     my $coords    = $self->seg_coords($_);
    055     my $segstring = "";
    056 
    057     my $bkground = (
    058       xybrightness($self->{image},
    059                    @{$coords->{8}}) +
    060       xybrightness($self->{image},
    061                    @{$coords->{9}})
    062      ) / 2;
    063 
    064      for my $c (1..7) {
    065        my($x, $y) = @{$coords->{$c}};
    066 
    067        if(pixel_dark($self->{image}, $x, 
    068                      $y, $bkground,
    069                      $self->{debug}, $c, 
    070                      $seg_orient{$c}, 
    071                      $self->{threshold})) {
    072          $segstring .= "$c";
    073        }
    074 
    075        if($self->{debug}) {
    076          my $red = Imager::Color->new(
    077                                 255, 0, 0);
    078          $self->{image}->circle(
    079           color=>$red, r=>1, x=>$x, y=>$y);
    080        }
    081      }
    082 
    083      my $digit = seg2digit($segstring);
    084      push @digits, 
    085           defined $digit ? $digit : "X";
    086     }
    087 
    088     return \@digits;
    089 }
    090 
    091 ###########################################
    092 sub seg_coords {
    093 ###########################################
    094   my($self, $digit) = @_;
    095 
    096   my $x = $self->{x_off} + 
    097         ($digit-1) * $self->{digit_dist};
    098   my $y = $self->{y_off};
    099   my $w = $self->{digit_width};
    100   my $h = $self->{digit_height};
    101   my $r = sub { [ $self->rotate(@_) ] };
    102 
    103   return {
    104     1 => $r->($x,        $y),
    105     2 => $r->($x + $w/2, $y + $h/4),
    106     3 => $r->($x + $w/2, $y + 3*$h/4),
    107     4 => $r->($x,        $y + $h),
    108     5 => $r->($x - $w/2, $y + 3*$h/4),
    109     6 => $r->($x - $w/2, $y + $h/4),
    110     7 => $r->($x,        $y + $h/2),
    111     # ref points
    112     8 => $r->($x,        $y + $h/4),
    113     9 => $r->($x,        $y + 3*$h/4),
    114   };
    115 }
    116 
    117 ###########################################
    118 sub seg2digit {
    119 ###########################################
    120   my %h = (
    121     "23"     => 1, "12457"   => 2,
    122     "12347"  => 3, "2367"    => 4,
    123     "13467"  => 5, "134567"  => 6,
    124     "123"    => 7, "1234567" => 8,
    125     "123467" => 9, "123456"  => 0,
    126     );
    127   return $h{$_[0]};
    128 }
    129 
    130 ###########################################
    131 sub rotate {
    132 ###########################################
    133     my($self, $xd, $yd) = @_;
    134 
    135     my $r  = sqrt($xd*$xd + $yd*$yd);
    136 
    137     my $phi = atan2($yd,$xd);
    138     $phi += $self->{angle};
    139     
    140     my $xd_rot = $r * cos($phi);
    141     my $yd_rot = $r * sin($phi); 
    142     my $x_abs  = $self->{x1_ref} + $xd_rot;
    143     my $y_abs  = $self->{y1_ref} + $yd_rot;
    144 
    145     return($x_abs, $y_abs);
    146 }
    147 
    148 use Inline C => <<'EOT' => WITH => 'Imager';
    149 
    150 int pixel_dark(Imager im, int x, int y, 
    151                int threshold, int debug, 
    152                int seg, char *direction, 
    153                float percent) {
    154   i_color val;
    155   int br, i, j, dark=0, min=-1;
    156   int imin=0, imax=1, jmin=0, jmax=1;
    157   float rel;
    158 
    159   if(direction == 'h') {
    160       jmin = -1; jmax =  2;
    161   } else {
    162       imin = -1; imax =  2;
    163   }
    164 
    165   for(i=imin; i<imax; i++) {
    166       for(j=jmin; j<jmax; j++) {
    167           i_gpix(im, x+i, y+j, &val);
    168           br = brightness(&val);
    169           if(min == -1 || min > br)
    170               min = br;
    171       }
    172   }
    173 
    174   rel = 1.0*min/threshold;
    175   if(rel < percent)
    176     dark = 1;
    177 
    178   if(debug) {
    179     printf("TH[%d]: %d (%d %.1f%%: %d)\n", 
    180      seg, min, threshold, rel*100.0, dark);
    181   }
    182   return dark;
    183 }
    184 
    185 int brightness(i_color *val) {
    186     return((val->channel[0] + 
    187             val->channel[1] + 
    188             val->channel[2])/3);
    189 }
    190 
    191 int xybrightness(Imager im, int x, int y) {
    192     i_color val;
    193     i_gpix(im, x, y, &val);
    194     return brightness(&val);
    195 }
    196 
    197 EOT
    198 
    199 1;

Dreh' dich im Kreis, Marie

Um den Drehwinkel des Tokens relativ zum Bildrand aus den Referenzpunkten zu berechnen, verwendet der Konstruktor new einfache trigonometrische Funktionen.

Die Koordinatenabstände bilden die Katheten eines rechtwinkligen Dreiecks, also ergibt sich der Drehwinkel aus dem Arcus Tangens des Quotienen von Gegenkathete zu Ankathete. Perl hat von Haus aus keine atan-Funktion, aber dafür atan2(), die die zwei Kathetenlängen separat entgegennimmt.

Abbildung 6: Der Neigungswinkel des Tokens berechnet sich aus den Koordinaten der Referenzpunkte.

 
Da der Token verdreht im Bild liegen darf, spannt die Zeichenerkennung
ihr Erkennungsnetz horizontal auf und dreht es mit
C<rotate()> in den zu diesem Zeitpunkt bereits bekannten Winkel des Tokens. 
Drehungen in einem karthesischen
X/Y-Koordinatensystem sind etwas umständlich zu berechnen, deshalb
werden die karthesischen Koordinaten zunächst in 
Polarkoordinaten C<r> und C<phi> umgewandelt (Abbildung 6)
Der Radius C<r> errechnet sich aus dem Satz des Pytagoras und 
der Drehwinkel C<phi> aus dem Arcus Tangens des Quotienten aus
Y- und X-Wert.

Zu diesem Winkel addiert Zeile 138 dann den bekannten Drehwinkel des Tokens, bevor die darauffolgenden Zeilen die Koordinaten mit einfacher Trigonometrie (Sinus, Gegenkathete, Hypotenuse!) wieder in karthesische X-/Y-Werte zurückrechnen. Fertig ist die Rotation der OCR-Maske um den Drehpunkt [x1_ref, y1_ref].

Abbildung 7: Umwandlung von karthesischen in Polarkoordinaten und zurück.

Schnell erkannt

Die Funktion seg2digit() ermittelt aus einem String mit sortierten Ordnungsnummern aktivierter Segmente die dargestellte Ziffer. Die Sortierung vereinfacht den Zugriff, denn findet das Skript, dass die Segmente 2 und 3 eines Elementes schwarz sind, liefert der Aufruf von seg2digit mit ``23'' einfach per Hash-Lookup ``1'' zurück, was genau der dargestellten Ziffer entspricht. Ist das von den Segmenten dargestellte keine Zahl, gibt seg2digit den Wert undef zurück und das Hauptprogramm setzt statt dessen ``X'' ein. So lässt sich feststellen, dass noch etwas mit der Justierung oder den Lichtverhältnissen im Argen liegt.

Abbildung 8: Die zwei grünen Messpunkte am Rand helfen, die Lage des Keyfobs zu ermitteln, die roten Messpunkte im Display greifen die dargestellten Ziffern ab.

Doppelt hält besser

Falls das Display nicht gleichmäßig ausgeleuchtet ist, ist der graue Hintergrund unterschiedlich hell und es ist nicht einfach, einen zuverlässigen Schwellenwert zu bestimmen, der die Pixelwerte eines aktivierten von denen eines inaktiven Segments unterscheidet.

Deshalb misst die Methode reco nicht nur die Pixelhelligkeit an den Stellen, an denen sich Segmente befinden, sondern auch in den segmentfreien Zonen in den oberen und unteren Bäuchen der viereckigen Achten. Diesen Messwerten werden die Nummern 8 und 9 zugewiesen Sie werden gesondert behandelt und ihr Mittelwert als Hintergrundhelligkeit eines Segments bewertet. Der Parameter threshold gibt an, um wieviel dunkler als der Hintergrund ein Messwert sein muss als, damit das Verfahren ein aktives Segment erkennt. Ist threshold zum Beispiel 0.85 und der Hintergrund hat gemäß den zwei Bauchwerten die mittlere Helligkeit 180, werden Messwerte größer gleich 153 als Hintergrund, also als inaktive LCD-Segmente interpretiert. Abbildung 8 zeigt, wie die Ziffer ``0'' mit einem threshold-Wert von 0.85 erkannt wird. Aktive Segmente schwanken zwischen 40.5% und 72.5% des vorher ermittelten Bauchmittelwerts von 131. Das inaktive Segment 7 weist hingegen den Helligkeitswert 123 auf, mit 93.9% knapp über dem festgestetzten Schwellwert von 85%.

Abbildung 9: Die Ziffer '0' wird erkannt, der Parameter 'threshold' zur Unterscheidung von aktiven und inaktiven Segmenten ist auf 85% gesetzt.

Um auch bei leicht verschobenen Koordinaten zuverlässig festzustellen, welche Segmente der Anzeige schwarz eingefärbt sind, misst die im Inline-C-Bereich definierte Funktion pixel_dark nicht nur den aktuellen Pixel, sondern auch umliegende und nimmt nur den dunkelsten als Messwert. Damit so nicht aus Versehen Teile des Nachbarsegments gemessen werden, misst die Funktion immer orthogonal zum Segment: Bei waagrechten Segmenten werden die oberen und unteren Pixel zur Messung hinzugezogen, bei senkrechten Segmenten die links und rechts liegenden. Der Hash %segdir gibt hierzu zu jeder Segmentnummer dessen Lage (h=horizontal v=vertikal) an.

Die Funktion brightness() misst jeweils die Helligkeit eines Pixelwertes und zählt dazu die Rot-, Grün- und Blauanteile eines Messpunkts zusammen. xybrightness() berechnet die Helligkeit an einer Koordinate [x,y].

Abbildung 10: Eine lustige Kombination wurde gefunden.

Die Funktion seg_coords($x, $y) liefert die X/Y-Koordinaten aller Segmente einer Ziffer, deren oberstes Segment auf den Koordinaten $x und $y liegt. Zurück kommt eine Referenz auf einen Hash, der als Schlüssel die Segmentordnungsnummern und als Werte anonyme Arrays mit X/Y-Koordinaten führt.

Ist die Debug-Option debug aktiviert, zeichnet die Funktion reco() die Segmentkoordinaten gemäß Abbildung 6 gleich in Rot ins Bild hinein. Dies erfolgt natürlich nach der Messung, denn sonst wären ja alle Pixel plötzlich rot. Mit diesen Zusatzinformationen lassen sich Feinkalibrierungen vornehmen, falls die Justierung noch nicht ganz stimmt und nachgebessert werden muss.

Installation

Zur Installation werden die erforderlichen CPAN-Module Video::Capture::V4l::Imager und YAML heruntergeladen. Innerhalb einer CPAN-Shell schleifen sie automatisch alle weiteren benötigten Module mit. Das Modul LCDOCR.pm muss irgendwo hin, wo das Skript reco es findet (z.B. /usr/lib/perl5/site_perl) und dann kann mit fobcam die erste Aufnahme gestartet werden. Mit Gimp werden dann die Referenzpunkte ermittelt und die Datei /etc/fobs.yml mit den Daten des verwendeten Displays erweitert. Anschließend wird reco mit dem Namen der gesicherten Bilddatei und den Referenzkoordinaten aufgerufen. Nach etwas Gefummle erkennt das OCR-System die angezeigten Zahlenkolonnen zuverlässig und die Auswertung kann beginnen. Wer das Skript über Nacht laufen lässt, sollte bedenken, die Schreibtischlampe angeschaltet zu lassen :).

Infos

[1]
Listings zu diesem Artikel: ftp://www.linux-magazin.de/pub/listings/magazin/2007/05/Perl

[2]
``000000'' auf einem SecurID-Token: http://www.flickr.com/photos/ferg2k/381185553/

[3]
``Fobcam'', http://fob.webhop.net/

[4]
Michael Schilli, ``Angeln in der Bilderflut'', http://www.linux-magazin.de/heft_abo/ausgaben/2006/06/angeln_in_der_bilderflut

Michael Schilli

arbeitet 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.