Zeitraffer mit Linux

Aus Hobbyelektronik.org

Was macht man mit einem stromsparenden Computer und einer Webcam?

Die Frage habe ich mir genau zur letzten Lernphase meines Studiums gestellt und gleich mal auf die Probe gestellt, ob es möglich ist, innerhalb einer Mittagspause eine Zeitraffer-Kamera zu basteln (siehe auch hier). Wenn man als Bash-Anfänger die die Dauer der Mittagspause an die südländische Siesta annähert kann ich schon vorweg sagen: Ja, es ist möglich!

Ausgangsmaterial

Als Computer kommt der Raspberry Pi zum Einsatz. Als Webcam dient eine Logitech C310. Um die SD-Karte etwas zu schonen kommt ein billiger USB-Stick zum Einsatz, der nur als Aufnahmeziel dient.

Einrichten

Kamera

Da es sich bei der Webcam um eine mit USB-Anschluss handelt, kann sie (meines Wissens) nicht ausschließlich mit dem verwendet werden, das Raspian hergibt. Mit dem Befehl lsusb kann man herausfinden, was am USB hängt:

$ lsusb
Bus 001 Device 002: ID 0424:9512 Standard Microsystems Corp.
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
Bus 001 Device 003: ID 0424:ec00 Standard Microsystems Corp.
Bus 001 Device 004: ID 058f:6387 Alcor Micro Corp. Flash Drive
Bus 001 Device 005: ID 046d:081b Logitech, Inc. Webcam C310

Die Webcam ist also am Computer anhängig, genauso wie der USB-Stick.

Über den Befehl ls /dev/video* werden alle Geräte angezeigt, die mit dem Namen video anfangen. Wurden also Treiber für die Kamera gefunden, erscheint sie in der Ausgabe:

$ ls /dev/video*
/dev/video0

Weil cat /dev/video0 > foo.jpg nicht so wirklich funktioniert, muss ein Vermittler her. Ich habe gute Erfahrungen mit fswebcam gemacht. Installation und Test sehen wie folgt aus:

$ sudo apt-get install fswebcam
[...]
$ fswebcam -r 1280x960 --jpeg 85 -q -d /dev/video0 testimg.jpg

Der letztere Befehl nimmt ein Bild mit der Auflösung (Parameter -r) 1280x960 auf. Das -q nach der Qualitätsstufe (hier: 85) ist Absicht - bei dem Parameter handelt es sich nicht um -q wie quality, sondern um quiet. Mit -d wird das Device gewählt und anschließend folgt die Ausgabedatei. Diese kann, sofern ein Webserver installiert ist, nach /var/www verschieben oder natürlich direkt auf dem Computer ansehen. In meinem Fall wird der Raspberry "headless" betrieben und die Datei über SSH-Filetransfer auf den Haupt-PC befördert.

Funktioniert die Kamera, kann der USB-Speicher angedockt werden:

USB-Stick einbinden

Viele Wege führen bekanntlich nach Rom, genauso kann man Speicher auf verschiedene Weisen einbinden. Das hier ist eine.

Zunächst muss ein Verzeichnis angelegt werden, in das der USB-Stick später eingebunden (gemountet) wird. In meinem Fall ist dies /media/usb:

$ sudo mkdir /media/usb

Nun muss herausgefunden werden, wie man den Speicher anspricht. Am besten verwendet man dessen Universally Unique Identifier (kurz UUID). Der Vorteil an diesem ist, wie der Name schon sagt, dass er Eindeutig ist. Mit dem Befehl blkid können die UUIDs aller verfügbaren Speicher angezeigt werden:

$ sudo blkid
/dev/sda1: UUID="0CA5-719C" TYPE="vfat" LABEL="KINGSTON"
/dev/mmcblk0p1: SEC_TYPE="msdos" UUID="C522-EA52" TYPE="vfat"
/dev/mmcblk0p2: UUID="62ba9ec9-47d9-4421-aaee-71dd6c0f3707" TYPE="ext4"

In geistiger Umnachtung habe ich den USB-Stick "KINGSTON" genannt, das hat allerdings keine Bedeutung. Wichtig ist das, was hinter "UUID=" steht. Man kann den Stick zwar schnell mit mount einbinden, nach dem nächsten Neustart hat man ihn aber verloren und das passiert - auch wenn das System stabil läuft - mit Sicherheit.

Also muss er in die fstab (file system table) eingetragen werden. Da man mit ihr nicht spielt, muss sie mit Superuser-Rechten bearbeitet werden:

$ sudo nano /etc/fstab

