Raspberry Video Camera – Teil 15: SW Einzelbilder exportieren für die Farberkennung

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.

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:

Schreiben Sie einen Kommentar

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