Aus Linux-Magazin 06/2006

Videoüberwachung mit Webcam und Perl

© photocase.com

Eine Webcam bietet die billigste Möglichkeit, die eigene Burg oder die Schwiegermutter zu überwachen. Die Skripte dieses Perl-Snapshots kalibrieren automatisch die Belichtung, wofür sie als Turbo C-Code einspannen, und fischen dann die interessanten Bilder aus dem Datenstrom der Kamera.

Die meisten Webcams bringen Windows-Software mit, die unter Linux nicht verwendbar ist. Neuere Linux-Distributionen haben allerdings oft Video4Linux an Bord, das problemarm eine eingestöpselte USB-Kamera ansteuert. Die in diesem Snapshot verwendete Kamera Creative NX Ultra kann neben Standbildern auch Videos liefern und kostet etwa 70 Euro. Für die Benutzung als einfache Webcam ist sie eigentlich zu schade, aber sie lag in einer Schublade des Perlmeister-Labors herum und war somit verfügbar. Sie benötigt keine externe Stromversorgung und das Hot-Plugging erkennt sie, sobald der USB-Stecker im Rechner steckt. Ihre Videodaten erscheinen typischerweise unter »/dev/video0«. Das Perl-Modul Linux::Capture::V4l vom CPAN hält sich am Device-Eintrag fest, greift die Framedaten ab und erlaubt es, Aufnahmeparameter wie die Empfindlichkeit laufend zu verändern.

Listing 1:
»single«

01 #!/usr/bin/perl
02 
03 use strict;
04 use warnings;
05 use Camcap;
06 
07 my $cam = Camcap->new(width  => 640,
08                       height => 480);
09 $cam->cam_bright(42_000);
10 my $img = $cam->capture();
11 $img->write(file => 'buero.jpg')
12   or die "Can't write: $!";
Abbildung 1: Diese Kamera aus der Schublade des Perl-Meisters, eine NX Ultra von Creative, musste für die Versuche herhalten.

Abbildung 1: Diese Kamera aus der Schublade des Perl-Meisters, eine NX Ultra von Creative, musste für die Versuche herhalten.

Abstrakter Bilderdieb

Listing 1 zeigt eine einfache Anwendung, die erst die Empfindlichkeit der Kamera auf 40000 einstellt, ein Bild aus dem Videostrom abzwackt und es anschließend als Jpeg-Foto auf der Festplatte ablegt (Abbildung 2). Das von »single« verwendete Modul »Camcap« (Listing 2) abstrahiert den Zugriff auf den Videostrom. Der Konstruktor ab Zeile 12 definiert einige Default-Parameter wie die Bildbreite und -höhe und die minimale und maximale Helligkeitseinstellung (»br_min«, »br_max«). Anschließend hängt sich der Code über das CPAN-Modul Video::Capture::V4l an das Videodevice »/dev/video0« an. Lauscht schon ein anderer Interessent daran, schlägt die Verbindung fehl.

Die ab Zeile 34 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 Datenstruktur Picture der Kamera, setzt in ihr mit »brightness()« den übergebenen Empfindlichkeitswert und übergibt sie anschließend mit der Methode »set()« an die Video4Linux-Schicht.

Die Methode »capture()« ab Zeile 94 nimmt optional eine Empfindlichkeitsvorgabe entgegen und macht sich dann daran, den nächsten Frame aus dem Videostrom abzugreifen. Der erste Frame erhält die Nummer 0. Ein anschließender Aufruf der Methode »sync()« mit der Framenummer stellt sicher, dass die Bilddaten auch gut im Skalar » ankommen.

Abbildung 2: Dieses Bild fischte ein kleines Perl-Programm aus dem Videostrom der Webcam.

Abbildung 2: Dieses Bild fischte ein kleines Perl-Programm aus dem Videostrom der Webcam.

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 => "P6n$self->{width} " .
115               "$self->{height}n255n" .
116               $frame
117     );
118     $i->flip(dir => "hv");
119     return $i;
120 }
121 
122 1;

Vom Kopf auf die Füße

Einige Tests zeigen, dass manchmal der erste vorbeisausende Frame 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. Sobald die Methode »sync()« zurückgekehrt ist, liegen in der Variablen » die rohen Bilddaten im BGR-Format. Drei aufeinander folgende Bytes beschreiben jedes Pixel mit den Blau-, Grün- und Rotwerten (jeweils 0 bis 255). Um daraus ein Format 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.

Wird das Ganze, wie in Zeile 114 geschehen, noch von einem P6-Header für das PPM-Format eingeleitet und die Breite und Höhe des Bildes angegeben, kann die Methode »read()« des CPAN-Moduls Imager daraus ein Bild zaubern. Allerdings hat sich durch die Umkehrung mit »reverse« auch die Reihenfolge der Pixel umgedreht, sodass das Bild nun auf dem Kopf steht. Das macht ein Aufruf der Methode »flip()« sofort rückgängig, die mit dem Parameter »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.

Belichtungsmesser

