Spione wie wir (Linux-Magazin, Juni 2006)

Mit Video4Linux lassen sich Webcams ansteuern und für Überwachungsaufgaben nutzen. Am Beispiel einer Creative NX Ultra zeigt der Perl-Snapshot wie Fotos richtig belichtet und Exemplare mit ``Action'' erkannt und gesichert werden.

Typische Webcams werden mit Windows-Software geliefert und damit ist unter Linux nicht viel anzufangen. Doch mit dem auf neueren Distributionen standardmaßig installierten Video4Linux ist es relative einfach, eine per USB-Stecker eingestöpselte Kamera anzusteuern und damit allerhand Schabernack zu treiben.

Die in diesem Snapshot verwendete Kamera Creative NX Ultra liefert eigentlich Videos und kostet etwa 45 Dollar. Für die Benutzung als einfache Webcam ist sie eigentlich zu schade, aber sie lag in einer Schublade des Perlmeister-Labors und war verfügbar. Sie benötigt keine externe Stromversorgung und sobald der USB-Stecker im Rechner steckt, wird sie per Hot-Plugging von Linux erkannt. Ihre Videodaten erscheinen typischerweise unter /dev/video0. Das Perl-Modul Linux::Capture::V4l vom CPAN beißt sich am Device-Eintrag fest, greift die Frame-Daten ab und erlaubt es, Aufnahmeparameter, wie die Empfindlichkeit der Kamera, laufend zu verändern.

Listing single zeigt eine einfache Anwendung, die erst die Empfindlichkeit der Kamera auf 40.000 einstellt, dann ein Bild aus dem Videostrom abzwackt, und dieses anschließend als JPEG-Foto auf der Festplatte ablegt (Abbildung 2).

Listing 1: single

    01 #!/usr/bin/perl
    02 use strict;
    03 use warnings;
    04 use Camcap;
    05 
    06 my $cam = Camcap->new(width  => 640, 
    07                       height => 480);
    08 $cam->cam_bright(42_000);
    09 my $img = $cam->capture();
    10 $img->write(file => 'buero.jpg') 
    11   or die "Can't write: $!";

Abbildung 1: Die Kamera "NX Ultra" von Creative

Abbildung 2: Ein aus dem Video-Strom eingefangenes Bild der Webcam

Das von single verwendete Modul in Listing Camcap.pm abstrahiert den Zugriff auf den Videostrom. Der Konstruktor ab Zeile 11 definiert einige Default-Parameter, wie die Bildbreite und Höhe und die minimale und die maximale Helligkeitseinstellung (br_min, br_max). Anschließend hängt sich der Code über das CPAN-Modul Video::Capture::V4l an das Video-Device /dev/video0 an. Lauscht schon ein anderer Interessent daran, schlägt die Verbindung fehl.

Die ab Zeile 33 definierte Methode cam_bright() stellt die Empfindlichkeit der Kamera ein. Sie nimmt einen Wert zwischen 0 und 65535 entgegen, holt mit der Methode picture() die Picture-Struktur der Kamera, setzt mit brightness() die dort definierte Empfindlichkeit und stellt mit der anschließend aufgerufenen Methode set() den Wert in der Video4Linux-Schicht ein.

Die ab Zeile 93 definierte Methode capture nimmt optional eine Empfindlichkeitseinstellung entgegen und macht sich dann daran, den nächsten Frame aus dem Videostrom abzugreifen. Der erste Frame wird mit der Nummer 0 gefangen und ein anschließender Aufruf der Methode sync() mit der Framenummer stellt sicher, dass die Bilddaten auch gut im Skalar $frame angekommen sind.

Einige Tests zeigten, dass der erste vorbeisausende Frame manchmal nicht akzeptabel ist, da eine kurz zuvor eingestellte Kameraempfindlichkeit noch nicht gegriffen hat. Deswegen holt capture grundsätzlich zwei Frames ab und wirft den ersten weg.

Nach dem die Methode sync() zurückgekehrt ist, liegen in der Variablen $frame die rohen Bilddaten im BGR-Format. Jeder Pixel wird in drei aufeinanderfolgenden Bytes mit seinen Blau-, Grün- und Rotwerten (jeweils 0-255) kodiert. Um daraus ein Bildformat zu generieren, das typische Bildverarbeitungsprogramme verstehen, kehrt das Kommando reverse den Bytestring zunächst um. Dies hat zur Folge, dass die Daten nun im etwas gängigeren RGB-Format vorliegen.

