Raspberry Video Camera – Teil 24: Anpassung von Programmparametern

Diesmal stelle ich keine neue Programmversion für die Raspberry Pi Video Cam vor, sondern gehe im Detail auf all die Konstanten ein, die sich inzwischen am Programmanfang angesammelt haben. Damit möchte ich all jene, die mein Python-Programm ausprobieren, ermutigen selbst Parameter zu verändern. Dadurch lassen sich Anpassungen an die eigenen Gegebenheiten vornehmen und an die Tiere, die erkannt und gefilmt werden sollen. Parameter-Tuning ist auch sinnvoll um die Erkennungsrate zu steigern um möglichst kein Tier zu verpassen.

Zuerst aber wieder ein Oachkatzl-Video. Mehr davon gibts in meinem YouTube-Kanal.

Worum es hier geht

Die Raspberry Video Camera ist eine fest installierte Außenkamera, die Tiere im Garten oder auf der Terrasse erkennen soll, um sie in Full-HD auf Video aufzuzeichnen. Auf dem Raspberry Pi laufen dazu – vereinfacht ausgedrückt – zwei Python-Programme gleichzeitig:

  • record2.py befüllt ständig einen Ringpuffer mit Videobildern und exportiert sekündlich ein einzelnes Bild zur Analyse. Im Falle einer Objekterkennung erfolgt hier die Videoaufzeichnung.
  • analyze3.py ist das Auswerteprogramm. Es liest das exportierte Einzelbild und durchsucht es nach den zu detektierenden Tieren. Dabei geht es um die Farben der Tiere. Im Falle einer Erkennung wird eine Triggerdatei geschrieben, die das record2.py Programm zur Aufzeichnung veranlasst.

Während record2.py ein recht kurzes Programm ist, hat analyze3.py durch einige Verbesserungen ziemlich an Umfang zugelegt und inzwischen zahlreiche Parameter, an denen man Änderungen vornehmen kann.

Der Parameterblock von analyze3.py

Und so sieht der Programmbereich aus, in dem diverse Vorgaben gemacht werden können:

samplePath = "samples/"             # path to sample images
hueBins = 30                        # histogram bins for hue
satBins = 30                        # histogram bins for saturation
simili = 0.5                        # similarity limit, smaller values are considered similar
                                    # greater values different

imagePath = "/tmp/"                 # path to image and timestamp files
tsExt = ".sig"                      # file extension of timestamp file
imageFile = "record.jpg"            # name of image file

alpha = 0.075                       # amount of influence of a single value to the computed average
sigma = 9.0                         # value to multipy volatility with for a higher threshold
roi = {'x1': 0,                     # region of interest: upper left corner
       'y1': 100,
       'x2': 600,                   # lower right corner
       'y2': 220}
minPix = 275                        # minimum pixel to detect
sunset = 60                         # lower brightnes switches to night
sunrise = 65                        # higher brightness switches to day
bProThresh = 50                     # back projection threshold (0-255), lower values will be ignored
histLifeTime = datetime.timedelta(minutes = 15) # histogram lifetime, recalc if timed out
objectDetTriggerTO = 10             # timeout for object detection trigger

triggerTimeout = 20                 # min. trigger duration, time extends, when triggered again
maxVideoDuration = 360              # force stop of recording if this duration exceeds
triggerFileExt = '.trg'             # file extension for trigger signal file
triggerPath = 'trigger/'            # path for trigger signal file

GPIO_PIR = 12                       # used pin for PIR motion detector

Die Parameter im Einzelnen

samplePath = "samples/"             # path to sample images

(Verwendet von initialLearn) Das ist das Unterverzeichnis, in dem die Beispielbilder für die Lernfunktion liegen. Alle Dateien darin werden gelesen und zur Ermittlung der Objektfarben herangezogen. Andere Dateien sollten hier nicht abgelegt werden.

 

hueBins = 30                        # histogram bins for hue
satBins = 30                        # histogram bins for saturation