Die bereits vorhandenen Einträge kann man ignorieren, hier muss der zu verwendende USB-Stick hinzugefügt werden. Ist dieser mit FAT formatiert und hat die UUID 0CA5-719C, kann folgende Zeile an das Ende der Datei geschrieben werden:

UUID=0CA5-719C  /media/usb/     vfat    defaults,auto,users,noatime,umask=000,rw  0       0

Mit Strg+X Schließen und das Speichern mit Y und Druck auf die Enter-Taste bestätigen.

Navigiert man unter /media/usb kann man nun wunderbar Dateien anlegen, ABER: Diese befinden sich NICHT auf dem USB-Stick! fstab wird nur beim Neustart gelesen. Also heißt es

$ sudo reboot

eingeben und somit die Uptime auf 0 zurücksetzen.

Mit dem Befehl df kann jetzt überprüft werden, ob der Speicher richtig eingebunden wurde. Gleichzeitig sieht man auch, wie viel Speicher bereits genutzt wird:

$ df
Filesystem     1K-blocks    Used Available Use% Mounted on
rootfs           1838936 1775400         0 100% /
/dev/root        1838936 1775400         0 100% /
devtmpfs          110568       0    110568   0% /dev
tmpfs              23768     232     23536   1% /run
tmpfs               5120       0      5120   0% /run/lock
tmpfs              47520       0     47520   0% /run/shm
/dev/mmcblk0p1     57288   18960     38328  34% /boot
/dev/sda1        4006852 1591764   2415088  40% /media/usb

Der letzte Eintrag ist der interessante: /dev/sda1 ist nach /media/usb gemountet. Wenn man sich nicht sicher ist, kann man vorher von einem anderen PC eine Datei auf dem USB-Stick ablegen und mit ls /media/usb prüfen, ob diese vorhanden ist.

(Es kann sein, dass man die Berechtigung auf den Mouting-Point noch anpassen muss, da der Artikels etwas später als das damalige Einrichten entstand, bin ich mir nicht absolut sicher)

Das Zeitraffer-Script

Die Kamera funktioniert, der Zusatzspeicher ist da - nun fehlt nur noch die zeitgesteuerte Aufnahme. Am einfachsten geht das über ein kleines Script, das wahlweise direkt im Benutzerordner (home) angelegt werden kann. Der schnellste Weg nach Hause führt über den Befehl cd.

Über den Befehl

$ nano timeshot.sh

wird Nano für die (noch nicht vorhandene) Datei timeshot.sh geöffnet. Dort kann nun folgender Code eingefügt werden:

#!/bin/sh
dt=$(date +"%Y-%m-%d")
time_now=$(date +%s)
time_mid=$(date -d "00:00" +%s)
mins=$((($time_now-$time_mid)/60))
timestamp=`printf "%04d" $mins`
dstdir=/media/usb/timelapse/$dt/

mkdir -p $dstdir

fswebcam -r 1280x960 --jpeg 85 -q -d /dev/video0 $dstdir/img_$timestamp.jpg

Ganz oben kommt erst einmal der Shebang, anschließend werden ein paar Variablen definiert: In der Variable dt steht das aktuelle Datum im Format "Jahr-Monat-Datum", anschließend wird in die Variablen time_now und time_mid der Unix-Timestamp (also die Sekunden seit dem 01.01.1970) für die aktuelle Zeit und Mitternacht des aktuellen Tags geschrieben.

Anschließend wird die Differenz der beiden Werte berechnet und durch 60 geteilt. Mit dem Befehl `printf "%04d" $mins` wird die Zahl auf alles "vor dem Komma" abgeschnitten und auf 4 Zeichen mit Nullen gefüllt und in die Variable timestamp geschrieben. In timestamp steht also nichts anderes als die aktuelle Minute des Tages. Warum? Später mehr dazu.

dstdir gibt, wie sich erahnen lässt, das Zielverzeichnis des Bildes an, wobei der Ordner in der tiefsten Ebene dem aktuellen Datum entspricht. Existiert dieses Verzeichnis nicht, wird es über mkdir angelegt.

Im letzten Befehl wird das bereits bekannte Programm fswebcam aufgerufen. Der endgültige Dateiname wird hier noch zusammengebaut. Vor die Minute des Tages wird noch ein "img_" vorangestellt und noch ein ".jpg" angehängt, damit die Dateien auch einen "Nachnamen" haben.

Mit Strg+X, y, Enter ist die Datei im Dateisystem, kann aber noch nicht ausgeführt werden.

Über den Befehl

$ chmod 766