Um die Kamera auf das Umgebungslicht einzustellen, untersucht ein Skript ein Testbild und korrigiert die Empfindlichkeit der Kamera entsprechend der Bildhelligkeit. Aber noch bleibt im Dunkeln, ob das Bild richtig belichtet ist? Die Abbildungen 3 und 4 zeigen die Verteilungen aller RGB-Werte der Pixel zweier unterschiedlicher Bilder. Das Histogramm in Abbildung 3 stammt von einem stark unterbelichteten Bild, das zwar einige RGB-Werte im unteren Bereich aufweist, dann aber abreißt und keine helleren Töne enthält. Abbildung 4 zeigt das Histogramm eines normal belichteten Bildes. Fast alle Werte zwischen 0 und 255 sind gleichmäßig vertreten.

Abbildung 3: Im Histogramm eines schwach ausgeleuchteten, unterbelichteten Fotos fehlen die höheren Helligkeitswerte.

Abbildung 3: Im Histogramm eines schwach ausgeleuchteten, unterbelichteten Fotos fehlen die höheren Helligkeitswerte.

Abbildung 4: Das Histogramm eines normal ausgeleuchteten Bildes zeigt eine gleichmäßige Verteilung der Helligkeitswerte.

Abbildung 4: Das Histogramm eines normal ausgeleuchteten Bildes zeigt eine gleichmäßige Verteilung der Helligkeitswerte.

Um nun die Helligkeit eines aufgenommenen Testbilds zu bestimmen, kommt ein primitiver Algorithmus zum Einsatz: Er addiert alle RGB-Werte und teilt die Summe durch drei und die Anzahl der Pixel. 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 jedes einen Wert im Rot-, Blau- und Grün-Kanal hat, umfasst 230400 Datenpunkte. Sie alle abklappern dauert seine Zeit, und wenn nicht jeder Zugriff blitzschnell erfolgt, gestaltet sich die Berechnung äußerst schleppend.

Turbo in C

Das Imager-Modul lässt sich aber glücklicherweise leicht durch C-Code erweitern. Der rast Maschinen-näher durch die Datenstrukturen und gibt die ermittelten Ergebnisse elegant ans Perl-Skript zurück. Hierzu legt

h2xs -Axn Imager::Misc

in der entpackten Distribution des Imager-Moduls vom CPAN (einige Header werden gebraucht) einfach ein neues Unterverzeichnis »Imager-Misc« an.

In der darunter entstandenen Datei »Makefile.PL« ist die Zeile »INC => -I.« in »INC => -I..« umzuändern, damit die nachfolgenden Kommandos »perl Makefile.PL« und »make« auch die Include-Dateien der Imager-Distribution finden. Außerdem hat »h2xs« eine Datei »Misc.xs« für den C-Code erzeugt, dessen neue Funktionen Listing 3 zeigt.

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;

Es zeigt ab Zeile 18 den C-Code der Funktion »brightness()« und ab Zeile 65 das Perl-XS-Voodoo, das ihn in ein Perl-Skript integriert (XS oder Xsubs: Perl-Interface, das C-Code einbindet, Abkürzung für Extended Subroutine). Die Breite des hereingereichten Bildes in Pixeln liefert »im->xsize«, die Höhe »im->ysize«. Zwei For-Schleifen laufen über alle Pixel und das Makro »i_gpix« ruft eine Funktion auf, die die Farbwerte an der Bildposition (x,y) in der Struktur »val« ablegt. Anschließend kann zum Beispiel »val.channel[0]« den Rotwert des Pixels hervorholen.

Die bekannte Folge »perl Makefile.PL; make; make install« übersetzt und installiert das Modul Imager::Misc. Bindet ein Perl-Skript das Modul mit »use Imager::Misc« ein, steht ihm danach die Funktion »Imager::Misc::brightness« zur Verfügung, die ihrerseits ein Imager-Bild entgegennimmt, untersucht und als Maß für dessen Helligkeit einen Integerwert zurückliefert.

Sonne lacht, Blende acht

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 die Kamera auf die herrschenden Lichtverhältnisse einzustellen, macht die Methode »calibrate()« aus Listing 2 eine Testaufnahme, ermittelt deren »brightness«-Wert und vergleicht ihn mit dem Idealwert 128. Liegt er darunter, stellt »calibrate()« mit »cam_bright()« eine höhere Kameraempfindlichkeit ein. Liegt er über dem Idealwert, ist das Bild also überbelichtet, startet »cam_bright()« die nächste Testaufnahme mit einem reduzierten Wert.

Am Anfang macht »calibrate()« zwei Aufnahmen mit maximaler beziehungsweise minimaler Kameraempfindlichkeit. Stellt sich heraus, dass selbst bei maximaler Empfindlichkeit das Bild zu dunkel (oder bei minimaler Empfindlichkeit zu hell) ist, hilft alles nichts, die Funktion behält dann den gerade eingestellten Wert bei.

