Wer da? (Linux-Magazin, Dezember 2016)

Statt langweilige Überwachungsvideos manuell durchzusehen, auf denen zu 90% nichts passiert, setzt Perlmeister Michael Schilli lieber die Bilderkennungssoftware OpenCV ein, die automatisch den Handlungsablauf an den interessantesten Stellen extrahiert.

Eine extrem ungleiche Einkommensverteilung, gepaart mit einer gewissen Wurstigkeit der Gesetzeshüter in meiner Wahlheimat San Francisco hat mittlerweile dazu geführt, dass kein Tag vergeht, ohne dass hunderte von Autos, Garagen und Häuser aufbrochen werden und dort gelagerte Sachen Füße bekommen. Statt mich darüber aufzuregen, lagere ich nichts Wertvolles mehr an einfach zugänglichen Stellen, und installiere heimlich Security-Kameras, um mich an Video-Aufnahmen von Dieben bei der Arbeit zu erfreuen.

Sogar ohne Stromkabel

Freilich ist die Installation einer Security-Kamera kein leichtes Unterfangen, denn Kabel müssen verlegt und Verbindungen zum Monitor hergestellt werden. Auch wenn die Kamera an sich drahtlos mit der Zentrale kommuniziert, benötigt sie dennoch eine Stromversorgung und die ist oft an den heißesten Brennpunkten wie in der Tiefgarage oder im Treppenhaus nicht leicht zu bekommen. Vor einigen Jahren kam die Firma Arlo deshalb mit batteriebetriebenen kinderfaustgroßen Kameras heraus ([2]), die der Hobbyspion einfach mittels eines Magneten aufhängt. Aufgenommene Videos senden die kleinen Wunderwerke vollkommen drahtlos zu einem bis zu etwa 30 Meter entfernten Hub, der sie wiederum übers Internet auf einen Server spielt, von dem allerlei Smartphone-Apps sowie eine Webseite die Daten auf Wunsch auf den Bildschirm des Users übertragen.

Schonbetrieb

Um die vier Litium-Batterien in der Kamera so weit zu schonen, dass sie etwa einen Monat halten, darf die Kamera etwa ein halbes Dutzend mal am Tag anspringen, wenn sie Bewegungen in ihrer Nähe feststellt, um dann jeweils ein einminütiges Video zu übertragen. Diese Filmchen darf der User dann von Arlos Webseite herunterladen und zum Beispiel golf-clappend dabei zusehen, wie Diebe gerade sein neues Rennrad aus der Garage schleppen. Meist ist allerdings nur am Anfang eines Überwachungsvideos eine Bewegung zu sehen, der Rest der einminütigen Aufnahmen zeigt üblicherweise gähnende Leere.

Abbildung 1: Ohne Stromversorgung findet die kleine Arlo-Kamera auch im kleinsten Kabuff Platz.

"Cut to the Chase" sagt der Amerikaner, wenn jemand nicht gleich zur Sache kommt. Das Bonmot bezieht sich wohl auf Aktionfilme, bei denen der Zuschauer keinen langen Spannungsbogenaufbau wünscht, sondern am liebsten gleich zum Autoverfolgungsrennen am Höhepunkt der Hollywood-Produktion vorspulen möchte. In diesem Sinne wäre es schön, wenn eine Software die Videos nach Frames durchforstete, in denen sich tatsächlich ein Objekt durchs Bild bewegt, sodass der Zuseher vorab weiß, ob sich das Ansehen des Videos überhaupt lohnt und zu welchen Stellen schleunigst vorspulen sollte.

Abbildung 2: Von der Kamera aufgenommene Videos stehen zum Download bereit.

Zappelfilmchen

Am einfachsten wäre ein Schnelldurchlauf des Videos, zum Beispiel mit Hilfe des Parameters fps (Frames per Second) im mplayer

     mplayer -framedrop -fps 150 video.mp4

Der Parameter -framedrop wirft in diesem Modus Frames einfach weg, falls die CPU mit dem dekodieren nicht mehr nachkommt. Heraus kommt in diesem Fall ein etwa fünfmal so schnell wie normal laufendes Video, das etwas an die Serie "Als die Bilder laufen lernten" oder alte handgekurbelte Werke von Charlie-Chaplin erinnert. Aber das Verfahren erfordert einen menschlichen Gutachter, während mir ein automatischer Vorgang vorschwebte, der zu einem automatisch eingeholten Video als Metadaten wie auf einem Kontaktabzug eines Fotografen die wichtigsten Sekunden mitsamt illustrierender Thumbnails auflistet.

