5.11. Einzelbild-Differenzbildung¶
Bei der Einzelbild-Differenzbildung wird jedes neue Einzelbild mit einem gespeicherten Referenz-Einzelbild verglichen, um die Teile der Szene zu finden, die sich verändert haben. Sie ist das Arbeitspferd von Kameraanwendungen, die auf ein Ereignis warten – bewegungsausgelöste Aufnahmen, Einbruchswarnungen, „speichere ein Video, wenn sich etwas bewegt“ – und sie ist vollständig aus den zuvor behandelten pixelweisen Operationen aufgebaut: einer absoluten Differenz, einem Schwellenwert und einer Regionssuche, die auf jedem Einzelbild ausgeführt werden.
5.11.1. Die grundlegende Pipeline¶
Die erste Stufe besteht darin, eine Referenz zu erfassen. Irgendwann nahe dem Start – idealerweise wenn sich die Szene in dem Zustand befindet, den „keine Veränderung“ bedeutet – nimmt die Anwendung ein Einzelbild auf und behält es. Das Einzelbild wird zur Grundlinie, mit der jede nachfolgende Aufnahme verglichen wird.
reference = csi0.snapshot().copy()
Das .copy() ist wichtig. csi0.snapshot() allein gibt ein Image zurück, dessen Puffer im Framebuffer liegt, wo der nächste Aufruf von snapshot ihn überschreiben wird. .copy() reserviert einen separaten Puffer für die Referenz und lässt die Pixel dieses Einzelbildes über die nächste Aufnahme hinaus erhalten bleiben.
Die zweite Stufe läuft auf jedem Einzelbild: ein frisches Bild aufnehmen und dann die absolute Differenz zwischen ihm und der Referenz berechnen. Genau das tut difference():
current = csi0.snapshot()
current.difference(reference)
Nach diesem Aufruf enthält current ein Bild, dessen von null verschiedene Pixel jede Position markieren, an der sich die Szene seit der Aufnahme der Referenz verändert hat, wobei die Größe jedes Pixels proportional dazu ist, wie stark es sich an dieser Position verändert hat.
Die dritte Stufe wendet einen Schwellenwert auf das Differenzbild an. Die rohe Differenz enthält immer etwas Rauschen: kleine Helligkeitsschwankungen durch das Schrotrauschen des Sensors, Gradientenänderungen durch Lichtdrift, Subpixel-Zittern durch leichte Kamerabewegung. Ein Schwellenwert-Durchgang – binary() mit einem Schwellenwert, der über diesem Rauschniveau gesetzt ist – behält nur die Veränderungen, die groß genug sind, um als echte Bewegung zu zählen, und verwirft den Rest, wodurch ein binäres Bild entsteht, dessen von null verschiedene Pixel die tatsächlich veränderten Positionen sind.
Die vierte Stufe extrahiert zusammenhängende Regionen dieser binären Maske – Gruppen benachbarter, von null verschiedener Pixel, die zusammenhängende Flecken bilden. find_blobs() erledigt das in einem Aufruf und gibt eine Liste von Bewegungsregionen zurück, jede mit einem Begrenzungsrahmen und einer Pixelanzahl, auf die der Rest der Anwendung reagieren kann.
Die Einzelbild-Differenzbildung-Pipeline: ein Referenz-Einzelbild plus ein aktuelles Einzelbild werden zu einem Differenzbild; die Schwellenwertbildung verwandelt die Differenz in eine binäre Maske veränderter Positionen; ein Schritt für zusammenhängende Regionen verwandelt die Maske in eine Liste von Bewegungsregionen.¶
5.11.2. Referenzen im Speicher und auf dem Datenträger¶
Die grundlegende Pipeline hält das Referenz-Einzelbild im RAM. Das ist die richtige Antwort, wenn die Referenz in diesem Lauf des Skripts erfasst wird und nur so lange erhalten bleiben muss, wie das Skript weiterläuft.
Für eine langlaufende Anwendung – eine Kamera, die die Veränderungserkennung nach einem Aus- und Einschalten wieder aufnehmen soll, ein intermittierendes Skript, das jede Veränderung seit einem früheren Zeitpunkt erkennen muss – muss das Referenz-Einzelbild das laufende Skript überdauern. Das Muster besteht darin, die Referenz auf dem Datenträger zu speichern:
csi0.snapshot().save("/sdcard/reference.bmp")
und sie zu Beginn jedes Laufs wieder zu laden:
reference = image.Image("/sdcard/reference.bmp")
Die Differenzbildungslogik ändert sich nicht; nur der Ort, an dem die Referenz zwischen den Aufnahmen lebt. Einige Verfeinerungen erweitern diese Datenträger-Variante auf natürliche Weise – automatische Neuaufnahme der Referenz über einen Timer, optionale gleitende Mittelwerte zur Verfolgung langsamer Lichtdrift – aber die Ersetzung im Zentrum bleibt dieselbe.
5.11.3. Isolierung von Lichtquellen¶
Dasselbe Subtraktionsmuster taucht in einem etwas anderen Kontext auf: der Isolierung einer Lichtquelle vom Rest der Szene. Der Trick besteht darin, eine „Licht-aus“-Referenz aufzunehmen – ein Einzelbild, das aufgenommen wird, wenn das zu erkennende Objekt (eine IR-Bake, ein Bildschirmpixel, eine Statusanzeige) nicht beleuchtet ist – und diese Referenz von jedem nachfolgenden Einzelbild zu subtrahieren. Das Ergebnis hat überall dort eine Helligkeit von null, wo die Szene in beiden Aufnahmen gleich war, und nur dort eine von null verschiedene Helligkeit, wo die Lichtquelle tatsächlich aufleuchtete.
5.11.4. Differenz oder Subtraktion wählen¶
Ein praktischer Hinweis dazu, welche arithmetische Operation zu wählen ist. difference() gibt den absoluten Betrag der Veränderung zurück – vorzeichenfrei – was sie empfindlich für Veränderungen in beide Richtungen (Aufhellung oder Abdunklung) macht, allerdings auf Kosten dessen, dass sie der Anwendung nicht mitteilt, in welche Richtung die Veränderung ging. Für reine Bewegungserkennung ist das die richtige Antwort: alles, was sich bewegt hat, ist interessant, unabhängig davon, in welche Richtung sich die Helligkeit verschoben hat.
Für die Erkennung von Lichtquellen ist das beleuchtete Pixel immer heller als die Licht-aus-Referenz, sodass sub() (mit seiner Begrenzung bei null) die ehrlichere Wahl ist. Überall dort, wo das aktuelle Einzelbild dunkler als die Referenz ist (was Sensorrauschen um den unbeleuchteten Wert herum wäre), wird auf null begrenzt, anstatt ein falsches „das Licht war an“-Signal zu melden.