Lässt sich hingegen etwas ausrichten, läuft ab Zeile 68 in Listing 2 eine Binärsuche für die optimale Empfindlichkeit zwischen 0 und 65535 an. Maximal fünf Durchgänge liefern jeweils in der Mitte des Intervalls einen Messwert. 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 bei +/-20 (Unschärfeparameter »avg_acc«) aufzuweisen hat.

Differenzen

Läuft die Webcam ununterbrochen, produziert sie ein Unmenge Bilder. Für Überwachungsaufgaben sollte der Rechner aber nur jene speichern, die eine signifikante Änderung gegenüber der letzten Aufnahme zeigen. Hierzu definiert »Misc.xs« aus Listing 3 eine Funktion »changed()«, sie liefert die Anzahl der RGB-Werte zurück, die in zwei Bildern unterschiedlich sind. Außer zwei Pointern auf »i_img«-Strukturen (»Imager::ImgRaw«-Objekte auf der Perl-Ebene) nimmt »Misc.xs« 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 im zweiten Bild 30, erhöht sich der Zähler »diffcount« um eins, falls der »diff«-Parameter mit einem Wert von 15 oder mehr belegt war. Das soll jene statistischen Effekte kompensieren, die aufgrund natürlicher Schwankungen der Lichtverhältnisse und durch das Rauschen von CCD-Chips unweigerlich auftreten.

Das Skript »tracker« aus Listing 4 läuft in einer Endlosschleife, schießt Aufnahme um Aufnahme, speichert neue Bilder aber nur, falls »Imager::Misc::changed()« eine signifikante Änderung signalisiert. Sonst überschreibt es einfach das letzte Bild im Cache, um graduellen Änderungen auf der Spur zu bleiben. Die gesicherten Bilder landen in einem Cache der Art »Cache::FileCache« und werden automatisch nach 48 Stunden gelöscht. Das Skript speichert die Bilder unter einem Datumsschlüssel (etwa »2006-03-28-11:21:22«).

Listing 4:
»tracker«

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

Um das letzte Bild aus dem Cache zu holen, ruft »tracker« einfach die Cache-Funktion »get_keys()« auf, die alle 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 die Cache-Funktion »get()« mit dem ermittelten Schlüssel als Argument.

Die Kamera soll sich alle fünf Minuten neu kalibrieren. Dafür sorgt ein Eintrag im Cache unter dem Schlüssel »calibrated«, den die Software automatisch alle 300 Sekunden löscht. Findet »tracker« den Eintrag nicht, leitet es eine neue Kalibrierung ein und setzt ihn neu.

Bildergalerie

Listing 5 holt die Aufnahmen aus dem Cache, macht daraus Jpeg-Bilder und legt sie in einem temporären Verzeichnis ab. Danach ruft es das Programm »montage« aus dem Imagemagick-Fundus auf, das den Linux-Distributionen üblicherweise beiliegt und Zusammenstellungen erzeugt, die ähnlich wie Contact-Prints aussehen. Der anschließend aufgerufene Viewer »xv« holt die Thumbnails auf den Bildschirm.

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 
10 my $dir = tempdir(CLEANUP => 1);
11 
12 my $c = Cache::SharedMemoryCache->new({
13     namespace => "tracker",
14 });
15 
16 for my $date (sort $c->get_keys()) {
17 
18   next unless $date =~ /d/;
19   my $val = $c->get($date);
20   my $img = Imager->new();
21   $img->read(type => "jpeg",
22              data => $val);
23   $date =~ s#/#-#g;
24   $img->write(file => "$dir/$date.jpg") or
25       die "Can't write $!";
26 }
27 
28 cd $dir;
29 my $str = "";
30 for (<*.jpg>) {
31     (my $date = $_) =~ s/.jpg//g;
32     $str .= "-label $date $_ ";
33 }
34 `montage -tile 6x6 $str sequence.jpg`;
35 `xv $_` for <sequence*>;

Abbildung 5 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 Uhr geht das Licht im Arbeitszimmer aus und ab 1:17 Uhr tut sich dann auch in der restlichen sichtbaren Wohnung nichts mehr. Bei nur graduellen Veränderungen zwischen 1:17 und 6:45 überschrieb die Software die Aufnahme mit dem Zeitstempel 01:17:03 immer wieder, bis um 06:45:37 eine im Dunkeln tappende Person eine signifikante Anzahl von Pixeln änderte und »tracker« die Bewegung registrierte. Um 07:07:56 zieht jemand den Vorhang auf und Tageslicht fällt herein. (jcb)

Abbildung 5: Das Skript gibt die von »tracker« ausgewählten Bilder nach Datum geordnet aus.

Abbildung 5: Das Skript gibt die von »tracker« ausgewählten Bilder nach Datum geordnet aus.

Der Autor


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 ist: [http://perlmeister. com]

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]

DIESEN ARTIKEL ALS PDF KAUFEN
EXPRESS-KAUF ALS PDFUmfang: 5 HeftseitenPreis €0,99
(inkl. 19% MwSt.)
LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Inline Feedbacks
Alle Kommentare anzeigen
Nach oben