(Verwendet von initialLearn und imageAnalyzer) Die Lernfunktion und die Farberkennung arbeiten mit 2D-Histogrammen. Bins sind dabei die Skalenteilungen des Histogramms, also die Anzahl der Werte auf der X- und der Y-Achse. Maximal könnte man für Hue 180 Bins verwenden, da Hue-Werte von 0 bis 179 gehen und für Saturation 256. Dann hätte man die maximal größte Auflösung des Histogramm, aber auch ein sehr großes Datenarray. Bei kleineren Bins-Werten nimmt die Größe des Histogramms ab, in dem benachbarte Werte zusammengefasst werden. Bei 30 hueBins fallen so jeweils 6 Huewerte in ein Bin. Die Auflösung oder die Farbgenauigkeit werden also ebenfalls geringer, was aber nicht von Nachteil sein muss, da die Objektfarben (Eichhörnchen) ja auch ein gewisses Spektrum einnehmen. Für eigene Experimente können die Werte ruhig in folgenden Bereichen verändert werden: hueBins von 8 – 180 und satBins von 8 -256. Bei hohen Werten aber auf die Programmlaufzeit achten!

 

simili = 0.5                   # similarity limit, smaller values are considered similar
                               # greater values different

(Verwendet von initialLearn) In der Lernphase werden viele Beispielbilder bzw.deren Histogramme auf einige wenige Histogramme verdichtet. Es werden also ähnliche Bilder farblich zusammengefasst. Die Ähnlichkeit von zwei Histogrammen wird dabei durch eine Zahl zwischen 0 und 1 ausgedrückt. 0 bedeutet, dass die Histogramme gleich sind, die beiden Bilder also die gleichen Farben enthalten und 1, dass die Bilder farblich total unterschiedlich sind. simili stellt nun einen Grenzwert dar, der zwischen 0 und 1 liegt und angibt, wie weit die Histogramme zusammengefasst werden sollen. Ein kleiner Wert führt zu vielen verbleibenden Histogrammen, ein großer Wert zu wenigen. Hier sollte also angepasst werden, wenn zu viele oder zu wenig Histogramme nach dem Verdichtungsprozess übrig bleiben.

 

imagePath = "/tmp/"                 # path to image and timestamp files
tsExt = ".sig"                      # file extension of timestamp file
imageFile = "record.jpg"            # name of image file

(Verwendet von imageLoader) Hier geht es um das Laden des Bildes, das vom Videoaufzeichnungsprogramm sekündlich exportiert wird. imagePath gibt das Verzeichnis an, wo die Bilder zu finden sind. Typischerweise ist das /tmp/, das Linux-seitig per fstab in eine Ram-Disk verwandelt wurde. Das ist wichtig, um die SD-Karte zu schonen und aus Geschwindigkeitsgründen. Veränderungen sollten hier mit Bedacht vorgenommen werden.

tsExt gibt die Dateierweiterung der Signaldatei an. Eine Signaldatei zeigt an, dass ein neues Bild vollständig geschrieben wurde und nun gelesen werden kann. Signaldateien sehen vom Dateinamen her so aus: 2016-08-26-08-05-00.sig und haben keinen Inhalt.

imageFile schließlich ist der Dateiname der Bilddatei.

Alle drei Parameter müssen gleich eingestellt sein, wie ihre Entsprechungen in record2.py, damit sich die beiden Programme verstehen können.

 

alpha = 0.075             # amount of influence of a single value to the computed average
sigma = 9.0               # value to multipy volatility with for a higher threshold
minPix = 275              # minimum pixel to detect

(Verwendet von detector) Hier wird es leider kompliziert und ich muss etwas ausholen. Das Ergebnis der Farbauswertung ist immer eine Anzahl von Pixeln, die den Farbwerten des Zielobjekts (Eichhörnchen) entsprechen. Also eine Zahl zwischen 0 und der Gesamtpixelanzahl des Bildes (wobei der letzte Fall natürlich nicht annähernd eintritt). Im optimalen Fall werden alle Pixel erkannt, die das Tier auf der Bildfläche einnimmt, nämlich dann, wenn auch alle Farben des Tierkörpers detektiert werden. Das ist aber selten der Fall, so dass oft nur Teile eines Tiers erkannt werden, beim Buntspecht zum Beispiel, der sehr verschiedene Farben im Gefieder hat.