Abbildung 3: Die meisten Frames im Überwachungsvideo zeigen nur die geschlossene Tür.

Trendthema Motion Detection

Zum Thema Mustererkennung in der Bildverarbeitung hat sich in den letzten Jahrzehnten viel getan und das Paket OpenCV bietet sogar einige hochwissenschaftliche Routinen als Open-Source-Software an. Um festzustellen, ob sich gerade ein Objekt durchs Video bewegt, muss ein Programm die Einzelbilder im Videostrom auslesen und feststellen, welche Bildpunkte sich von den Koordinaten X,Y zu den Koordinaten X+deltaX, Y+deltaY verschoben haben. Findet sich eine größere zusammenhängende Fläche, für das offensichtlich der Fall ist, fand zwischen den beiden Frames eine Bewegung statt.

Eines der in OpenCV gebündelten Verfahren heißt "Lucas-Kanade" ([4]) und es versucht, "Optical Flow" in einem Video zu finden, also Bereiche um zentrale Punkte, die sich gemeinschaftlich von einem Frame zum nächsten verschieben. Dazu benötigt es zunächst eine Reihe "interessanter" Bereiche, deren Beobachtung Erfolg verspricht, und die ein weiterer Algorithmus aus einem Bild zu extrahieren versucht. Letzterer konzentriert sich auf Bildpunkte in Flächen mit erkennbarer Struktur oder an Kanten dargestellter Objekte.

Zur Lucas-Kanade-Analyse stellt das Paket OpenCV (auf Ubuntu heißt es libopencv-dev) die Funktion calcOpticalFlowPyrLK() mit sage und schreibe 11 Parametern bereit.

Klimmzüge mit cmake

Das C++-Programm in Listing 1 schnappt sich ein Video und erkennt Bewegungen von Objekten zwischen verschiednen Frames. Es in ein ausführbares Programm umzuwandeln erfordert einige Klimmzüge mit Include-Dateien und Link-Libraries, was am einfachsten mit cmake und dem Meta-Makefile in Listing 2 geht. Ein cmake . (der Punkt steht für das aktuelle Verzeichnis in dem die Datei CMakeLists.txt residiert) und anschließendes make erzeugen nach einigen Zwischenschritten schließlich das Binary max-movement-lk, das eine Video-Datei erwartet und die Sekundenwerte im Video ausspuckt, an denen Bewegungen stattfinden.

Listing 1: max-movement-lk.cpp

    01 #include "opencv2/opencv.hpp"
    02 
    03 using namespace std;
    04 using namespace cv;
    05 
    06 const int MAX_FEATURES = 500;
    07 const int MAX_MOVEMENT = 100;
    08 
    09 int move_test(Mat& oframe, Mat& frame) {
    10     // Select features for optical flow 
    11   vector<Point2f> ofeatures;
    12   goodFeaturesToTrack(oframe, 
    13     ofeatures, MAX_FEATURES, 0.1, 0.2 );
    14 
    15     // Parameters for LK
    16   vector<Point2f> new_features;
    17   vector<uchar> status;
    18   vector<float> err;
    19   TermCriteria criteria(TermCriteria::COUNT
    20       | TermCriteria::EPS, 20, 0.03);
    21   Size window(10,10);
    22   int max_level   = 3;
    23   int flags       = 0;
    24   double min_eigT = 0.004;
    25 
    26     // Lucas-Kanade method
    27   calcOpticalFlowPyrLK(oframe, frame, 
    28     ofeatures, new_features, status, err, 
    29     window, max_level, criteria, flags, 
    30     min_eigT );
    31 
    32   double max_move = 0;
    33   double movement = 0;
    34   for(int i=0; i<ofeatures.size(); i++) {
    35     Point pointA 
    36       (ofeatures[i].x, ofeatures[i].y);
    37     Point pointB
    38       (new_features[i].x, new_features[i].y);
    39 
    40     movement = norm(pointA-pointB);
    41     if(movement > max_move)
    42         max_move = movement;
    43   }
    44   return max_move > MAX_MOVEMENT;
    45 }
    46 
    47 int main(int argc, char *argv[]) {
    48   int i = 0;
    49   Mat frame;
    50   Mat oframe;
    51 
    52   if (argc != 2) {
    53     cout << "USAGE: <cmd> <file_in>\n";
    54     return -1;
    55   }
    56 
    57   VideoCapture vid(argv[1]);
    58   if (!vid.isOpened()) {
    59     cout << "Video corrupt\n";
    60     return -1;
    61   }
    62 
    63   int fps = (int)vid.get(CV_CAP_PROP_FPS);
    64 
    65   i++;
    66   if(!vid.read(oframe)) return 1;
    67 
    68   cvtColor(oframe, oframe, COLOR_BGR2GRAY);
    69 
    70   while (1) {
    71     if (!vid.read(frame))
    72       break;
    73     i++;
    74 
    75     cvtColor(frame,frame,COLOR_BGR2GRAY);
    76     if(move_test(oframe, frame))
    77       cout << i/fps << "\n";
    78     oframe = frame;
    79   }
    80 
    81   return 0;
    82 }