gibt man der Datei Ausführungsrechte für den eigenen Benutzer. Führt man sie nun aus, bringt sie zumindest bei mir eine Warnung (die man aber ignorieren kann):

$ ./timeshot.sh
Corrupt JPEG data: 1 extraneous bytes before marker 0xd0

Damit man nun nicht jede Minute vor dem Rechner verbringen muss, ist es zumindest empfehlenswert, die Aufnahme zeitgesteuert auszuführen. Dafür gibt es unter Linux crontab (Zeitplan).

Mit dem Befehl crontab -e öffnet man diesen für den aktuellen Benutzer. Ganz am Ende der Datei kann man nun folgendes eintragen:

* * * * * /home/pi/timeshot.sh

Was da auf den ersten Blick etwas kryptisch aussieht ist es eigentlich nicht. /home/pi/timeshot.sh deutet zumindest einmal auf die Datei hin, die ausgeführt wird. Die Sterne vorne sind Wildcards, und bedeutet so viel wie "jede". In Cron gibt man vorne die Ausführung für die Minuten, Stunden, Tage im Monat, den Monaten und Wochentage an.

Die 5 Sterne bedeuten also: "Führe die hintenstehende Datei jede Minute, jede Stunde, jeden Tag im Monat, jeden Monat und jeden Wochentag aus". Will man in der Nacht keine schwarzen Bilder sammeln, kann man auch folgende Zeile verwenden:

* 6-21 * * * /home/pi/timeshot.sh

Heißt dann so viel wie: "Führe die hintenstehende Datei jede Minute, in den Stunden von 6 bis 21, jeden Tag im Monat, jeden Monat und jeden Wochentag aus".

Damit ist aber nicht "von 6 bis 21 Uhr!" gemeint - es werden Bilder von 6:00 Uhr bis 21:59 Uhr aufgenommen.

Bilder verarbeiten

Fotos einfach der der Bilder aufnehmen macht natürlich wenig Sinn. Zunächst ist aber erst einmal die Frage: Wie viel Daten kommen überhaupt zusammen? In meinem Fall - jede Minute (also 1440 am Tag) ein Bild 1280x960 bei 85 % Qualität sammeln sich am Tag etwa 85 MiB an.

Was macht man damit? Hier ein paar Vorschläge:

Video

Naheliegend ist natürlich, ein Video der Fotos zu erstellen. Nichts leichter als das! Das ist auch der Grund, warum sie nach dem oben angegebenen Schema durchnummeriert sind. Dateien mit diesem Schema lassen sich direkt mit FFmpeg verarbeiten.

Ich wollte es auf dem Raspberry Pi machen, musste aber sehr schnell einsehen, dass man es schlichtweg vergessen kann, zumindest bei meinen Parametern - h.264 bei 2,5 Mbit/s Bitrate. Die ersten Bilder werden noch relativ schnell zusammengefügt, irgendwann kommt man aber deutlich unter 1 fps beim Codieren. Vermutlich scheitert es hier beim RAM und vermutlich verwendet FFmpeg den in Hardware vorhandenen h.264-Codec nicht.

Der Desktop-PC hat da deutlich mehr Power.

Also Bilder übertragen, in dem Ordner eine Kommandozeile öffnen und folgenden Befehl reinklöppeln:

ffmpeg.exe -f image2 -i img_%04d.jpg -vcodec libx264 -vb 2500k -acodec null -r 25 day.mp4

Die Magie kommt vom Parameter -i, in dem alle Bilder mit dem Format img_%04d.jpg zusammengestückelt werden.

Wer den Befehl in eine Batch-Datei stecken möchte, muss das % durch %% ersetzen, damit das "%0" nicht interpretiert wird.

Allerdings gibt es ein kleines Problem: Fehlt ein Bild, wird die Erstellung dort beendet.

Um diesem Problem zu gehen, habe ich ein kleines PHP-Script geschrieben, das hierfür Platzhalter einfügt:

<?php

$w = 1280;
$h = 960;
$name_min = 0;
$name_max = 1439;
$name_digits = 4;

$name_format = "img_#.jpg";

$im = imagecreatetruecolor($w, $h);
$col = imagecolorallocate($im, 0, 0, 255);
imagefilledrectangle($im, 0, 0, $w, $h, $col);

for($i = $name_min; $i <= $name_max; $i++) {
	$digits = str_pad($i, $name_digits, "0", STR_PAD_LEFT);
	$fname = str_replace("#", $digits, $name_format);
	if(!is_file($fname)) {
		echo "file " . $fname . " not found - create.\n";
		imagejpeg($im, $fname, 85);
	}
}