Jetzt gilt es, anhand dieser Zahl zu entscheiden, ob eine Erkennung vorliegt. Das könne man im einfachsten Fall durch einen Schwellwert machen: Wert >= Schwellwert = Erkennung; Wert < Schwellwert = keine Erkennung. Das klappt aber nur, wenn im Hintergrund des Bildes keine Farben auftreten, die auch im Objekt vorhanden sind. Im Falle meiner Eichhörnchen mit einem Baum im Hintergrund ist aber genau das regelmäßig der Fall. Also muss der Schwellwert irgendwie dynamisch sein, um sich an den Hintergrund anpassen zu können, wenn sich im Tagesverlauf die Lichtverhältnisse ändern.

Erreicht wird das folgendermaßen: Zuerst wird aus der eben berechneten Pixelanzahl zusammen mit den entsprechenden Werten aus der näheren Vergangenheit ein Durchschnittswert gebildet. Genauer gesagt ein exponentieller gleitender Durchschnitt. Der hat den Vorteil, dass nur ein (1) Durchschnittswert dauerhaft gespeichert werden muss und die neue Pixelanzahl nur zu einem geringen Anteil in den Durchschnitt eingeht. Das ist wie beim Portwein: Die neue Ernte kommt in ein großes Fass, in dem sich der Portwein der vergangenen Jahre befindet. Nachdem der neue Wein mengenmäßig viel geringer ist als der vorhandene, verändert er die Qualität und den Geschmack nur gering und in Summe ist eine gleichbleibende Portwein-Qualität gewährleistet.

Der Parameter alpha ist nun das Maß zu dem die neue Pixelanzahl in den Durschnittswert eingeht. Hier zu 7,5% – der bisherige Durchschnitt hat folglich ein Gewicht von 92.5%. Je größer alpha ist, desto schneller passt sich der gleitende Durchschnitt an geänderte Situationen an und umgekehrt.

Der so gebildete Durchschnitt kann aber noch nicht direkt als Schwellwert genutzt werden. Wir brauchen einen Abstand zum gleitenden Durchschnitt, dessen Durchstoßen des aktuellen Werts das Erkennungssignal auslösen würde. Dieser Abstand wird wieder dynamisch generiert und zwar anhand der Volatilität. Die Volatilität ist die Schwankungsbreite der Pixelanzahlen um ihren gleitenden Durchschnitt über die nähere Vergangenheit betrachtet. Und weil die Volatilität noch zu gering ist, um in Addition zum Durchschnitt einen geeigneten Schwellwert zu bilden, kommt jetzt sigma ins Spiel. sigma ist der Faktor mit dem die Volatilität multipliziert wird, bevor das Produkt auf den gleitenden Durchschnitt aufgeschlagen wird. Das Ergebnis ist dann ein Schwellwert, der ständig dynamisch an die Gegebenheiten (das sind die Farben im Hintergrund, welche gleichzeitig auch potentielle Objektfarben sind) angepasst wird.

Das wäre es schon fast gewesen, wenn es nicht einen Sonderfall gäbe. Wenn im Leerlauf, also ohne Tier vor der Kamera, die Anzahl der trotzdem erkannten Pixel sehr gering ist – und das sollte der Regelfall sein – dann passiert folgendes: Sagen wir, die Pixelanzahl wäre sehr gering, also 0, 1, oder 2, im Durchschnitt also ca.1. Dann wäre die Volatilität maximal 1, weil kein Wert weiter als 1 vom Durchschnitt abweicht. Dann ergäbe die Multiplikation der Volatilität (1) mit sigma (9) den Wert 9 und wenn wir den auf den gleitenden Durchschnitt (1) aufschlagen erhalten wir einen Schwellwert von 10. 10 Pixel Schwellwert ist jedoch bei einem Bild, das mehrere 10000 Pixel beinhaltet viel zu gering. Der Schwellwert würde ständig bei der geringsten Änderung der Lichtverhältnisse überschritten. Wir müssen also einen wesentlich höheren Mindestschwellwert vorgeben, der sich in etwa an der Größe des erwarteten Tiers orientiert. Bei Eichhörnchen in 1m Entfernung von der Kamera wäre 500 ein passabler Wert, wenn der Buntspecht auch noch auslösen soll, dann vielleicht knappe 300. Das ist der minPix-Parameter und hier sollte eifrig experimentiert werden, um Fehlauslösungen zu minimieren.

 