Listing 2: Camcap.pm

    001 ###########################################
    002 package Camcap;
    003 ###########################################
    004 use strict;
    005 use warnings;
    006 use Video::Capture::V4l;
    007 use Imager;
    008 use Imager::Misc;
    009 use Log::Log4perl qw(:easy);
    010 
    011 ###########################################
    012 sub new {
    013 ###########################################
    014     my($class, @options) = @_;
    015 
    016     my $self = {
    017         width   => 320,
    018         height  => 240,
    019         avg_opt => 128,
    020         avg_acc => 20,
    021         br_min  => 0,
    022         br_max  => 65535,
    023         @options,
    024     };
    025 
    026     $self->{video} = 
    027         Video::Capture::V4l->new() or
    028             LOGDIE "Open video failed: $!";
    029 
    030     bless $self, $class;
    031 }
    032 
    033 ###########################################
    034 sub cam_bright {
    035 ###########################################
    036     my($self, $brightness) = @_;
    037 
    038     my $pic = $self->{video}->picture();
    039     $pic->brightness($brightness);
    040     $pic->set();
    041 }
    042 
    043 ###########################################
    044 sub img_avg {
    045 ###########################################
    046   my($img) = @_;
    047 
    048   my $br = Imager::Misc::brightness($img);
    049   DEBUG "Brightness: $br";
    050   return $br;
    051 }
    052 
    053 ###########################################
    054 sub calibrate {
    055 ###########################################
    056   my($self) = @_;
    057 
    058   DEBUG "Calibrating";
    059 
    060   return if 
    061    img_avg($self->capture($self->{br_min}))
    062    > $self->{avg_opt};
    063 
    064   return if 
    065    img_avg($self->capture($self->{br_max}))
    066    < $self->{avg_opt};
    067 
    068       # Binary search
    069   my($low, $high) = ($self->{br_min}, 
    070                      $self->{br_max});
    071 
    072   for(my $max = 5; 
    073       $low <= $high && $max; 
    074       $max--) {
    075     my $try = int( ($low + $high) / 2);
    076 
    077     my $i  = $self->capture($try);
    078     my $br = img_avg($i);
    079 
    080     DEBUG "br=$try got avg=$br";
    081     return if abs($br-$self->{avg_opt}) <= 
    082               $self->{avg_acc};
    083 
    084     if($br < $self->{avg_opt}) {
    085         $low = $try + 1;
    086     } else {
    087         $high = $try - 1;
    088     }
    089   }
    090   # Nothing found, use last setting
    091 }
    092 
    093 ###########################################
    094 sub capture {
    095 ###########################################
    096     my($self, $br) = @_;
    097 
    098     $self->cam_bright($br) if defined $br;
    099 
    100     my $frame;
    101     for my $frameno (0, 1) {
    102        $frame = $self->{video}->capture(
    103               $frameno, $self->{width}, 
    104               $self->{height});
    105 
    106        $self->{video}->sync($frameno) or 
    107                LOGDIE "Unable to sync";
    108     }
    109 
    110     my $i = Imager->new();
    111     $frame = reverse $frame;
    112     $i->read(
    113       type => "pnm", 
    114       data => "P6\n$self->{width} " .
    115               "$self->{height}\n255\n" . 
    116               $frame
    117     );
    118     $i->flip(dir => "hv");
    119     return $i;
    120 }
    121 
    122 1;

Wird das Ganze, wie in Zeile 113 geschehen, noch von einem ``P6''-Header eingeleitet und die Breite und Höhe des Bildes angegeben, kann die Methode read() des CPAN-Moduls Imager daraus ein Bild im PNM-Format zaubern. Allerdings hat sich durch die Umkehrung mit reverse auch die Reihenfolge der Pixel umgedreht, so dass das Bild nun auf dem Kopf steht. Dies wird sofort durch einen Aufruf der Methode flip rückgängig gemacht, die mit den Parametern dir => "hv" eine vertikale 180-Grad-Spiegelung vornimmt. Die Methode capture() liefert ein Objekt vom Typ Imager zurück, das die aufrufende Funktion dann weiterverarbeiten kann.

