Prof. Dr.-Ing. Oliver Radfelder
Informatik / Wirtschaftsinformatik
Hochschule Bremerhaven
Kurz und Knapp: netpbm und ffmpeg

Mit ffmpeg lassen sich Bilderfolgen automatisiert auf der Kommandozeile und damit in einem Skript zu einem Video konvertieren. Es gibt noch viele andere Anwendungsfälle - Konvertieren, Schneiden, Skalieren, Überblenden etc.

Hier soll nur gezeigt werden, wie sich ein Video aus einer Menge von Bildern erzeugen lässt.

Generiere mit einem beliebigen Verfahren eine Abfolge von gleich großen Bildern als einfaches Textformat.

#!/usr/bin/env bash
header="P1
30 30"
for i in {00..29}; do 
  filename=image-$i.pbm
  echo "$header" > $filename
  for h in {00..29}; do
    for w in {00..29}; do
      pixel=0
      if [ $h = $i ] ; then
        pixel=1
      fi
      echo -n "$pixel "
    done
    echo
  done >> $filename
done

Mit dem Skript werden per bash 30 pbm-Dateien erzeugt. Das könnte natürlich eine beliebige Programmiersprache sein, mit der wir in Dateien schreiben können oder zumindest Textzeilen auf stdout ausgeben können.

Die Endung pbm steht für Portable Bitmap Format und stammt aus dem Umfeld von netpbm. Das Format besteht aus einer Textzeile, in der die MagicNumber angegeben wird - in diesem Fall P1. In der zweiten Zeile wird die Auflösung des Bildes angegeben. Danach folgen die einzelnen Pixel zeilenweise von jeweils links nach rechts. Zeilenumbrüche können beliebig zur Erhöhung der Lesbarkeit eingefügt werden. Es hilft aber zum Experimentieren die jeweilige Zeilenbreite zu nutzen.

P1
10 10
1 0 0 0 0 0 0 0 0 0
1 0 0 0 0 0 0 0 0 0
1 0 0 0 0 0 0 0 0 0
1 0 0 0 0 0 0 0 0 0
1 0 0 0 0 0 0 0 0 0
1 0 0 0 0 0 0 0 0 0
1 0 0 0 0 0 0 0 0 0
1 0 0 0 0 0 0 0 0 0
1 0 0 0 0 0 0 0 0 0
1 0 0 0 0 0 0 0 0 0
Sollten wir ein farbiges Bild erstellen wollen, erzeugen wir den Header wie folgt:
#!/usr/bin/env bash
header="P3
30 30
255"

Die MagicNumber wird von P1 zu P3. In der dritten Zeile gibt man noch den Wertebereiche der einzelnen Farbkomponenten an - typischerweise 255. Die Daten sind jeweils mit drei Werten als Rot-Grün-Blau (RGB) zu kodieren, die zusammen ein Pixel darstellen. Die Endung ist dann ppm.

pixel="$red $green $blue "
echo -n "$pixel "

Irgendwann zeigt sich aber - bei großen farbigen Bildern - dass das pbm Format vielleicht nicht perfekt ist, weil es sehr große Dateien erzeugt. Für das Verstehen der Prinzipien ist es aber ganz hervorragend geeignet.

ffmpeg kann sowohl mit pbm-Dateien als auch anderen Formaten wie png umgehen. In jedem Fall aber sollte gleich beim Erzeugen der Bilddateien darauf geachtet werden, dass die Dateinamen mit dem impliziten Zähler zum einen bei 0 anfangen und zum anderen jeweils gleich viele Ziffern besitzen. Daher wird oben bei 00 begonnen und bis 29 hochgezählt.

Der typische minimale Aufruf besteht nun aus der Angabe, wieviele Bilder pro Sekunde eingelesen werden sollen (-r), welchem Format die eingelesenen Dateinamen (-i image-%02d.pbm) entsprechen, welcher Codec benutzt werden soll (-vcodec), das Ausgabepixelformat (-pix_fmt yuv420p) und dem Dateinamen der Zieldatei:

ffmpeg \
  -y \
  -r 10 \
  -i image-%02d.pbm \
  -vcodec libx264 \
  -pix_fmt yuv420p \
  pixel.mp4

Das Format für die Eingabedateien ist vielleicht etwas erklärungsbedürtig: Der Platzhalter %02d steht für eine zweistellige Dezimalzahl. So liest ffmpeg die Dateien mit den Namen image00.pbm, image01.pbm etc. nacheinander ein. Die beiden Werte für die Schalter -vcodec und -pix_fmt solltest Du vorerst, bis Du Dich tiefer mit Videokodierung beschäftigt haben, übernehmen.

Solltest Du mehr als 100 oder 1000 Bilder zu einem Film umwandeln wollen, müssen sie im Zähler bei 000 bzw. 0000 beginnen. Das funktioniert mit der for-Schleife und brace-Expansion recht gut. Solltest Du aus einem Datenstrom heraus Bilder erzeugen und in jeder Iteration einen Zähler hochzählen, sind führende Nullen sehr hinderlich. Dann ist ein üblicher Trick, den Zähler nicht bei 0 sondern beispielsweise bei 10000 beginnen zu lassen und die führende 1 als Teil des Dateinamens zu betrachten.

-i image-1%04d.pbm  

ffmpeg besitzt so viele Schalter und Funktionen, dass Du Dich auf diversen Seiten darüber informieren solltest und auch in die Dokumentation schauen solltest ffmpeg.org. Einige Schalter seien hier aber noch angemerkt. Da das Video mit dem obigen Verfahren sehr klein ist (30x30 Pixel), kannst Du die einzelnen Bilder mit convert vorher hochskalieren und zugleich, um Speicherplatz zu sparen, in png-Dateien konvertieren.:

for i in {00..29}; do 
  convert -scale 300 image$i.pbm image$i.png
done

Alternativ kann das auch direkt ffmpeg:

ffmpeg \
  -y \
  -r 10 \
  -i image-%02d.pbm \
  -crf 23 \
  -vcodec libx264 \
  -vf scale=300x300:flags=neighbor \
  -pix_fmt yuv420p \
  pixelsharp.mp4

-y
Beantworte Nachfragen (nach dem Überschreiben) automatisch mit yes
-crf
Constant Rate Factor: je nach Format unterschiedliche Wertebereiche für die Kompressionsstärke. 0 steht meist für keine Kompression (verlustfrei). Bei dem x264-Kodierer steht 51 für das Maximum an möglichem Qualitätsverlust. 18 bis 28 sind erfahrungsgemäß gute Werte.
-vf
Videofilter - hier können sehr komplexe Ausdrücke stehen. Dieser skaliert auf die Größe 300 mal 300 und benutzt den Algorithmus nearest neighbor, wodurch die Skalierung nicht weichzeichnet. Für solche Fälle wie GameOfLife oder ähnliches ist das wünschenswert (siehe unten). Für andere Fälle kann das Flag weglassen werden. Lasse :flags=neighbor weg und vergleiche die Ergebnisse.

Bisweilen wollen wir explizit zwei Effekte vergleichen. Dann hilft es, sie nebeneinander aufzustapeln:

ffmpeg -y -i pixelsharp.mp4 -i pixel.mp4 -filter_complex hstack pixelboth.mp4

Wenn Du mit einer anderen Programmiersprache (z.B. Java) die Möglichkeit hast, einzelne Zustände herauszuschreiben, dort aber noch keinen Dateizugriff kennst, kannst Du mit einem einfachen Protokoll arbeiten. Wenn vor jedem Bild ein Marker gesetzt wird (das Wort next auf einer Zeilen oder einfach eine Leerzeile), kannst Du die Ausgabe in eine Pipe schicken und immer dann, wenn der Marker kommt, ein neues Bild erzeugen:

#!/usr/bin/env bash
# stream2pbm
num=10000
while read line; do
  if test "$line" = "next"; then
    ((num++));
    echo "P1"  > image$num.pbm
    echo "50 50" >> image$num.pbm
  else
    echo "$line" >> image$num.pbm
  fi
done
java GameOfLife | ./stream2pbm.sh

Danach haben wir dann so viele Bilder, wie next geschrieben wurde in einzelnen, fortlaufend nummerierten Dateien und können sie wie oben mit ffmpeg zusammenbringen.

Auf besonderen Wunsch noch eine Vorlage zum Überblenden von Bildern mit Effekten. Das ist sicher starker Tobak und vermutlich nichts für Leute, die erstmalig mit ffmpeg oder gar der Bash in Kontakt kommen- aber denke an den Anwendungsfall, einfach ein paar Bilder in ein Verzeichnis zu kopieren und den passenden ffmpeg-Aufruf automatisch aus einem ls zu erzeugen (was natürlich alles in einem Skript steht) und eine wunderbare Präsentation für den Diaabend zu haben ...:


ffmpeg -y  \
  -loop 1 -t 5 -i img2.jpg \
  -loop 1 -t 5 -i img1.jpg \
  -loop 1 -t 5 -i img2.jpg \
  -loop 1 -t 5 -i img1.jpg \
  -loop 1 -t 5 -i img2.jpg \
  -loop 1 -t 5 -i img1.jpg \
  -filter_complex "
   [0][1]xfade=transition=fadeblack:duration=0.5:offset=2[f1];
   [f1][2]xfade=transition=diagtr:duration=0.5:offset=6[f2];
   [f2][3]xfade=transition=radial:duration=0.5:offset=8[f3];
   [f3][4]xfade=transition=circlecrop:duration=0.5:offset=10[f4];
   [f4][5]xfade=transition=squeezeh:duration=0.5:offset=12,format=yuv420p[v]
   " \
  -map "[v]" \
  -movflags \
  +faststart \
  -r 25 output.mp4
  

Text ein- und ausblenden auf einem Video mit Transparenz: Bauchbindengenerator.


ffmpeg -y  -i ersti.mov -c:v libvpx-vp9 -filter_complex \
"[0]split[base][text];\
[text]drawtext=fontfile=Poppins-Medium.ttf:text='ffmpeg dilettant': \
fontcolor=#8FFFC5:fontsize=60:\
enable='between(t,0.5,6.5)':\
x=504:y=1080,\
format=yuva420p,\
fade=t=in:st=0.6:d=0.3:alpha=1,\
fade=t=out:st=5.9:d=0.3:alpha=1[subtitles];\
[base][subtitles]overlay,format=yuva420p" \
-pix_fmt yuva420p  -lossless 1 -metadata:s:v:0 alpha_mode="1"  -auto-alt-ref 0 converted.webm

Filter zum Schärfen und für Kontrasterhöhung:

ffmpeg -i input.mp4 -vf 'unsharp=lx=5:ly=5,eq=contrast:1.3' output.mp4

Vergleich zweier Videos nach Filtern:

ffmpeg -i input1.mp4 -i input2.mp4 -filter_complex "[0]crop=(iw/6)*3:ih:0:0[left];[1]crop=(iw/6)*3:ih:ow:0[right];[left][right]hstack=inputs=2" -pix_fmt yuv420p -movflags +faststart compare.mp4
Weiterführendes
ffmpeg.org
ffmpeg scale filter
youtube Video FFmpeg installieren - Filmmaterial effizient konvertieren
youtube FFmpeg Crash Course | Install, Convert, Reduce File Size, Trim, Crop, Watermark, Chroma Key, Overlay
youtube FFMPEG Advanced Techniques Pt1 - Advanced Filters
xfade filter
crossfade mit xfade und co
Cheatcheet