Raspberry Video Camera – Teil 11: SW Python für die Kamera

Nachdem Raspbian Lite auf dem Pi installiert ist, geht es nun an die Programmierung. Raspberry-typisch erfolgt die in Python, eine Sprache, die ich mir auch erst mühsam anlesen musste. Aber inzwischen – ohne behaupten zu wollen, in die Tiefe vorgedrungen zu sein – muss ich sagen, dass mich die Schönheit und die Mächtigkeit von Python begeistert. Ich schicke voraus, dass ich versuchen werde Python3-Syntax zu verwenden. Python 2.7 tue ich mir nicht mehr an und teste die Programme ausschließlich unter Python 3. Was aber niemand abhalten soll, meinen Code auf Python 2.7 umzuschreiben. Angefangen wird heute mit der Programmierung der Raspberry Pi Kamera – unter Verwendung des Picamera-Python-Moduls.


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

Picamera

Picamera ist das Python-Modul für die Raspberry Pi Kamera, also ein Interface für Pythonprogrammierer um die Kamera anzusteuern. Es bedient explizit das Raspberry Pi Camera Module und keine anderen Kameras, wie zum Beispiel USB- oder Web-Cams. Wer seine Raspberry Cam per Python ansteuern möchte, dem empfehle ich die Dokumentation zu Picamera – dort finden sich zahlreiche Programmierbeispiele, die sich leicht anpassen lassen. Auch mein Programm hat dort starke Wurzeln. Aktuell ist im März/April 2017 die Picamera Version 1.13 und genau diese (oder eine neuere) wird für dieses Projekt auch benötigt. 1.12 hat einen Fehler in der copy_to Methode und wird nicht zufriedenstellend funktionieren.

Trennung von Auslösung und Aufzeichnung

Ich greife noch einmal zurück auf ein grundlegendes Konzept, das diesem Projekt zugrunde liegt und das ich in einem früheren Artikel bereits beschrieben habe. Die programmtechnische Trennung von Auslösung (Erkennung eines Eichhörnchens) und Aufzeichnung (Filmen des Eichhörnchens). Warum ist das wichtig? Ganz einfach: In diesem Projekt soll es die Möglichkeit geben, dass eine Kamera es übernimmt, das Objekt oder eine Bewegung zu detektieren. Mehrere Kameras sollen dann die Szene gleichzeitig, aber aus unterschiedlichen Blickwinkeln aufzeichnen. Eine zweite Kamera hat natürlich ihren eigenen Raspberry Pi und damit läuft Software auch auf zwei (oder mehreren) verschiedenen Rechnern. Damit ist klar, dass nicht alles (Auslösung und Aufzeichnung) in einem einzigen Programm abgehandelt werden kann – es muss eine definierte Schnittstelle geschaffen werden, auf die mehrere Kameras zugreifen können. Wenn nur eine Kamera zum Einsatz kommen soll, dann ist dieses Konzept auch nicht von Schaden, denn es sorgt für Klarheit, wenn deutlich zwischen der Auslösung (Trigger) und der Aufzeichnung (Video) unterschieden wird.

Wie sieht diese Schnittstelle aus? Einfachheit geht vor Schnelligkeit, deshalb verzichte ich auf Konstrukte wie Inter-Prozess-Kommunikation oder TCP/IP-Sockets und wähle die einfachst mögliche Lösung: eine Datei. Die hat nicht mal einen Inhalt, deshalb muss sie auch nicht eingelesen werden. Die gesamte Information eines Triggers befindet sich im Dateinamen. Dabei repräsentiert der Dateiname der Trigger-Datei den Triggerzeitpunkt, also den Moment, an dem ein Objekt (also das Eichhörnchen) erkannt wurde. Das Format ist folgendes:
YYYY-mm-dd-HH-MM-SS.trg    also zum Beispiel:
2016-08-26-08-05-00.trg
Um 08:05 Uhr und 0 Sekunden erfolgte also am 26.08.2016 eine Auslösung und nicht – und das ist wichtig – zu dem Zeitpunkt, an dem die Datei erzeugt oder gelesen wird. Wobei alles natürlich zeitlich eng bei einander liegen wird, wenn auch nicht gleichzeitig. Das Ende des Triggers und damit auch das Ende einer Videoaufzeichnung ist aber tatsächlich durch das Löschen der Triggerdatei gekennzeichnet. Beim Abschalten der Aufnahme geht es nicht so genau.

Ringpuffer