Maschinennahes Rasen

Um die Kamera auf das Umgebungslicht einzustellen, wird ein Testbild untersucht und anhand dessen Helligkeit die Empfindlichkeit der Kamera entweder nach oben oder unten korrigiert. Doch wie misst man, ob ein Bild 'richtig' belichtet wurde?

Die Abbildungen 2 und 3 zeigen die Verteilungen aller RGB-Werte der Pixel zweier unterschiedlicher Bilder. Das Histogramm in Abbildung 2 stammt von einem stark unterbelichteten Bild, das zwar einige RGB-Werte im unteren Bereich aufweist, dann aber schlagartig abreisst und keinerlei hellere Töne zeigt. Abbildung 3 hingegen zeigt ein Histogramm eines normal belichteten Bildes. Fast alle Werte zwischen 0 und 255 sind gleichmäßig vertreten.

Abbildung 3: Histogramm eines schwach ausgeleuchteten Fotos

Abbildung 4: Histogramm eines normal ausgeleuchtetes Bildes

Um nun die Helligkeit eines aufgenommenen Testbildes zu bestimmen, verwenden wir eine primitiven Algorithmus: Alle RGB-Werte werden aufaddiert, durch drei und die Anzahl der Pixel geteilt, und wenn dann etwa die Hälfte von 256 herauskommt, ist das Bild einigermaßen ausgewogen.

Allerdings ist Perl nicht gerade für solche Sprints konzipiert. Ein Bild mit 320 mal 240 Pixeln, von denen jeder einen Wert im Rot-, Blau- und Grün-Kanal hat, besitzt 230.400 Datenpunkte. Diese alle abzuklappern, dauert Zeit, und wenn nicht jeder Zugriff blitzschnell erfolgt, gestaltet sich die Berechnung äußerst schleppend.

Das Imager-Modul lässt sich aber glücklicherweise leicht auf C-Ebene erweitern. Dort kann man maschinennah durch die Datenstrukturen rasen und ermittelte Ergebnisse elegant ins Perlskript zurückgeben. Hierzu legt man in der entpackten Distribution des Imager-Moduls vom CPAN (denn einige Header werden gebraucht) einfach mit

    h2xs -Axn Imager::Misc

ein neues Unterverzeichnis Imager-Misc an. In der darunter entstandenen Datei Makefile.PL ist die Zeile INC => -I. in INC => -I.. umzuändern, damit ein nachfolgendes perl Makefile.PL und schließlich ein make auch die Include-Dateien der Imager-Distribution findet. Außerdem hat h2xs auch eine Datei Misc.xs für den neuen C-Code erzeugt, der in Listing Misc.xs zu sehen ist.

Listing 3: Misc.xs

    01 #ifdef __cplusplus
    02 extern "C" {
    03 #endif
    04 #include "EXTERN.h"
    05 #include "perl.h"
    06 #include "XSUB.h"
    07 #include "ppport.h"
    08 #ifdef __cplusplus
    09 }
    10 #endif
    11 
    12 #include "imext.h"
    13 #include "imperl.h"
    14 
    15 DEFINE_IMAGER_CALLBACKS;
    16 
    17 /* ===================================== */
    18 int
    19 brightness(i_img *im) {
    20   int     x, y;
    21   i_color val;
    22   double  sum;
    23   int     br;
    24   int     avg;
    25 
    26   for(x = 0; x < im->xsize; x++) {
    27     for(y = 0; y < im->ysize; y++) {
    28       i_gpix(im, x, y, &val);
    29       br = (val.channel[0] + val.channel[1]
    30                      + val.channel[2]) / 3;
    31       sum += br;
    32     }
    33   }
    34 
    35   avg = sum / ((int) (im->xsize) * 
    36                (int) (im->ysize));
    37   return avg;
    38 }
    39 
    40 /* ===================================== */
    41 int
    42 changed(i_img *im1, i_img *im2, int diff) {
    43   int     x, y, z, chan;
    44   i_color val1, val2;
    45   int     diffcount = 0;
    46 
    47   for(x = 0; x < im1->xsize; x++) {
    48     for(y = 0; y < im1->ysize; y++) {
    49 
    50       i_gpix(im1, x, y, &val1);
    51       i_gpix(im2, x, y, &val2);
    52 
    53       for(z = 0; z < 3; z++) {
    54         if(abs(val1.channel[z] - 
    55                val2.channel[z]) > diff)
    56           diffcount++;
    57       }
    58     }
    59   }
    60 
    61   return diffcount;
    62 }
    63 
    64 /* ===================================== */
    65 MODULE=Imager::Misc PACKAGE=Imager::Misc
    66 
    67 PROTOTYPES: ENABLE
    68 
    69 int
    70 brightness(im)
    71         Imager::ImgRaw im
    72 
    73 int
    74 changed(im1, im2, diff)
    75         Imager::ImgRaw im1
    76         Imager::ImgRaw im2
    77         int diff
    78 
    79 BOOT:
    80         PERL_INITIALIZE_IMAGER_CALLBACKS;