Listing 2: CMakeLists.txt

    1 cmake_minimum_required(VERSION 2.8)
    2 project( max-movement-lk )
    3 find_package( OpenCV REQUIRED )
    4 add_executable( max-movement-lk max-movement-lk.cpp )
    5 target_link_libraries( max-movement-lk ${OpenCV_LIBS} )

Hierzu liest die Hauptfunktion main() den Dateinamen des Videos von der Kommandozeile und öffnet in Zeile 57 eine VideoCapture aus dem OpenCV-Paket. Die Framerate, also die Anzahl der Bilder pro Sekunde, liest Zeile 63 aus der Video-Datei und speichert sie in der Variablen fps ab. Da der LK-Algorithmus am besten mit Grauton-Bildern funktioniert, entziehen die Zeilen 68 und 75 dem gerade analysierten Frame die Farbe. Eine while-Schleife iteriert über alle Frames und die Funktion move_test() in Zeile 76 prüft, ob sich zwischen dem zuletzt gelesenen Frame oframe und dem aktuellen frame eine Bewegung nachweisen lässt. Ist dies der Fall, teilt Zeile 77 den Zählerwert durch den Frames-per-Second-Wert des Videos und erhält somit den Zeitwert in Sekunden im Video, an dem die Bewegung stattfand.

Der Algorithmus bei dem jeder mit muss