Ein zweites Konzept, das ich in besagtem Artikel ebenfalls bereits angerissen hatte, ist der Ringpuffer. Der Umweg des Auslösers über das Speichern und Lesen einer Datei kostet Zeit, umso mehr, wenn die Triggerdatei gar auf einem anderen Rechner liegt. Derweil ist das Eichhörnchen vielleicht schon ein Stück weiter gelaufen, ohne dass die Kamera aufzeichnet. Und eigentlich soll die Aufzeichnung ja auch nicht erst damit beginnen, dass ein Eichhörnchen mitten im Bild am Futterschälchen sitzt, sondern besser bereits vorher, noch bevor es ins Bild hinein läuft. Eine Vorgehensweise à la „erst Erkennen und dann schnell Video starten“ wird nur mäßige Ergebnisse bringen. Wenn also vorher nicht bekannt ist, wann das Eichhörnchen kommen wird (durch weit vorgelagerte Sensoren zum Beispiel), dann muss ständig Videomaterial vorgehalten werden, auf das im Bedarfsfall zurückgegriffen werden kann.

Die Lösung dafür ist ein so genannter Ringpuffer. Das ist ein Stück Speicher (Memory nicht Disk), das einige Sekunden Videoaufzeichnung speichern kann und zwar so, dass ein neu aufgenommenes Bild das jeweils älteste im Puffer überschreibt. Der Speicher wird quasi im Kreis befüllt und das ständig – 24 Stunden pro Tag. So stehen im Puffer immer – je nach Größe – die letzten x Sekunden zur Verfügung. Bei Eintritt eines Triggerereignisses kann dann darauf zugegriffen werden.

Rechnerisch besteht die Größe des Ringpuffers aus zwei Teilen: Einmal dem gewünschten Vorlauf. Das ist die Zeit, die generell im Video vor dem Triggerzeitpunkt sichtbar sein soll. Also die Zeit, während der das Eichhörnchen aus dem Baum springt und zum Futterschälchen läuft, wo es dann vom Bewegungssensor erfasst wird. Die zweite Komponente ist ein Zeitpolster, das es ermöglicht, dass wir uns bei der Programmverarbeitung etwas Zeit lassen können. Zeit, während der eine zweite Kamera per WLAN auf die erste, die Triggerkamera, zugreift um nach einer Triggerdatei zu suchen. Oder Zeit, die es uns ermöglicht bei einer Objekterkennung durch Bildauswertung nur ein Bild pro Sekunde auszuwerten und nicht jedes. Und schließlich die Zeit, die uns den Luxus ermöglicht, überhaupt den zeitaufwendigen Umweg des Auslösetriggers über eine Datei zu gehen. Der Ringpuffer nimmt uns also die Not, schnell sein zu müssen, dadurch, dass wir uns jederzeit ein Stück Vergangenheit aus dem Ringpuffer holen können. Die Größe des Ringpuffers addiert sich also aus der gewünschten Vorlaufzeit plus Zeitpolster für die Verarbeitung. In nachfolgendem Programm sind das 10s + 10s = 20s. Den Ringpuffer müssen wir zum Glück nicht selber programmieren, Picamera stellt diese Funktion zur Verfügung.

Dateisystem

Bevor es um das Programm an sich geht, schauen wir uns kurz das Umfeld an. Die Python-Programme brauchen keine Rootrechte und laufen deshalb unter dem voreingestellten User Pi. Die Programme liegen dabei im Homeverzeichnis von Pi, also /home/pi. Dazu brauchen wir zwei Unterverzeichnisse, die im Homeverzeichnis angelegt werden müssen:

  • trigger: als Verzeichnis für die Triggerdateien (nur wenn auf diesem Rechner ein Trigger erzeugt wird) und
  • Videos: zur Ablage der Videodateien

„Videos“ ist nur deshalb groß geschrieben, weil in der Raspbian Vollversion mit grafischer Oberfläche im Homeverzeichnis bereits vom Betriebssystem her.ein Verzeichnis Videos existiert.

mkdir trigger
mkdir Videos

Python Programm für die Kamera (record.py)

Dann schauen wir uns das Programm an:

import os
from datetime import datetime
import io
import picamera

secs_before = 10
trigger_fileextension = '.trg'
trigger_path = 'trigger/'
video_path = 'Videos/'

# Format of trigger file name:          2016-08-26-08-05-00.trg


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)
# Look for trigger file
      triggerfiles = [f for f in os.listdir(trigger_path) if f.endswith(trigger_fileextension)]
      if triggerfiles:
        print('Trigger detected!')
# Motion trigger file detected, trim file extension ".trg"
        triggerfile = triggerfiles[0].split('.')[0:1][0]
        print(triggerfile)
# 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 os.path.isfile(trigger_path + triggerfiles[0]):
          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)
        print('Files connected')
  finally:
    camera.stop_recording()