Listing Misc.xs zeigt ab Zeile 18 den C-Code der Funktion brightness() und ab Zeile 64 das notwendige Perl-XS-Voodoo, um ihn in ein Perl-Skript einzubinden. Die Breite des hereingereichten Bildes in Pixeln ist mit im->xsize verfügbar, die Höhe mit im->ysize. Zwei for-Schleifen laufen über alle Pixel und das Makro i_gpix ruft intern eine Funktion auf, die die Farbwerte eines Pixels der Bildposition (x,y) in der Struktur val ablegt. Anschließend kann zum Beispiel mit val.channel[0] der Rotwert des Pixels hervorgeholt werden.

Das Modul Imager::Misc wird mit der bekannten Folge perl Makefile.PL; make; make install übersetzt und installiert. Bindet ein Perl-Skript das Modul mit use Imager::Misc ein, steht die Funktion Imager::Misc::brightness zur Verfügung, die ein Imager-Bild entgegennimmt und als Maß für dessen Helligkeit einen Integerwert zurückliefert.

Der einfache Algorithmus berechnet für das zu dunkle Bild in Abbildung 3 den Wert 7, während sich für das normal belichtete Foto gemäß dem Histogramm in Abbildung 4 der Wert 125 für brightness() ergibt.

Um nun die Kamera auf die aktuell herrschenden Lichtverhältnisse einzustellen, macht die Methode calibrate() aus Camcap.pm eine Testaufnahme, ermittelt den Rückgabewert der schnellen brightness-Funktion und vergleicht ihn mit dem 'Idealwert' 128. Ist der gemessene Wert darunter, stellt calibrate() mit cam_bright() eine höhere Kameraempfindlichkeit ein. Liegt der Messwert über dem Idealwert, ist das Bild also überbelichtet, wird cam_bright() vor der nächsten Testaufnahme mit einem reduzierten Wert aufgerufen.

Am Anfang macht calibrate() zwei Aufnahmen mit maximaler bzw. minimaler Kameraempfindlichkeit. Stellt sich heraus, dass selbst bei maximaler Empfindlichkeit das Bild zu dunkel (oder bei minimaler Empfindlichkeit das Bild zu hell) ist, hilft alles nichts und der gerade eingestellte Wert wird beibehalten.

Lässt sich hingegen etwas ausrichten, startet ab Zeile 68 in Capcam.pm eine Binärsuche für die optimale Empfindlichkeit zwischen 0 und 65535. In maximal 5 Durchgängen wird jeweils in der Mitte des Intervalls eine Messung vorgenommen. Ist das Bild zu dunkel, fährt der Algorithmus in der oberen Hälfte des Intervalls mit der Suche fort. Ist es zu hell, kommt hingegen die untere Hälfte dran. Am Ende der Suche sollte die Kamera ein Bild produzieren, das einen Helligkeitswert von 128 +/-20 (Unschärfeparameter avg_acc) liefert.

Differenzen

Läuft die Webcam ununterbrochen, produziert sie ein Unmenge an Bildmaterial. Für Überwachungsaufgaben sollen aber nur diejenigen Bilder gespeichert werden, auf denen sich eine signifikante Änderung gegenüber der letzten Aufnahme zeigt.