Das Script macht nichts anderes als Dateien mit dem Format "img_#.jpg" zu suchen, wobei # durch eine mit Nullen auf vier aufgefüllten Zahl von 0 bis 1439 ersetzt wird. Ist eine Datei nicht vorhanden, wird für diese das vorbereite Bild als JPEG geschrieben. Wer will, kann anstelle des imagefilledrectangle natürlich jeden anderen Befehl zur Bildmanipulation einsetzen.

Tagesbild

Nicht wirklich aussagekräftig, aber eine nette Spielerei, die im Prinzip ganz einfach ist: Man nimmt von jedem Einzelbild einen von der Uhrzeit abhängigen Streifen heraus und baut damit ein neues Bild, das aus (fast) allen Bildern des Tages zusammengebaut wurde.

Da die 1280 Pixel Bildbreite schon relativ nah an den 1400 Bildern pro Tag sind, habe ich vertikale Streifen verwendet. Der Einfachheit halber ist das Script ebenfalls in PHP zusammengeschustert:

<?php

$folder = "/bilder/";
$dst = imagecreatetruecolor(1280, 960);
$red = imagecolorallocate($dst, 255, 0, 0);
imagefilledrectangle($dst, 0,0,1280,960, $red);

for($i = 0; $i < 1280; $i++) {
	$file = $folder."/img_".str_pad($i+160, 4, "0", STR_PAD_LEFT).".jpg";
	if(is_file($file)) {
		echo ".";
		$src = imagecreatefromjpeg($file);
		imagecopyresampled($dst, $src, $i, 0, $i, 0, 1, 1280, 1, 1280);
		imagedestroy($src);
	} else {
		echo "#";
	}
}
echo "\n";

imagejpeg($dst, $folder.".jpg", 90);
imagedestroy($dst);

Nicht ganz so übersichtlich wie das vorherige Script. Im Prinzip wird für jedes Bild (es wird erst ab Bild 160 eingelesen, damit es 1280 Streifen werden) ein imagecopyresampled in das Zielbild durchgeführt, wobei jeder Streifen 1 Pixel breit ist.

Ursprünglich wollte ich jede Streifen 2 bzw. 3 Pixel breit machen und die die "linken" mit leicht überblenden, damit es keine harten Kanten gibt. Nur scheint das in PHP nicht so ganz trivial zu sein.

Hier aber ein paar Ergebnisse der Tagesbilder:

Webserver

Natürlich kann man, wenn man das aktuelle Webcam-Bild auf einer Internetseite einbinden will, jede Minute mit immer gleichem Namen in den /var/www-Ordner kopieren lassen. Normalerweise sollte das Betriebssystem die Datei auch im RAM cachen. Trotzdem kann es sein, dass das Ganze unnütze Schreibzyklen auf dem Flash-Speicher erzeugt und somit für eine schnellere Alterung sorgt.

Mit folgendem PHP-Script lässt sich das aktuelle Bild von einem anderen Ort im Dateisystem lesen und in den Webspace holen:

<?php
$img_path = "/media/usb/timelapse/#d/img_#m.jpg";

$date = date("Y-m-d");
$minute = date("H") * 60 + date("i");

$search = array("#d", "#m");
$replace = array($date, $minute);

$file = str_replace($search, $replace, $img_path);

header("Content-Type: Content-Type: image/jpeg");
header("Cache-Control: no-cache, must-revalidate");

if(is_file($file)) {
	readfile($file);
} else {
	$w = 1280;
	$h = 960;
	$im = imagecreatetruecolor($w, $h);
	$col = imagecolorallocate($im, 0, 0, 255);
	imagefilledrectangle($im, 0, 0, $w, $h, $col);
	imagejpeg($im);
}

Wird kein aktuelles Bild gefunden (entweder weil die Kamera Aufnahmepause hat oder es noch kein Bild gibt - Cron läuft nicht zwangsläufig mit dem Minutenumbruch), wird eine blaue Fläche ausgeliefert.

Um die Bilder laden zu können, muss man aber dem Benutzer des Webservers mindestens Leserechte in den entsprechenden Ordnern geben.

Anmerkungen

Hier noch ein paar Dinge, die mir beim Betreiben der Kamera aufgefallen sind:

  • Auch halbwegs moderne Bildsensoren mögen keine dauerhafte Sonneneinstrahlung
  • Die Logitech-Kamera oder deren Treiber stürzen ab und zu ab und reißen den kompletten Raspberry Pi herunter.
  • Schuld ist wohl die Netzwerkkarte. sudo rpi-update hilft, löst das Problem aber nicht vollständig.