In den ersten vier Zeilen werden die benötigten Module importiert, darunter auch Picamera zur Ansteuerung der Raspberry Pi Kamera. Die nächsten vier Zeilen setzen Variablen für Verzeichnisse, die Fileextension der Triggerdatei und mit secs_before den gewünschten Videovorlauf (also die Aufnahmezeit vor dem Triggerereignis). In diesem Fall 10 Sekunden.

Im eigentlichen Programmblock wird zuerst die Kamera (camera) definiert und dann Auflösung und Framerate spezifiziert. Die beiden auskommentierten Parameter hflip und vflip kann man in der Experimentalphase verwenden, wenn die Kamera noch nicht eingebaut ist und nur kopfüber am Kabel hängt.

Als nächstes wird mit stream der Ringpuffer angelegt. Der seconds Parameter gibt dabei seine Größe an. In diesem Fall ist das der vorher festgelegte Vorlauf und ein zusätzlicher Pufferbereich von weiteren 10s. Mit start_recording beginnt jetzt die Videoaufzeichnung im Ringpuffer.

In der nachfolgenden Whileschleife wird erst einmal eine Sekunde aufgenommen und weiter nichts gemacht. Dann erfolgt die Überprüfung auf einen möglichen Trigger. Dazu werden aus dem Verzeichnis trigger alle Dateinamen mit der Endung .trg geholt. Falls keine Triggerdatei vorhanden ist, läuft die Schleife leer durch und beginnt erneut mit der einsekündigen Wartezeit.

Im Falle einer erkannten Triggerdatei wird vom Dateinamen das .trg abgespalten und der vordere Teil in ein datetime Objekt gewandelt, mit dem sich rechnen lässt. Denn nun muss berechnet werden, wie viele Sekunden Video aus dem Ringpuffer entnommen werden müssen, damit der gewünschte Vorlauf gewährleistet ist. In diese Rechnung fließt auch die Zeit ein, die seit dem Triggerereignis bereits verstrichen ist. Diese beforetime wird mit print ausgegeben.

Jetzt wird mit split_recording die laufende Videoaufzeichnung vom Ringpuffer in eine Datei umgeschaltet. Die wird im Verzeichnis Videos angelegt und bekommt als Dateinamen ein vorangestelltes a-, dann den Namen der Triggerdatei mit der Dateierweiterung .h264. Das a steht für after, denn das ist der Videoteil, der nach der Umschaltung erzeugt wird.

Analog dazu wird eine Videodatei mit einem vorangestellten b- für before geschrieben. Das ist der Videoteil, der mit copy_to aus dem Ringpuffer stream entnommen wird.

Jetzt muss nur noch gewartet werden bis der Trigger weggenommen wird. Dazu wird in einer weiteren Whileschleife überprüft, ob die Triggerdatei noch vorhanden ist. Ist das der Fall wird einfach eine weitere Sekunde lang aufgezeichnet. Ist die Triggerdatei jedoch gelöscht, passiert folgendes: Zuerst wird der Videodatenstrom, der von der Kamera kommt per split_recording wieder in den Ringpuffer geleitet und das Schreiben der After-Datei damit beendet. Und danach startet die Nachbearbeitung (postprocessing) in einem eigenen Prozess. Der Vorgang beginnt nun von Neuem, es wird laufend in den Ringpuffer gespeichert und gewartet, bis eine Triggerdatei gefunden wird.

Python Programm zur Video Nachbearbeitung (postprocess.py)

Kommen wir zum Postprocessing. Drei Dinge bleiben nun noch zu tun:

  1. Die beiden Videodateien b und a müssen zusammenkopiert werden,
  2. das Videoformat sollte von H264 auf MP4 umgestellt werden und
  3. die beiden ursprünglichen a- und b-Dateien können gelöscht werden.

Genau das macht postprocess.py. Das Programm bekommt als Parameter den Pfad zum Video-Verzeichnis übergeben und den Rumpf-Dateinamen ohne voranstehende a oder b und ohne Dateierweiterung. Damit erzeugt das Programm dann mit Hilfe des Tools MP4Box (muss vorher installiert sein) aus den beiden H264-Dateien eine zusammenhängende MP4-Datei. Der Dateiname ist wieder der Trigger-Timestamp, diesmal aber ergänzt durch den Hostnamen des Rechners. Das ist eine kleine Vorleistung in Hinblick darauf, dass es einmal mehrere Kameras geben könnte und die Dateinamen der Videos unterscheidbar, aber vor allem nicht exakt gleichlautend sein  sollen.

Die beiden letzten Programmzeilen löschen dann nur noch die ursprünglichen a- und B-Dateien.

import os
import sys

path = sys.argv[1]
name = sys.argv[2]
hostname = os.uname()[1]