roi = {'x1': 0,                     # region of interest: upper left corner
       'y1': 100,
       'x2': 600,                   # lower right corner
       'y2': 220}

(Verwendet von imageAnalyzer) Das ist die Region of Interest, also eine Verkleinerung des Bildausschnitts auf den Bereich, in dem das Objekt erwartet wird. Die linke obere und die rechte untere Ecke müssen hier koordinatenmäßig von Hand eingegeben werden, wobei der Ursprung des Bildes links oben liegt. Wer roi nicht verwenden möchte oder kann, der trägt hier entweder die Abmessungen des ganzen Bilds ein, oder entfernt die entsprechende Verkleinerungsanweisung aus imageAnalyzer.

 

sunset = 60                         # lower brightnes switches to night
sunrise = 65                        # higher brightness switches to day

(Verwendet von imageAnalyzer) Wenn wir keine Nachtbildkamera einsetzen, dann machen Nachtaufnahmen nur wenig Sinn. imageAnalyzer hat deshalb eine Tag-Nacht-Erkennung anhand der Bildhelligkeit. Die drückt sich aus in einem Helligkeitswert, der 0 ist bei absoluter Dunkelheit und gegen 255 gehen kann bei einem komplett weißen Bild. Letzteres ist natürlich unrealistisch, auch die Tageshelligkeit wird sich etwas oberhalb von 100 bewegen. Die Werte sind aber sehr abhängig vom jeweiligen Kamerastandort, so dass bei einem Standortwechsel nachjustiert werden sollte. Die Bedeutung der beiden Schwellwerte ist naheliegend: Steigt die Helligkeit über sunrise, so wird auf Tag geschaltet, fällt sie unter sunset, dann ist Nacht. Wichtig ist die kleine negative Überlappung der Werte, die eine Hysterese bewirkt und verhindert, dass mehrfach zwischen Tag und Nacht hin und her gewechselt wird.

Unbedingt an die eigenen Gegebenheiten anpassen! Wie man das macht? In imageAnalyzer den brightnes-Wert per print ausgeben lassen und den Sonnenuntergang beobachten!

 

bProThresh = 50         # back projection threshold (0-255), lower values will be ignored

(Verwendet von detector) Die Farberkennung funktioniert per Histogramm-Rückprojektion, das bedeutet, dass ein Farbhistogramm (das die Objektfarben enthält) auf ein aktuelles Bild zurück projiziert wird. Das Ergebnis ist dann ein Graustufenbild, dessen Pixel um so heller sind, je eher sein Farbwert den Histogrammvorgaben entspricht. Um die Pixel aber zählen zu können, brauchen wir einen Schwellwert, ab wann ein Pixel gültig sein soll. Über den Schwellwert wird aus dem Graustufenbild ein Schwazweißbild. Pixel, die dunkler als der Schwellwert sind, werden zu schwarz, hellere Pixel zu weiß. Die weißen können dann gezählt werden. Die Aktion bewirkt also, dass Pixel ausgesondert werden, die den Objektfarben nur wenig entsprechen. Das ist quasi eine Rauschunterdrückung.

 

histLifeTime = datetime.timedelta(minutes = 15) # histogram lifetime, recalc if timed out

