Raspberry Video Camera – Teil 26: Optimierungen gegen Frame Drops

Wer kennt das? Die Raspberry Video Camera zeichnet ein Full-HD Video auf, aber wenn man es sich danach anschaut, gibt es gelegentlich einen Sprung im Filmablauf, als ob kurze Sequenzen fehlen würden. Kurze Sequenzen, das können einzelne Frames sein, die gar nicht auffallen oder auch einmal Aussetzer über mehrere Sekunden. Frame Drops also Bilder, die bei der Aufnahme verloren gegangen sind. Wer so etwas bei seinen Versuchen mit der Raspi Cam nicht beobachtet, der kann glücklich sein und diesen Artikel getrost überspringen. Für alle anderen teste und bewerte ich hier eine Anzahl von Tuningmaßnahmen, um die Frame Drops zu minimieren.

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

Woher kommen diese Frame Drops?

Wenn ich das wüsste, dann wäre es vermutlich leicht, eine Lösung zu finden. Was man aber sagen kann, ist dass auf dem Weg vom Kamerasensor auf die SD-Karte irgendwo Videoframes verloren gehen. Das wird weniger beim Schreiben auf die SD-Karte passieren, denn dieses Verfahren ist gut gesichert und außerdem nicht zeitkritisch. Also passieren die Drops vermutlich eher bei der Übergabe der einzelnen Bilder vom Kamerasensor an die Software und beim Kodieren eines H.264 Videos daraus. Also bevor die Kameradaten in unser Python-Programm geraten, was es natürlich schwer macht, dem programmtechnisch zu begegnen.

Warum fehlen einzelne (oder mehrere) Frames? Die Kamera liefert jede Sekunde 25 Frames in Full-HD-Auflösung, das ist zumindest die Einstellung in diesem Projekt. An sich liegen wir damit innerhalb der Spezifikationen der Kamera-Hardware und des Picamera-Software-Moduls. Allerdings fast am äußeren Ende der Spezifikationen, 30 fps könnte man noch einstellen, aber dann ist Schluss. Und für den Raspberry Pi 3 scheint es ambitioniert zu sein, diese Datenmenge weg zu schaffen. Die Kamera liefert und liefert und wenn hinten der RasPi nicht nachkommt, die Daten zu kodieren (was bei H264 zum Glück in Hardware passiert) und weiter zu reichen, dann muss es zwangsweise zu Datenverlusten kommen. Nun ist es aber durchaus nicht so, dass es immer Probleme gibt, da wird auch mal ein Video korrekt und ohne Drops aufgezeichnet und das lässt vermuten, dass es ein Lastproblem ist. Irgendwann kommt der Raspberry Pi in eine Überlastsituation, in der er nicht mehr alle Frames vom Kamerasensor abholen kann. Ob das aber eine überlastete CPU (wir haben immerhin 4 CPU-Kerne), zu knapper Speicher oder ein Flaschenhals im I/O-Bereich ist, das ist erst mal nicht ersichtlich.

Was findet sich darüber im Internet?

Leider nicht viel, was entweder daran liegen kann, dass nur ich Probleme habe und sonst niemand, oder daran, dass die Raspberry Kamera kaum mit dieser hohen Auflösung gefahren wird. In der Tat gehen die Probleme drastisch zurück, wenn Auflösung und Framerate gesenkt werden. Allerdings finden sich im Internet durchaus generelle Tuningvorschläge, vor allem in Richtung Overclocking.

Testserie verschiedener Maßnahmen

Es braucht also eigene Tests um auszumessen, welche Maßnahmen Erfolge bringen. Und diese Erfolge müssen in Zahlen gefasst werden um sie vergleichen zu können.

Testumgebung und -durchführung

Ich verwende meine beiden Kameras für diese Tests, lasse dabei die Konfiguration so, wie es für den Kamerabetrieb eingestellt ist, beende aber natürlich alle laufenden Python-Programme des Projekts. Die Tests werden mit RaspiVid auf der Commandline-Ebene durchgeführt, mit einem Kommando wie diesem:

pi@raspi166:~ $ raspivid -o myvid.h264 -w 1920 -h 1080 -t 60000
pi@raspi166:~ $

Das ist eine Videoaufnahme von 60 Sekunden Dauer mit Full-HD 1080p30. Die 30fps sind die Defaulteinstellung. Die Auswertung erfolgt im Nachgang mit MP4Box, in dem das erzeugte H.264-Video eingelesen wird:

pi@raspi166:~ $ MP4Box -fps 30 -add myvid.h264 -new /dev/null
AVC-H264 import - frame size 1920 x 1080 at 30.000 FPS
AVC Import results: 1791 samples - Slices: 30 I 1761 P 0 B - 0 SEI - 30 IDR
Saving /dev/null: 0.500 secs Interleaving
pi@raspi166:~ $