os.system('MP4Box -fps 25 -cat '+path+'b-'+ name +'.h264 -cat '+path+'a-'+ name +
          '.h264 -new '+path+name+'-'+hostname+'.mp4 -tmp ~ -quiet')
os.remove(path+'b-'+ name +'.h264')
os.remove(path+'a-'+ name +'.h264')

Warum wird das Postprocessing als eigenes Programm angelegt und die paar Zeilen nicht in das Hauptprogramm integriert?

Das hat Lastgründe. Das Zusammenbauen und Umformatieren der Videodateien mit MP4Box erzeugt hohe CPU- und IO-Last. Gegen letzteres lässt sich wenig machen, aber da der Raspberry Pi 3 vier Prozessorkerne hat, macht es durchaus Sinn diese Nacharbeiten von dem CPU-Kern fernzuhalten, der die Videoaufzeichnung in den Ringpuffer vornimmt. Oder – um es anders auszudrücken – das Hauptprogramm sollte sich ausschließlich um die Videoaufzeichnung kümmern und dabei möglichst wenig Drops einstecken müssen. Deshalb sollte – so weit möglch – jede andere Tätigkeit in einen Parallelprozess ausgelagert werden. Das betrifft das Postprocessing, aber auch die Objekterkennung.

Timing

Hier das Zeitverhalten nochmal als Grafik veranschaulicht:

Dem Triggerzeitpunkt folgen kurz darauf (aber dennoch verzögert) zwei Ereignisse:

  1. das Schreiben der Triggerdatei durch das Programm, das das Objekt erkennt und
  2. das Lesen der Triggerdatei (bzw des Dateinamens) durch das Kameraprogramm record.py.

Das Videomaterial für die dann bereits verflossene Zeit vom Triggerzeitpunkt bis zum Lesen der Triggerdatei wird dem Ringpuffer entnommen – ebenso wie der gewünschte Vorlauf.

Testen

Die beiden oben vorgestellten Programme sind erst die halbe Miete, zu einem funktionierenden Gesamtsystem fehlt noch der Part der Triggererzeugung. Trotzdem lässt sich der Videoteil bereits eigenständig testen:

Wir öffnen zwei SSH-Verbindungen zu unserem Raspberry Pi. In Windows per Putty, in Linux einfach in zwei Terminalfenstern. Nach dem Login starten wir Im ersten Fenster das Programm mit:

$ python3 record.py
Ready for trigger

Im zweiten Fenster gehen wir in des Triggerverzeichnis, erstellen eine Triggerdatei, warten ein paar Sekunden und löschen die Datei dann wieder:

$ cd trigger
$ touch 2017-03-27-10-44-45.trg
$ rm *

Im ersten Fenster passiert gleichzeitig folgendes:

Trigger detected!
2017-03-27-10-44-45
239.726183
Trigger stopped!
Connect files
Files connected
Appending file Videos/b-2017-03-27-10-44-45.h264
No suitable destination track found - creating new one (type vide)
Appending file Videos/a-2017-03-27-10-44-45.h264
Saving Videos/2017-03-27-10-44-45-raspi166.mp4: 0.500 secs Interleaving

Die letzten 4 Zeilen sind dabei die Ausgaben von MP4Box. Hier sieht man, wie erst die b- und dann die a-Datei eingelesen und dann die fertige MP4-Datei geschrieben wird. Die Zeilen darüber sind die Ausgaben unseres record.py Programms. Bemerkenswert sind zwei Dinge:

  1. Die beforetime, die in der dritten Zeile ausgegeben wird, ist hier wesentlich größer als der Ringpufferinhalt, was daran liegt, dass ich einen Triggerzeitpunkt eingegeben habe, der entsprechend weit in der Vergangenheit liegt.
  2. Der Ausdruck Files connected kommt sofort nach Connect files und nicht erst nachdem MP4Box fertig ist. Das ist so gewollt und bestätigt, dass das Hauptprogramm zwar die Nachbeareitung als eigenen Prozess ansteuert, sich aber dann nicht weiter darum kümmert und mit der eigenen Arbeit fortfährt – also den Ringpuffer befüllt. MP4Box läuft parallel dazu als eigener Prozess

Im Verzeichnis Videos finden wir nun das fertige MP4-Video mit dem Dateinamen 2017-03-27-10-44-45-raspi166.mp4, für den Fall dass der Rechner den Namen raspi166 trägt. Die Länge des Videos sollte der Zeit zwischen Triggerzeitpunkt und Löschung der Triggerdatei entsprechen mit einem zusätzlichen zeitlichen Vorlauf von 10 Sekunden. Natürlich nur so weit die Ringpuffergröße das hergibt.

 


Weitere Artikel in dieser Kategorie:

Schreiben Sie einen Kommentar

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