Der aus [3] entliehene Algorithmus in der Funktion move_test() ab Zeile 9 ruft zunächst die Funktion goodFeaturesToTrack(() aus dem OpenCV-Paket auf, um im alten Frame oframe interessante Punkte aufzustöbern, deren Maximalzahl die Konstante MAX_FEATURES auf 500 setzt. Zeile 27 ruft dann calcOpticalFlowPyrLK(oframe() auf, und zurück kommt in new_features eine Reihe von Bereichen, die sich gegenüber ofeatures im letzten Frame offenbar verschoben haben. Die For-Schleife ab Zeile 34 iteriert über sie und sucht den Bereich, der den weitesten Weg zurückgelegt hat. Überschreitet einer davon den Wert 500, gibt Zeile 44 in move_test() den Wert 1 zurück, deutet also an, dass offenbar eine Bewegung stattgefunden hat.

Abbildung 4: Der Motion-Filter zeigt nur die Videosekunden, in denen tatsächlich etwas passiert.

Aufmoppen und Anzeigen

Listing 1 gibt also zu einem Video reihenweise Integerwerte aus, die die Sekundenwerte im Video angeben, an denen sich von einem Frame zum nächsten etwas im Bild bewegt hat. Es ist nun an Listing 2, diese Rohdaten aufzumoppen, Thumbnails an den entsprechenden Zeitpunkten zu generieren und das Ganze zu einer Übersicht wie in Abbildung 4 zusammenzufassen.

Listing 3: motion-meta

    01 #!/usr/local/bin/perl -w
    02 use strict;
    03 use File::Temp qw( tempdir );
    04 use File::Copy qw( move );
    05 use DateTime::Duration;
    06 use DateTime::Format::Duration;
    07 use Image::Magick;
    08 
    09 my %seen = ();
    10 
    11 my $video = shift @ARGV;
    12 if( !defined $video ) {
    13   die "usage: $0 video";
    14 }
    15 
    16 my $tmpdir = tempdir( CLEANUP => 1 );
    17 my $magick = Image::Magick->new;
    18 
    19 while( <> ) {
    20   chomp;
    21   my $second = $_;
    22   next if $seen{ $second }++;
    23 
    24   system "mplayer", "-ss", $second, $video,
    25     "-vo", "jpeg:outdir=$tmpdir", 
    26     "-ao", "null", "-frames", 1;
    27 
    28   my( $frame ) = glob "$tmpdir/0*";
    29   my $newname = sprintf 
    30     "$tmpdir/frame-%s.jpg",
    31       secs_format( $second );
    32   move $frame, $newname;
    33   $magick->Read( $newname );
    34 }
    35 
    36 my $montage = $magick->Montage(
    37     label  => "%f",
    38     shadow => "True",
    39     tile   => "5",
    40 );
    41 
    42 $montage->Write( "motion-meta.jpg" ) and
    43   die "write failed";
    44 
    45 sub secs_format {
    46   my( $secs ) = @_;
    47 
    48   my $fmt = 
    49     DateTime::Format::Duration->new(
    50       pattern => "%T" );
    51 
    52   return $fmt->format_duration(
    53     DateTime::Duration->new(
    54       seconds => $secs )
    55   );
    56 }

Für die Thumbnails nutzt es das gute alte Allround-Tool mplayer, spult damit mittels der Option -ss bis zur angegebenen Videosekunde vor und legt in einem temporären Verzeichnis $tmpdir den dort liegenden Frame ab. Die Option -frames 1 legt fest, dass danach Schicht im Schacht ist, keine weiteren Frames mehr ausgelesen werden, und mplayer sich beendet. Die Funktion move() aus dem CPAN-Modul File::Copy benennt die Datei im temporären Verzeichnis in eine im aktuellen um und rechnet den Sekundenwert mit Hilfe des CPAN-Moduls DateTime::Format::Duration ins Format SS::MM:ss um. Aus dem Frame an Sekunde 64 wird so die Datei 00:01:04.jpg.

Das Ubuntu-Paket perlmagick bringt das CPAN-Modul Image::Magick aufs System, mit dem es ganz einfach ist, aus mehreren Bilddateien sogenannte Montage-Konstrukte, also Kontaktabzüge im Format von Abbildung 4 zu erstellen. Der Aufruf

     $ max-movement-lk test.mp4 | ./motion-meta test.mp4

hängt die beiden Teile der Pipeline aneinander. Der erste Teil analysiert die Frames im Video und gibt die Sekundenwerte aus, an denen Bewegungen stattfanden. Der zweite schnappt die Sekundenwerte auf, dedupliziert sie, sucht die zugehörigen Thumbnails im Video heraus und montiert sie zu einem Kontaktabzug zusammen, wobei die Dateinamen so gewählt sind, dass sich der Zeitpunkt im Video in Minuten und Sekunden ablesen lässt.

10 Millionen pro Spezialist

Verfahren zum Erkennen von beweglichen Objekten in Videostreams kommen nicht nur bei Überwachungsvideos zum Einsatz. Auch selbstfahrende Autos analysieren aufgenommene Bilddaten mit ähnlichen Verfahren, um gefährdete Fußgänger von feststehenden Straßenschildern zu unterscheiden. Diese Techniken zu erlernen könnte sich im beruflichen Werdegang auszahlen: Laut Ex-Googler Sebastian Thrun überbieten sich die Firmen zurzeit in diesem Bereich gegenseitig und zahlen etwa 10 Millionen Dollar pro Fachkraft ([5]). Wer kann es sich leisten, dazu Nein sagen?

Infos

[1]

Listings zu diesem Artikel: http://www.linux-magazin.de/pub/listings/magazin/2016/12/perl-snapshot

[2]

"Arlo Security System", https://www.amazon.com/dp/B00P7EVST6

[3]

Oscar Deniz Suarez, "OpenCV Essentials", 2014

[4]

"Lucas–Kanade method", https://en.wikipedia.org/wiki/Lucas%E2%80%93Kanade_method

[5]

"Ex-Googler Sebastian Thrun says the going rate for self-driving talent is $10 million per person", http://www.recode.net/2016/9/17/12943214/sebastian-thrun-self-driving-talent-pool

Michael Schilli

arbeitet als Software-Engineer in der San Francisco Bay Area in Kalifornien. In seiner seit 1997 laufenden Kolumne forscht er jeden Monat nach praktischen Anwendungen der Skriptsprache Perl. Unter mschilli@perlmeister.com beantwortet er gerne Ihre Fragen.