(Verwendet von imageAnalyzer) Die Farbhistogramme für die Detektoren werden einmal am Anfang des Programms anhand der Beispielbilder erzeugt. Zur Laufzeit wird dann in bestimmten Zeitabständen ein Histogramm des aktuellen Bildhintergrunds gebildet und die Hintergrundfarben aus allen Detektor-Histogrammen herausgerechnet. histLifeTime legt den Zeitabstand für die Histogrammerneuerung fest – hier auf 15 Minuten. Die Aktualisierung der Hintergrundfarben kann man auch öfter vornehmen, aber bei jeder Neuberechnung der Detektor-Histogramme werden die bis dahin berechneten Durchschnittswerte (gleitender Durchschnitt und Volatilität, siehe oben) hinfällig und müssen zurückgesetzt werden. Das bewirkt jedes mal eine gewisse kurze Blindheit der Farberkennung.

 

objectDetTriggerTO = 10             # timeout for object detection trigger

(Verwendet von imageAnalyzer) Der Hardware-PIR-Bewegungssensor hat einen eingebauten Timeout – erst wenn der abgelaufen ist, ohne dass eine weitere Bewegung erkannt wurde, geht das Signal auf Low. Der objectDetTriggerTO bildet diesen Timeout für die Farberkennung nach. Die Farberkennung darf für 10 Sekunden aussetzen, bevor das Erkennungssignal abfällt.

 

triggerTimeout = 20           # min. trigger duration, time extends, when triggered again

(Verwendet von triggerGenerator) Auch das ist ein Trigger-Timeout, wie der eben besprochene, aber dieser wird vom triggerGenerator verwendet. Der überwacht die Trigger-Ereignisse des Hardware-Bewegungssensors und der Farberkennung und führt beide Triggersignale zu einem gemeinsamen zusammen, das über eine Triggerdatei dann den Videorecorder steuert. Und hier gibt es einen zusätzlichen Timeout, der ausläuft, wenn während 20 Sekunden keiner der beiden Trigger mehr ein Signal liefert. Der Wert ist großzügig lang gewählt, weil er auch für einen gewissen Video-Nachlauf sorgt, also für Videolaufzeit nachdem das Tier die Szene verlassen hat.

 

maxVideoDuration = 360              # force stop of recording if this duration exceeds

(Verwendet von triggerGenerator) Das ist quasi die Notabschaltung, wenn einer der Trigger (gerne mal der PIR-Bewegungssensor) ständig Signale liefern sollte. In so einem Fall würden endlos lange Videos entstehen, die den Speicherplatz auf der SD-Karte des Raspberry Pi aufbrauchen würden, gäbe es keine Zeitbegrenzung. Der Wert sollte (in Sekunden) so eingestellt werden, dass die maximale Aufenthaltszeit des Zielobjekts vor der Kamera abgedeckt wird – durchaus mit Puffer aber auch nicht viel mehr.

 

triggerFileExt = '.trg'             # file extension for trigger signal file
triggerPath = 'trigger/'            # path for trigger signal file

(Verwendet von triggerGenerator) Und das ist schließlich das Verzeichnis und die Dateierweiterung für die Triggerdatei. Die sieht in etwa so aus: 2016-08-26-08-05-00.trg und veranlasst durch ihr Auftauchen den Start der Videoaufzeichnung. Der Zeitstempel im Namen der Triggerdatei bestimmt dabei, wie weit  in die Vergangenheit Videomaterial aus dem Ringpuffer entnommen werden soll. Die Datei selbst hat keinen Inhalt und die Einträge hier müssen natürlich mit denen in record2.py übereinstimmen.

 

GPIO_PIR = 12                       # used pin for PIR motion detector

(Verwendet von pir) Das ist der GPIO-Pin an dem der PIR-Bewegungssensor am Raspberry Pi angeschlossen ist. Wenn ein anderer Pin verwendet wird, kann das hier angepasst werden.

Eigene Experimente

Ich möchte anregen, nicht nur die Programme blind zu übernehmen, sondern auf jeden Fall eigene Anpassungen vor zu nehmen. Der erste Schritt dazu sind Experimente mit den hier vorgestellten Parametern. Jede Beleuchtungssituation ist anders, jedes Zielobjekt und jeder Bildhintergrund, so dass sich Anpassungen auszahlen. Dann können im zweiten Schritt auch individuelle Programmierungen dazu kommen.

 


Weitere Artikel in dieser Kategorie:

Schreiben Sie einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.