Die bisher entwickelte Version der Oachkatzl-Cam verwendet einen Bewegungssensor als Auslöser für die Videoaufzeichnung. Das geht aber auch anders, wie ich in Teil 4 dieser Artikelserie bereits beschrieben hatte. Als nächstes realisiere ich eine Objekterkennung anhand der Farbe der Eichhörnchen. Das ist ein recht umfangreiches Thema und ein größeres Python-Programm. Da braucht es mehrere Artikel um dem gerecht zu werden. Im letzten Artikel hatten wir bereits OpenCV auf dem Raspberry Pi installiert, damit steht das programmiertechnische Rüstzeug bereits zur Verfügung. Heute zeige ich, wie wir aus dem Videodatenstrom einzelne Bilder exportieren können, die dann im nächsten Artikel nach den typischen Eichhörnchenfarben untersucht werden.
Zuerst aber wieder ein Oachkatzl-Video. Mehr davon gibts in meinem YouTube-Kanal.
Zur Wahrung deiner Privatsphäre wird erst eine Verbindung zu YouTube hergestelt, wenn du den Abspielbutton betätigst.
Konzept
Wer nach Bildauswertung mit OpenCV googelt, der findet üblicherweise Konzepte, die realtime ein Bild aus dem Kameradatenstrom entnehmen und zum Beispiel mit dem vorhergehenden Bild vergleichen. Das kann man so machen, aber man muss sich im Klaren sein, dass pro Sekunde 25 (oder 30) Einzelbilder daher kommen. Das Auswerteprogramm muss also schon flott arbeiten.
Wer meine Artikelserie verfolgt hat, der weiß, dass ich ein anderes Konzept verfolge. Zum einen möchte ich die Videoaufzeichnung von der Triggererzeugung – so weit das möglich ist – trennen und zum zweiten habe ich einen Ringpuffer eingerichtet, der Videobilder über viele Sekunden zurück in die Vergangenheit bereit hält. Dadurch kann ich es mir leisten, nur einmal pro Sekunde ein Bild auszuwerten, ohne den Auftritt eines Eichhörnchens zu verpassen.
Etwas Aufwand verursacht aber die Trennung von Aufzeichnung und Bildauswertung, denn irgendwie muss das Bild aus dem Videoprogramm ins Auswerteprogramm kommen. Und zwar so, dass beide Programme möglichst auf verschiedenen Prozessorkernen laufen, damit der Empfang von Videodaten vom Kamerachip möglichst ungestört abläuft.
Und so funktionierts: Wie bisher füllt das Aufzeichnungsprogramm beständig den Ringpuffer, während es auf ein Triggersignal wartet. Dazu kommt jetzt, dass einmal pro Sekunde ein Bild aus dem Videodatenstrom kopiert wird, um es als einzelne Bilddatei zu speichern. Das bisherige Programm, das Signale des Bewegungssensors auswertet, entfällt und wird durch ein neues Analyseprogramm ersetzt. Dieses Analyseprogramm hat grob drei Funktionen: Es liest ständig die sekündlich aktualisierte Bilddatei, analysiert den Bildinhalt auf ein mögliches Auftreten eines Eichhörnchens hin und schreibt in diesem Fall eine Triggerdatei für das Videoprogramm, das dann seinerseits ein Video aufzeichnet.
Es dreht sich also tatsächlich ein wenig im Kreis: Videoprogramm gibt Bilddatei an Analyseprogramm und Analyseprogramm gibt Triggerdatei ans Videoprogramm zurück. Der Umweg, das Bild über das Dateisystem zu leiten, erlaubt zwar eine komplette Trennung der beiden Programme und erspart Prozess-zu-Prozess-Kommunikation, aber es kostet auch Zeit. Die können wir minimieren, wenn wir die Bilddatei nicht wirklich jede Sekunde auf die SD-Karte schreiben, sondern dafür eine RAM-Disk einrichten und die Bilddatei so quasi im Speicher halten.
So weit eine grobe Beschreibung der Programmabläufe. In diesem Artikel modifizieren wir das Videoprogramm, das wir ja bereits kennen, so, dass es zusätzlich jede Sekunde eine Bilddatei exportiert.
Python Programm für die Kamera (record2.py)
import os
from datetime import datetime
import io
import picamera
secs_before = 10
video_path = 'Videos/'
trigger_fileextension = '.trg'
trigger_path = 'trigger/'
triggerfile = ''
triggered = False
image_path = '/tmp/'
image_file = 'record.jpg'
signal_fileextension = '.sig'
# Format of trigger file name: 2016-08-26-08-05-00.trg
# Format of signal timestamp file name: 2016-08-26-08-05-00.sig
# Look for trigger file and export still images periodically
def detect_trigger(camera):
global triggerfile, triggered
# Export a still image and a corresponding signal timestamp file
# First delete previous signal timestamp file(s)
signalFiles = [f for f in os.listdir(image_path) if f.endswith(signal_fileextension)]
for f in signalFiles:
os.remove(image_path+f)
camera.capture(image_path + image_file, use_video_port=True, resize=(640, 360))
timeStamp = datetime.now().strftime("%Y-%m-%d-%H-%M-%S")
open(image_path + timeStamp + signal_fileextension, 'w').close()
# Detect trigger file
if not triggered:
# Not triggered before - look for trigger file
triggerfiles = [f for f in os.listdir(trigger_path) if f.endswith(trigger_fileextension)]
if triggerfiles:
# Trigger file detected, trim file extension
triggerfile = triggerfiles[0].split('.')[0:1][0]
triggered = True
print('Trigger detected!')
print(triggerfile)
return True
else:
return False
else:
# Trigger already active - check whether trigger file ist still present
if os.path.isfile(trigger_path + triggerfile + trigger_fileextension):
return True
else:
triggered = False
return False
with picamera.PiCamera() as camera:
camera.resolution = (1920, 1080)
camera.framerate = 25
# camera.hflip = True
# camera.vflip = True
stream = picamera.PiCameraCircularIO(camera, seconds=(secs_before+10))
camera.start_recording(stream, format='h264')
print('Ready for trigger')
try:
while True:
camera.wait_recording(1)
if detect_trigger(camera):
# Convert filename to datetime object
triggertime = datetime.strptime(triggerfile, '%Y-%m-%d-%H-%M-%S')
# Calc seconds to fetch from ringbuffer
currenttime = datetime.now()
if triggertime > currenttime:
triggertime = currenttime
beforetime = (currenttime - triggertime).total_seconds() + secs_before
print(beforetime)
# As soon as we detect trigger, split the recording to record the frames "after" trigger
camera.split_recording(video_path + 'a-' + triggerfile + '.h264')
# Write the seconds "before" trigger to disk as well
stream.copy_to((video_path + 'b-' + triggerfile + '.h264'), seconds=beforetime)
stream.clear()
# Wait for trigger to disappear, then split recording back to the in-memory circular buffer
while detect_trigger(camera):
camera.wait_recording(1)
print('Trigger stopped!')
camera.split_recording(stream)
# Start postprocessing
print('Connect files')
postprocess = 'python3 postprocess.py '+video_path+' '+triggerfile+' &'
os.system(postprocess)
finally:
camera.stop_recording()
Werfen wir zuerst einen Blick auf den zweiten Teil, also ab der Zeile with picamera.PiCamera() as camera
. Das Hauptprogramm entspricht fast vollständig der vorhergehenden Version, lediglich das Aufprüfen auf die Triggerdatei wird in eine eigene Funktion ausgelagert. Die hat den Namen detect_trigger
und bekommt das camera
-Objekt als Parameter übergeben. Die Rückgabe ist ein Wahrheitswert und besagt, ob der Auslösetrigger aktiv ist oder nicht.
Die Funktion detect_trigger
steht oberhalb des Hauptprogramms und die sehen wir uns nun etwas genauer an: Sie hat grundsätzlich zwei Aufgaben. Einmal die Überwachung einer möglichen Triggerdatei – das ist das, was früher vom Hauptprogramm selbst erledigt wurde, und (neu dazugekommen) den sekündlichen Export einer Bilddatei. Für die Bilddatei werden außerhalb der Funktion bereits drei globale Variablen angelegt, die Pfad und Dateinamen der Bilddatei angeben und eine Signal-Dateierweiterung. Die Signaldatei soll durch ihr Entstehen einem anderen Programm, welches die Bilddatei lesen will, anzeigen, dass die Bilddatei jetzt komplett geschrieben ist. Damit soll verhindert werden, dass ein anderes Programm versucht die Bilddatei zu einem Zeitpunkt zu lesen, an dem sie noch nicht ganz geschrieben ist, oder bereits wieder gelöscht, bzw durch das Nachfolgebild überschrieben wird. Die Signaldatei liegt im selben Verzeichnis wie die Bilddatei, also in /tmp/
, trägt als Namen den Datum-Zeit-Stempel, den wir bereits von der Triggerdatei her kennen und als Dateierweiterung .sig
. Die Bilddatei heißt immer record.jpg
und sie soll von einem auswertenden Programm immer genau dann gelesen werden, sobald eine neue Signaldatei geschrieben wurde.
Wie läuft das Erzeugen der beiden Dateien nun im Programm ab? Zuerst werden alle möglicherweise existierenden Signaldateien (.sig
) gelöscht. Das zeigt anderen Programmen an, dass die Bilddatei zwar noch existiert, aber nicht mehr als gültig betrachtet werden darf und folglich nicht gelesen werden soll. Die capture
-Methode des camera
-Objekts holt nun ein einzelnes Bild vom Video-Port ab, verkleinert es auf 640×360 Pixel und speichert es als /tmp/record.jpg
ab:
camera.capture(image_path + image_file, use_video_port=True, resize=(640, 360))
Das Einzelbild muss für die Auswertung nicht Full-HD-Auflösung haben, deshalb verkleinern wir es gleich ein wenig. Dadurch ist es schneller geschrieben und auch wieder eingelesen. Danach wird ein aktueller Timestamp erzeugt und damit eine neue Signaldatei geschrieben. Die Signaldatei hat keinen Inhalt, durch ihre bloße Existenz zeigt sie einem lesenden Programm an, dass die Bilddatei nun gültig ist und gelesen werden darf.
Der zweite Teil der Funktion kümmert sich um das Lesen einer möglichen Triggerdatei (Datei, die die Videoaufzeichnung auslöst) und ist nichts gravierend Neues. Allerdings muss die Funktion beide Ereignisse steuern, den Beginn der Videoaufzeichnung (wenn ein Triggerfile auftaucht) und das Beenden des Videofilms (wenn die Triggerdatei verschwindet). Deshalb muss der aktuelle Status auch zwischen zwei Funktionsaufrufen gehalten werden. Das passiert in zwei Variablen, die global angelegt wurden und am Anfang der Funktion mit global triggerfile, triggered
der Funktion auch schreibend zur Verfügung gestellt werden. Die verschachtelten if-else
-Statements machen nichts anderes als nachzusehen, ob im Ruhemodus ein neuesTriggerfile auftaucht, bzw. ob im Aufnahmemodus das Triggerfile wieder verschwindet. In Abhängigkeit davon wird True
an das Hauptprogramm zurückgegeben, wenn (weiterhin) Video aufgezeichnet werden soll, oder False
, wenn nicht.
Und wo wird nun festgelegt, dass nur ein Bild pro Sekunde abgespeichert wird? Das passiert durch die Wartezeiten an zwei Stellen im Hauptprogramm per camera.wait_recording(1)
. Die Eins in der Klammer lässt das Hauptprogramm quasi eine Sekunde lang schlummern, während aber weiterhin Video aufgezeichnet wird (entweder in eine Video-Datei oder in den Ringpuffer). Wer genau hinschaut, wird erkennen, dass die Zeitspanne zwischen zwei exportierten Bilddateien etwas größer sein muss als genau eine Sekunde. Zu der einen Sekunde Schlafzeit kommt ja noch die Zeit für die Programmausführung dazu. Das spielt aber keine große Rolle, das Programm, das die Bilddateien lesen soll, wird sich daran anpassen.
Tmp-Verzeichnis in eine Ram-Disk legen
Jede Sekunde eine Bilddatei auf die SD-Karte zu speichern und wieder zu lesen, das ist zeitaufwändig und tut auch der SD-Karte nicht gut. Schneller und besser ist es, wenn die Bilddaten im Speicher gehalten werden können. Sehr einfach geht das in Linux über eine Ram-Disk, also ein Laufwerk, das im flüchtigen Speicher emuliert wird. Die beteiligten Python-Programme müssen dafür nicht geändert werden, Linux erledigt alles. Ich mache es mir hier einfach und lege das komplette /tmp
-Verzeichnis als Ram-Disk an. Das ist unter Linux gar nicht unüblich, die Inhalte dieses Verzeichnisses werden nach einem Reboot eh nicht mehr benötigt.
Wir editieren dazu die Datei /etc/fstab
mit:
sudo nano /etc/fstab
und ergänzen folgende Zeile:
tmpfs /tmp tmpfs defaults,mode=1777 0 0
Dann starten wir den Raspberry Pi neu und können dann zum Beispiel mit df überprüfen, dass /tmp ein tmpfs (temporary file system) ist:
$ df
...
tmpfs 311468 208 311260 1% /tmp
...
Eins gilt es aber noch zu beachten, das /tmp
-Verzeichnis wird auch von anderen Programmen benutzt. Im Falle unserer Raspberry Video Cam ist das vor allem MP4Box, also der Video-Konverter, den wir aus postprocess.py
heraus aufrufen. MP4Box verwendet das /tmp
-Verzeichnis für temporäre Dateien während dem Einlesen und Umwandeln unserer Videodateien. Die können sehr groß werden und damit auch die Temp-Dateien. Und zwar so groß, dass der 1GB große Ram-Speicher des Raspberry Pi dafür nicht ausreicht und MP4Box abstürzt. Wir müssen MP4Box also sagen, dass es nicht nach /tmp
schreiben, sondern seine Temp-Dateien auf der SD-Karte speichern soll. Beim Anlegen des postprocess.py
-Programms habe ich dem bereits Rechnung getragen und in den Programmaufruf von MP4Box folgenden Parameter eingefügt: -tmp ~
. Das bewirkt, dass Temp-Dateien ins jeweilige Homeverzeichnis (~) geschrieben werden.
Wie gehts weiter?
Wir haben nun das frühere Pythonprogramm record.py
dahingehend erweitert, dass das neue record2.py
sekündlich eine Bilddatei aus dem laufenden Videodatenstrom in eine Ram-Disk kopiert. Im nächsten Artikel wird das bisherige Triggerprogramm motioninterrupt.py
in Rente geschickt und durch ein neues Programm analyze.py
ersetzt, das nicht mehr den Bewegungssensor auswertet, sondern die sekündlich bereitgestellten Bilddateien. Und in den Bilddateien versucht dieses Analyseprogramm die Farben eines Eichhörnchens zu erkennen.
Weitere Artikel in dieser Kategorie:
- Raspberry Video Camera – Teil 1: Oachkatzl-Cam
- Raspberry Video Camera – Teil 2: Komponenten & Konzepte
- Raspberry Video Camera – Teil 3: Raspberry Pi Kamera Modul V2.1
- Raspberry Video Camera – Teil 4: Aufnahmeauslöser
- Raspberry Video Camera – Teil 5: Passiver Infrarot Bewegungssensor
- Raspberry Video Camera – Teil 6: Stromversorgung
- Raspberry Video Camera – Teil 7: Spannungsregler 5V
- Raspberry Video Camera – Teil 8: Montage Modell 850
- Raspberry Video Camera – Teil 9: Montage Kamera Modul
- Raspberry Video Camera – Teil 10: SW Installation Betriebssystem und Module
- Raspberry Video Camera – Teil 11: SW Python für die Kamera
- Raspberry Video Camera – Teil 12: SW Trigger per Bewegungssensor
- Raspberry Video Camera – Teil 13: SW Autostart und Überwachung
- Raspberry Video Camera – Teil 14: SW Installation Computer Vision (OpenCV 3.2)
- Raspberry Video Camera – Teil 16: SW Trigger per Farberkennung
- Raspberry Video Camera – Teil 17: Exkurs – Wie Computer Farben sehen
- Raspberry Video Camera – Teil 18: SW Farbkalibrierung
- Raspberry Video Camera – Teil 19: SW Kombinationstrigger
- Raspberry Video Camera – Teil 20: Exkurs – Farbdarstellung per 2D-Histogramm
- Raspberry Video Camera – Teil 21: Konzept einer selbstlernenden Farberkennung
- Raspberry Video Camera – Teil 22: SW selbstlernende Farberkennung in Python
- Raspberry Video Camera – Teil 23: Verbesserung durch ROI und Aufnahmezeitbegrenzung
- Raspberry Video Camera – Teil 24: Anpassung von Programmparametern
- Raspberry Video Camera – Teil 25: Zweite Kamera
- Raspberry Video Camera – Teil 26: Optimierungen gegen Frame Drops
- Raspberry Video Camera – Teil 27: Was kostet der Spaß?
- Raspberry Video Camera – Teil 28: Kamera Modell 200 mit Raspberry Pi Zero
- Raspberry Video Camera – Teil 29: Stromversorgung für Kamera Modell 200
- Raspberry Video Camera – Teil 30: Software für Kamera Modell 200