Hierzu definiert Misc.xs eine Funktion changed, die die Anzahl der RGB-Werte zurückliefert, die in zwei Bildern unterschiedlich sind. Außer zwei Pointern auf i_img-Strukturen (Imager::ImgRaw-Objekte auf der Perl-Ebene) nimmt es mit dem Parameter diff eine Mindestdifferenz für Kanalwerte entgegen. Ist der Rotwert eines Pixels des ersten Bildes zum Beispiel 15 und der Rotwert des gleichen Pixels des zweiten Bildes 30, wird der Zähler diffcount um Eins erhöht, falls der diff-Paramter 15 oder größer ist. Damit sollen statistische Schwankungen kompensiert werden, die aufgrund von natürlichen Lichtschwankungen und des Rauschens von CCD- Chips unweigerlich auftreten,

Das Skript tracker läuft in einer Endlosschleife, schießt Aufnahme um Aufnahme, speichert neue Bilder aber nur, falls Imager::Misc::changed() eine signifikante Änderung signalsiert. Sonst überschreibt es einfach das letzte Bild im Cache, um graduellen Änderungen auf der Spur zu bleiben.

Gesicherte Bilder landen in einem Cache der Marke Cache::SharedMemoryCache und werden automatisch nach 48 Stunden gelöscht. Die Bilder werden unter einem Datumschlüssel (z. B. ``2006/03/28-11:21:22'') gespeichert. Um das letzte Bild aus dem Cache zu holen, ruft tracker einfach die Cache-Funktion get_keys() auf, die sämtliche bekannten Schlüssel zurückliefert. Die Funktion maxstr aus dem Modul List::Util sucht sich daraus das jüngste Datum aus. Das zugehörige Bild liefert dann einfach die Cache-Funktion get() mit dem Schlüssel als Argument.

Um die Kamera alle fünf Minuten neu zu kalibrieren und auf die aktuellen Lichtverhältnisse einzustellen, wird unter dem Schlüssel calibrated ein Eintrag im Cache abgelegt, der automatisch alle 300 Sekunden gelöscht wird. Findet tracker den Eintrag nicht mehr, leitet es eine neue Kalibrierung ein und setzt den Eintrag neu.

Das Skript cacheprint holt die Aufnahmen aus dem Cache, dank Shared Memory kann es tatsächlich auf die von tracker erzeugten Speicherdaten zugreifen. Aus den Daten macht es JPEG-Bilder us und legt sie auf der Festplatte in einem neu erzeugten temporären Verzeichnis ab. Anschließend ruft cacheprint das Programm montage aus dem Image-Magick-Fundus auf, das Linux-Distributionen üblicherweise beiliegt und Zusammenstellungen erzeugt, die wie Contact-Prints aussehen. Der anschließend aufgerufene Viewer xv holt die Thumbnails mit den zugehörigen Datumsangaben auf dem Bildschirm (Abbildung 5).

Abbildung 4 zeigt den kontinuierlichen Strom von Aufnahmen, den tracker für speichernswert hielt. Sie zeigen die surreale Welt der Perlmeister-Studios mit der Besetzungscouch im Vordergrund. Um 0:09 wird das Licht im Arbeitszimmer ausgeknipst, und ab 1:17 tut sich dann auch in der restlichen sichtbaren Wohnung nichts mehr. Bei graduellen Veränderungen zwischen 1:17 und 6:45 wurde die Aufnahme mit dem Zeitstempel 01:17:03 kontinuierlich überschrieben, bis um 06:45:47 eine im dunkeln tappende Person eine signifikante Anzahl von Pixels verändert und tracker die Bewegung registriert. Um 07:07:56 wird der Vorhang aufgezogen und das Tageslicht fällt herein.