Das Ergebnis der MP4Box-Verarbeitung interessiert dabei nicht und wird ins Null-Device geschrieben. Interessant ist die Ausgabe der Frame-Anzahl, im Beispiel oben mit 1791 samples angegeben. Rein rechnerisch müssten 60 Sekunden á 30 Frames 1800 Frames ergeben, da fehlt also schon ein wenig. Um Ausreißern das Gewicht zu nehmen, werden alle Tests mindestens 10x auf jedem Kamera-Rechner durchgeführt und danach eine durchschnittliche Prozentzahl der fehlenden Frames berechnet.

Und dann braucht es natürlich eine Last, die CPU und I/O-System etwas ins Schwitzen bringt. Dazu lasse ich in einer getrennte SSH-Session MP4Box eine mehrere Minuten dauernde Videosequenz von H.264 nach MP4 wandeln.

Leerlauf (Frame Drop = 0,21%)

Hier gibt es keine Last, der Raspberry Pi hat nicht anderes zu tun, als mit RaspiVid das Video aufzuzeichnen. Die Frameverluste sind dabei äußerst gering.

Unter Last (Frame Drop = 17,45%)

Der selbe RaspiVid-Befehl aber diesmal arbeitet nebenher eine aufwändige MP4Box-Verarbeitung. Deutliche Frame Drops sind zu beobachten, wobei sich interessanterweise meine beiden Rechner auch deutlich von einander unterscheiden: Raspi166 = 10,06% und Raspi167 = 24,84% Frameverlust.

Diese 17,45% Frame Loss sind die Benchmark für die folgenden Tests. Maßnahmen, die keine Verbesserung bringen, können ausgesondert werden.

Nun beginnen die eigentlichen Tests verschiedener Maßnahmen – alle natürlich unter Last.

Test 1: Verringerung der Framerate auf 25fps (Frame Drop = 15,62%)

raspivid -o myvid.h264 -w 1920 -h 1080 -t 60000 -fps 25

Das war zu erwarten, wenn wir der Kamera mit 25 Frames per Second weniger abverlangen, als die 30fps zuvor, dann gehen die Frame Drops zurück, allerdings nicht sonderlich stark. Diese Erkenntnis bringt jetzt keinen großen Vorteil, die Videoaufzeichnung im Raspberry Video Camera Projekt erfolgt eh nur mit 25fps.

Test 2: Overclocking GPU und SDRAM (Frame Drop = 17,24%)

Versuchen wir es mit Overclocking – bei Raspberry Pis ist das ja sehr beliebt. Hier die Einstellungen für diesen Test in der config.txt:

over_voltage=5
gpu_freq=550
sdram_freq=550
sdram_schmoo=0x02000020
over_voltage_sdram_p=6
over_voltage_sdram_i=4
over_voltage_sdram_c=4

Hier wird die GPU und das SDRAM etwas aufgedreht, der Erfolg ist aber gleich null.

Test 3: Overclocking CPU, GPU und SDRAM (Frame Drop = 16,27%)

Test wie vor, nur dass auch die CPU etwas mehr Speed verpasst bekommt:

arm_freq=1300
over_voltage=5
gpu_freq=500
sdram_freq=500
sdram_schmoo=0x02000020
over_voltage_sdram_p=6
over_voltage_sdram_i=4
over_voltage_sdram_c=4

Eine kleine Verbesserung, aber der richtige Bringer ist das nicht. Auch weitere Experimente mit Overclocking-Parametern bringen bei mir keine signifikanten Verbesserungen.

Test 4: Prozesspriorität erhöhen (Frame Drop = 17,45%)

Mit dem Linux-Befehl nice lässt sich die Priorität eines Prozesses beeinflussen. Die Idee ist also, den Recordingprozess gegenüber anderen etwas zu privilegieren, in der Hoffnung, dass das die Frame Drops senkt. Dazu rufen das RaspiVid Kommando von oben mit nice auf.:

sudo nice -n -10 raspivid -o myvid.h264 -w 1920 -h 1080 -t 60000

Hier brauchen wir sudo, sonst könnte ja jeder kommen und seine Prozesse bevorrechtigen wollen. Die Prozesspriorität setze ich auf -10, das Minus kommt daher, dass der Prozess weniger nett zu anderen ist, als der Standard. Hätte die Zahl ein positiver (also kein) Vorzeichen, dann wäre der Prozess netter zu anderen, würde seine eigene Priorität also etwas zurücknehmen. Ein Ergebnis dieser Maßnahme ist nicht spürbar, daran würde sich auch nichts ändern, wenn ich dem parallel laufenden Lastprozess mit nice eine geringere Priorität gäbe.

Test 5: GPU Memory auf 256MB erhöhen (Frame Drop = 15,25%)

Bei Verwendung der Kamera am Raspberry Pi wird empfohlen, der GPU (graphic processing unit) 128MB Speicher zuzuordnen. Die Verteilung des System-RAM lässt sich per raspi-config vornehmen. Der Menüpunkt heißt Memory Split. Die Frage ist, ob es Vorteile bringt, der GPU mehr Speicher zuzuordnen. Und in der Tat, eine Erhöhung auf 256MB bringt zumindest eine kleine aber spürbare Verbesserung, die neugierig macht, wie es mit noch mehr Speicher aussieht.

Test 6: GPU Memory auf 512MB erhöhen (Frame Drop = 7,36%)

Endlich eine Maßnahme, die wirklich zieht. Wenn die Hälfte des Speichers des Raspberry Pi 3 der GPU geschenkt wird, senkt das die Frame Drops dramatisch. Allerdings ist Speicher wertvoll, speziell, wenn wir ein wenig davon auch noch für eine RAM-Disk brauchen. Schwierig wäre diese Maßnahme auch bei einem Raspberry Pi Zero, der überhaupt nur 512MB Speicher hat.

Test 7: Verringerung der Video Bitrate (Frame Drop = 11,03%)

Ähnlich wie bei der Reduzierung der Frames per Second geht es bei der Video Bitrate um eine Herabsetzung der Video Qualität. Der Begriff der Bitrate ist in diesem Zusammenhang etwas schwer zu verstehen. Gemeint ist der Datenstrom zwischen Kamera und dem Raspberry Pi – der Defaultwert ist 17000000 entsprechend 17MBit/s. Ein höherer Wert erhöht die Videoqualität, vergrößert aber auch die entstehende Videodatei. Eine geringere Bitrate vermindert die Qualität, verringert aber auch die Datenmenge. Ich mache einen Versuch mit 8,5Mbit/s, das ist die Hälfte der Defaulteinstellung und in etwa auch der Wert, mit dem das Videoverarbeitungsprogramm kdenlive standard-mäßig Videodaten schreibt. Die Bitrate können wir RaspiVid einfach in der Kommandozeile (-b …) mitgeben:

raspivid -o myvid.h264 -w 1920 -h 1080 -t 60000 -b 8500000

Und tatsächlich, eine Verringerung der Bitrate senkt die Frame Drop Rate. Die Ursache wird vermutlich darin liegen, dass sich das zu verarbeitende Datenaufkommen senkt und die Rechner-Ressourcen damit dann besser zurecht kommen.

Test 8: Disk Cache vergrößern (Frame Drop = 16,75%)

Diese Maßnahme zielt darauf ab, das Disk Caching des Betriebssystems zu beeinflussen. Zu diesem Thema empfehle ich den exzellenten Artikel Better Linux Disk Caching & Performance with vm.dirty_ratio & vm.dirty_background_ratio von Bob Plankers. Das Linux Cacheverhalten lässt sich beeinflussen durch Einträge in die Datei /etc/sysctl.conf. Der erste Versuch vergrößert den Disk-Cache. Es können somit mehr Daten im Cache gehalten werden, bevor auf die Disk (hier natürlich die SD-Karte) geschrieben werden muss. Dadurch besteht die Chance, dass Daten, die durch eine Verarbeitung modifiziert werden, diese Modifikation bereits im Cache erfahren und nicht erst langsam auf der SD-Karte. Und so sehen die Parameter in der /etc/sysctl.conf aus:

vm.dirty_background_ratio = 50
vm.dirty_ratio = 80
vm.dirty_expire_centisecs = 80000
vm.dirty_writeback_centisecs = 5000

Der Erfolg ist allerdings mäßig. Bemerkenswerterweise beschleunigt diese Maßnahme den Lastprozess, was sich aber vermutlich eher negativ auf die Videoaufzeichnung auswirkt.

Test 9: Disk Cache verkleinern (Frame Drop = 7,83%)

Dann probieren wir es anders herum und verkleinern den Disk Cache. Das bewirkt, dass weniger Daten im Cache gehalten werden können und dadurch kontinuierlicher auf die SD.Karte geschrieben wird. Die Parameter sind folgende:

vm.dirty_background_ratio = 3
vm.dirty_ratio = 50
vm.dirty_expire_centisecs = 300
vm.dirty_writeback_centisecs = 300

Und interessanterweise bringt das einen sehr guten Erfolg. Der Lastprozess wird etwas ausgebremst, was scheinbar deutliche Vorteile für die Videoaufzeichnung hat. Ähnliche Erfahrungen macht übrigens ein interessantes Rakentprojekt, das allerdings einen alten Raspberry Pi B einsetzt.