Listing 4: tracker

    01 #!/usr/bin/perl
    02 use strict;
    03 use warnings;
    04 use Camcap;
    05 use Imager::Misc;
    06 use Log::Log4perl qw(:easy);
    07 use Cache::SharedMemoryCache;
    08 use Time::Piece;
    09 use List::Util qw(maxstr);
    10 
    11 my $c = Cache::SharedMemoryCache->new({
    12     namespace          => "tracker",
    13     default_expires_in => 48*3600 });
    14 
    15 Log::Log4perl->easy_init($DEBUG);
    16 
    17 my $cam = Camcap->new();
    18 
    19 while(1) {
    20   my $lkey = maxstr grep /\d/,
    21                     $c->get_keys();
    22 
    23   if(! $c->get("calibrated")) {
    24     $cam->calibrate();
    25     $c->set("calibrated", 1, 300);
    26     my $img = $cam->capture();
    27     saveimg($img, $c, $lkey);
    28     next;
    29   }
    30 
    31   my $img = $cam->capture();
    32 
    33   if($lkey) {
    34     my $limg = Imager->new();
    35     $limg->read(type => "jpeg", 
    36                 data => $c->get($lkey));
    37     my $dpix = Imager::Misc::changed($limg,
    38                                  $img, 80);
    39     DEBUG "$dpix pixels changed";
    40     if($dpix > 2000) {
    41         saveimg($img, $c);
    42         next;
    43     } else {
    44           # minor change, 
    45           # refresh reference
    46         saveimg($img, $c, $lkey);
    47     }
    48   } else {
    49       # save first img
    50     saveimg($img, $c);
    51   }
    52 
    53   sleep(1);
    54 }
    55 
    56 ###########################################
    57 sub saveimg {
    58 ###########################################
    59     my($img, $cache, $date) = @_;
    60 
    61     if(! $date) {
    62         $date = localtime()->
    63              strftime("%Y/%m/%d-%H:%M:%S");
    64     }
    65 
    66     DEBUG "Saving image $date";
    67     $img->write(type => "jpeg", 
    68                 data => \my $val) or die;
    69     $cache->set($date, $val);
    70 }

Listing 5: cacheprint

    01 #!/usr/bin/perl -w
    02 use strict;
    03 use Imager;
    04 use Cache::FileCache;
    05 use Time::Piece;
    06 use List::Util qw(maxstr);
    07 use Sysadm::Install qw(rmf mkd cd);
    08 use File::Temp qw(tempdir);
    09 use Log::Log4perl qw(:easy);
    10 Log::Log4perl->easy_init($DEBUG);
    11 
    12 use DateTime;
    13 use DateTime::Format::Strptime;
    14 my $format = DateTime::Format::Strptime->new(
    15                         pattern   => "%Y/%m/%d-%H:%M:%S");
    16 my $today  = DateTime->today();
    17 
    18 my $dir = tempdir(CLEANUP => 1);
    19 print "dir=$dir\n";
    20 
    21 my $c = Cache::FileCache->new({
    22     namespace => "tracker",
    23 });
    24 
    25 $c->Purge();
    26 
    27 for my $date (sort $c->get_keys()) {
    28 
    29   next unless $date =~ /\d/;
    30 
    31   # 2006/05/19-01:02:40
    32   my $dt = $format->parse_datetime($date);
    33   die "Cannot parse '$date'" unless $dt;
    34 
    35   my $days = $dt->delta_days($today)->delta_days();
    36   next if $days > 1;
    37   print "$dt: days: $days\n";
    38 
    39   my $val = $c->get($date);
    40   my $img = Imager->new();
    41   $img->read(type => "jpeg", 
    42              data => $val);
    43   $date =~ s#/#-#g;
    44   DEBUG "Writing $date";
    45   $img->write(file => "$dir/$date.jpg") or 
    46       warn "Can't write $dir/$date.jpg ($!)";
    47 }
    48 
    49 cd $dir;
    50 my $str = "";
    51 for (<*.jpg>) {
    52     (my $date = $_) =~ s/\.jpg//g;
    53     $str .= "-label $date $_ ";
    54 }
    55 `montage -tile 6x6 $str sequence.jpg`;
    56 `xv $_` for <sequence*>;

Abbildung 5: Die von 'tracker' ausgewählten Bilder, ausgegeben vom Skript 'cacheprint'.

Infos

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

[2]
Definition der Bildhelligkeit: http://en.wikipedia.org/wiki/Brightness

[3]
Marc Lehmann, ``Capturing Video in Real Time'', The Perl Journal, 2005/02

[4]
Website des Perl-Moduls Imager: http://imager.perl.org/

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.