Kombination der besten Maßnahmen ( Frame Drop = 1,09%)

Das waren die 9 Einzelmaßnahmen, die mir eingefallen sind, um die Frame Drop Rate bei Videoaufzeichnungen in hoher Auflösung zu senken. Einige bringen fast nichts, andere bieten aber gute Ansätze. Im nächsten Test kombiniere ich nun die drei erfolgreichsten Maßnahmen:

  • Test 6: GPU Memory auf 512MB erhöhen (Frame Drop = 7,36%)
  • Test 9: Disk Cache verkleinern (Frame Drop = 7,83%)
  • Test 7: Verringerung der Video Bitrate (Frame Drop = 11,03%)

Und siehe da, die Kombination der drei Maßnahmen senkt die Frame Drops von ursprünglich 17,45% auf 1,09%. Das ist ein richtig guter Erfolg. Allerdings möchte ich daran noch ein wenig weiter optimieren, denn zwei der Maßnahmen verursachen mir etwas Unbehagen.

  • 512MB Speicher an die GPU zu geben, das ist schon ein wenig viel.
  • Und die Verringerung der Video-Bitrate geht auf Kosten der Qualität.

Optimierung der besten Maßnahmen ( Frame Drop = 1,06%)

Bei nahezu gleichem Erfolg habe ich die drei Maßnahmen noch einmal wie folgt angepasst:

  • Der Speicher für die GPU (siehe oben Test 6) wird etwas verringert auf 384MB. Das ist genau der Mittelwert zwischen 256 und 512MB und lässt damit etwas mehr RAM übrig. Erreicht wird das per raspi-config.
  • Die Video-Bitrate (siehe Test 7) senke ich von ursprünglichen 17MBit/s nicht bis auf 8,5MBit/s ab sondern nur auf 10MBit/s, was die Videoqualität wieder geringfügig erhöht.
  • Im Gegenzug verschärfe ich die Verkleinerung des Disk-Caches (siehe Test 9) noch ein wenig weiter. Da meine Raspberry Pis ausschließlich als Kamerarechner eingesetzt werden, kann ich ruhig in diese Richtung optimieren und muss keine Rücksicht auf andere Anwendungen nehmen. Die Einträge in die /etc/sysctl.conf Datei sehen nun wie folgt aus:
vm.dirty_background_ratio = 2
vm.dirty_ratio = 20 (default)
vm.dirty_expire_centisecs = 300
vm.dirty_writeback_centisecs = 300

Setzen der Video-Bitrate in Python

Zwei der Maßnahmen können auf Betriebssystemseite wie beschrieben vorgenommen werden, was die Bitrate aber angeht, so hatte ich im Test RaspiVid angepasst. Für die Produktion hilft das natürlich nicht, hier wird RaspiVid ja gar nicht eingesetzt. Wir müssen also an die Python-Programme ran und den entsprechenden Parameter von Picamera setzen. Betroffen sind zwei Python-Programme:

  • record2.py oder falls noch jemand den Vorgänger einsetzt dann eben record.py und
  • recordremote.py für alle Remote-Kameras.

Die Anpassung der Video-Bitrate zeige ich jetzt anhand von record2.py, weil dieses Programm sicher häufiger genutzt wird, als die anderen beiden. Die Programme sind sich aber sehr ähnlich, so dass die Anpassung überall gleich funktioniert.

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), bitrate=10000000)
  camera.start_recording(stream, format='h264', bitrate=10000000)
  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()

An zwei Stellen (im Programm fett hervorgehoben) wird der bitrate-Parameter hinzugefügt und eine Bitrate von 10MBit/s eingestellt. Das Komma zur Trennung von den bestehenden Parametern dabei aber nicht vergessen! Das restliche Programm bleibt unverändert.

Schlussbetrachtung

Glücklich der, der keine Frame Drops bei seiner Videoaufzeichnung hat, denn er kann sich diesen Artikel und alle Maßnahmen daraus sparen. Wer doch, dem können drei Maßnahmen helfen.

  1. Das Herunterschrauben der Anforderungen. Eine geringere Auflösung und Framerate sowie geringere Videoqualität (z.B. Bitrate) senken das Datenaufkommen und wirken Frameverlusten entgegen.
  2. Mehr RAM für die Grafikeinheit (GPU). Das hilft wohl der GPU, Videodaten schneller zu verarbeiten.
  3. Gleichmäßigeres Schreiben auf SD-Karte durch kleineren Disk-Cache. Das scheint das I/O-Verhalten so anzupassen, dass es der Videoverarbeitung gut tut.

Am besten nimmt man alle drei Maßnahmen zusammen.

 


Weitere Artikel in dieser Kategorie:

Schreiben Sie einen Kommentar

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