Generieren von Filmen aus Python, ohne einzelne Frames in Dateien zu speichern

73

Ich möchte einen h264- oder divx-Film aus Frames erstellen, die ich in einem Python-Skript in matplotlib generiere. Es gibt ungefähr 100.000 Bilder in diesem Film.

In Beispielen im Web [z. 1] habe ich nur die Methode gesehen, jeden Frame als PNG zu speichern und dann Mencoder oder ffmpeg für diese Dateien auszuführen. In meinem Fall ist das Speichern jedes Frames unpraktisch. Gibt es eine Möglichkeit, einen aus matplotlib generierten Plot direkt an ffmpeg weiterzuleiten und keine Zwischendateien zu generieren?

Das Programmieren mit der C-API von ffmpeg ist mir zu schwierig [z. 2]. Außerdem benötige ich eine Codierung mit guter Komprimierung wie x264, da die Filmdatei ansonsten für einen nachfolgenden Schritt zu groß ist. Es wäre also großartig, bei mencoder / ffmpeg / x264 zu bleiben.

Gibt es etwas, das mit Rohren gemacht werden kann [3]?

[1] http://matplotlib.sourceforge.net/examples/animation/movie_demo.html

[2] Wie codiert man eine Reihe von Bildern mit der x264 C-API in H264?

[3] http://www.ffmpeg.org/ffmpeg-doc.html#SEC41

Paul
quelle
Ich habe noch keinen Weg gefunden, dies mit derzeit verwalteten Bibliotheken zu tun ... (Ich habe in der Vergangenheit Pymedia verwendet, aber es wird nicht mehr gewartet und baut nicht auf einem System auf, das ich verwende ...) Wenn es hilft, Sie können einen RGB-Puffer einer Matplotlib-Figur erhalten, indem Sie buffer = fig.canvas.tostring_rgb()und die Breite und Höhe der Figur in Pixel mit fig.canvas.get_width_height()(oder fig.bbox.widthusw.)
Joe Kington
OK danke. Das ist nützlich. Ich frage mich, ob eine Transformation des Puffers an ffmpeg weitergeleitet werden kann. pyffmpeg verfügt über einen kürzlich aktualisierten, hochentwickelten Cython-Wrapper zum Lesen eines AVI Frame für Frame. Aber nicht schreiben. Das klingt nach einem möglichen Ausgangspunkt für jemanden, der mit der ffmpeg-Bibliothek vertraut ist. Sogar so etwas wie der im2frame von matlab wäre großartig.
Paul
1
Ich spiele damit herum, dass ffmpeg entweder von einer Eingabepipe (mit der -f image2pipeOption, dass eine Reihe von Bildern erwartet wird) oder von einem lokalen Socket (z. B. udp://localhost:some_port) gelesen wird und in Python in den Socket schreibt ... Bisher nur Teilerfolg ... Ich fühle mich aber fast da ... Ich bin einfach nicht genug mit ffmpeg vertraut ...
Joe Kington
2
Mein Problem war, dass ffmpeg einen Stream von PNG- oder RGB-Rohpuffern akzeptierte (es ist bereits ein Fehler aufgetreten : roundup.ffmpeg.org/issue1854 ). Es funktioniert, wenn Sie JPEGs verwenden. (Verwenden Sie ffmpeg -f image2pipe -vcodec mjpeg -i - ouput.whatever. Sie können ein öffnen subprocess.Popen(cmdstring.split(), stdin=subprocess.PIPE)und jeden Frame in sein schreiben stdin) Ich werde ein detaillierteres Beispiel veröffentlichen, wenn ich eine Chance bekomme ...
Joe Kington
Das ist großartig! Ich werde es morgen versuchen.
Paul

Antworten:

56

Diese Funktionalität wird jetzt (mindestens ab 1.2.0, möglicherweise 1.1) über die MovieWriterKlasse und ihre Unterklassen im animationModul in matplotlib eingebrannt . Sie müssen auch ffmpegim Voraus installieren .

import matplotlib.animation as animation
import numpy as np
from pylab import *


dpi = 100

def ani_frame():
    fig = plt.figure()
    ax = fig.add_subplot(111)
    ax.set_aspect('equal')
    ax.get_xaxis().set_visible(False)
    ax.get_yaxis().set_visible(False)

    im = ax.imshow(rand(300,300),cmap='gray',interpolation='nearest')
    im.set_clim([0,1])
    fig.set_size_inches([5,5])


    tight_layout()


    def update_img(n):
        tmp = rand(300,300)
        im.set_data(tmp)
        return im

    #legend(loc=0)
    ani = animation.FuncAnimation(fig,update_img,300,interval=30)
    writer = animation.writers['ffmpeg'](fps=30)

    ani.save('demo.mp4',writer=writer,dpi=dpi)
    return ani

Dokumentation für animation

Tacaswell
quelle
Gibt es eine Möglichkeit, bestimmte Achsen aufzuzeichnen, nicht die gesamte Figur? Besonders mit FFMpegFileWriter?
Alex
@Alex Nein, der Bereich, in dem Sie Frames speichern können, ist der Figure-Bereich (dasselbe gilt für savefig).
Tacaswell
22

Nachdem ich ffmpeg gepatcht hatte (siehe Kommentare von Joe Kington zu meiner Frage), konnte ich png's wie folgt an ffmpeg weiterleiten:

import subprocess
import numpy as np
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt

outf = 'test.avi'
rate = 1

cmdstring = ('local/bin/ffmpeg',
             '-r', '%d' % rate,
             '-f','image2pipe',
             '-vcodec', 'png',
             '-i', 'pipe:', outf
             )
p = subprocess.Popen(cmdstring, stdin=subprocess.PIPE)

plt.figure()
frames = 10
for i in range(frames):
    plt.imshow(np.random.randn(100,100))
    plt.savefig(p.stdin, format='png')

Ohne den Patch , der zwei Dateien trivial ändert und hinzufügt, würde dies nicht funktionieren libavcodec/png_parser.c. Ich musste den Patch manuell anwenden libavcodec/Makefile. Zuletzt habe ich '-number' entfernt Makefile, um die Manpages zum Erstellen zu bringen. Mit Kompilierungsoptionen,

FFmpeg version 0.6.1, Copyright (c) 2000-2010 the FFmpeg developers
  built on Nov 30 2010 20:42:02 with gcc 4.2.1 (Apple Inc. build 5664)
  configuration: --prefix=/Users/paul/local_test --enable-gpl --enable-postproc --enable-swscale --enable-libxvid --enable-libx264 --enable-nonfree --mandir=/Users/paul/local_test/share/man --enable-shared --enable-pthreads --disable-indevs --cc=/usr/bin/gcc-4.2 --arch=x86_64 --extra-cflags=-I/opt/local/include --extra-ldflags=-L/opt/local/lib
  libavutil     50.15. 1 / 50.15. 1
  libavcodec    52.72. 2 / 52.72. 2
  libavformat   52.64. 2 / 52.64. 2
  libavdevice   52. 2. 0 / 52. 2. 0
  libswscale     0.11. 0 /  0.11. 0
  libpostproc   51. 2. 0 / 51. 2. 0
Paul
quelle
Schön gemacht! +1 (ich war nie in der Lage einen Strom von .png ist zu akzeptieren , zu bekommen ffmpeg, ich glaube , ich brauche meine Version von ffmpeg zu aktualisieren ...) Und, falls Sie sich wundern, es ist durchaus akzeptabel , um Ihre Antwort zu markieren , wie die Antwort auf Ihre Frage. Siehe Diskussion hier: meta.stackexchange.com/questions/17845/…
Joe Kington
1
Hallo @Paul, der Patch-Link ist tot. Wissen Sie, ob es in den Hauptzweig aufgenommen wurde? Wenn nicht, gibt es eine Möglichkeit, diesen Patch zu erhalten?
Gabe
@ Gabe, ich vermute, der Patch wurde aus dem folgenden Beitrag übernommen: superuser.com/questions/426193/…
Paul
@tcaswell, ich habe die Antwort in Ihre Antwort geändert (ich wusste nicht, dass dies möglich ist.) Können Sie bitte die erforderlichen Änderungen vornehmen?
Paul
Ich wollte, dass Sie Ihre Frage bearbeiten, um die neue Funktionalität widerzuspiegeln, aber das funktioniert. Ich habe meine Änderungen rückgängig gemacht. Bist du mit dem Stand der Dinge zufrieden?
Tacaswell
14

Das Konvertieren in Bildformate ist recht langsam und fügt Abhängigkeiten hinzu. Nachdem ich mir diese und andere Seiten angesehen hatte, funktionierte es mit rohen, nicht codierten Puffern mit Mencoder (ffmpeg-Lösung immer noch erwünscht).

Details unter: http://vokicodder.blogspot.com/2011/02/numpy-arrays-to-video.html

import subprocess

import numpy as np

class VideoSink(object) :

    def __init__( self, size, filename="output", rate=10, byteorder="bgra" ) :
            self.size = size
            cmdstring  = ('mencoder',
                    '/dev/stdin',
                    '-demuxer', 'rawvideo',
                    '-rawvideo', 'w=%i:h=%i'%size[::-1]+":fps=%i:format=%s"%(rate,byteorder),
                    '-o', filename+'.avi',
                    '-ovc', 'lavc',
                    )
            self.p = subprocess.Popen(cmdstring, stdin=subprocess.PIPE, shell=False)

    def run(self, image) :
            assert image.shape == self.size
            self.p.stdin.write(image.tostring())
    def close(self) :
            self.p.stdin.close()

Ich habe ein paar nette Beschleunigungen.

user621442
quelle
Ich habe dies für ffmpeg geändert, siehe meine Antwort unten, wenn Sie es noch wollen
cxrodgers
10

Das sind alles wirklich gute Antworten. Hier ist ein weiterer Vorschlag. @ user621442 ist richtig, dass der Engpass normalerweise das Schreiben des Bildes ist. Wenn Sie also PNG-Dateien auf Ihren Videokompressor schreiben, ist dies ziemlich langsam (selbst wenn Sie sie über eine Pipe senden, anstatt auf die Festplatte zu schreiben). Ich habe eine Lösung mit reinem ffmpeg gefunden, die ich persönlich einfacher finde als matplotlib.animation oder mencoder.

In meinem Fall wollte ich das Bild auch nur in einer Achse speichern, anstatt alle Tick-Beschriftungen, den Titel der Abbildung, den Hintergrund der Abbildung usw. zu speichern. Grundsätzlich wollte ich einen Film / eine Animation mit Matplotlib-Code erstellen, habe dies aber nicht getan es "sieht aus wie eine Grafik". Ich habe diesen Code hier eingefügt, aber Sie können Standarddiagramme erstellen und sie stattdessen an ffmpeg weiterleiten, wenn Sie möchten.

import matplotlib.pyplot as plt
import subprocess

# create a figure window that is the exact size of the image
# 400x500 pixels in my case
# don't draw any axis stuff ... thanks to @Joe Kington for this trick
# /programming/14908576/how-to-remove-frame-from-matplotlib-pyplot-figure-vs-matplotlib-figure-frame
f = plt.figure(frameon=False, figsize=(4, 5), dpi=100)
canvas_width, canvas_height = f.canvas.get_width_height()
ax = f.add_axes([0, 0, 1, 1])
ax.axis('off')

def update(frame):
    # your matplotlib code goes here

# Open an ffmpeg process
outf = 'ffmpeg.mp4'
cmdstring = ('ffmpeg', 
    '-y', '-r', '30', # overwrite, 30fps
    '-s', '%dx%d' % (canvas_width, canvas_height), # size of image string
    '-pix_fmt', 'argb', # format
    '-f', 'rawvideo',  '-i', '-', # tell ffmpeg to expect raw video from the pipe
    '-vcodec', 'mpeg4', outf) # output encoding
p = subprocess.Popen(cmdstring, stdin=subprocess.PIPE)

# Draw 1000 frames and write to the pipe
for frame in range(1000):
    # draw the frame
    update(frame)
    plt.draw()

    # extract the image as an ARGB string
    string = f.canvas.tostring_argb()

    # write to pipe
    p.stdin.write(string)

# Finish up
p.communicate()
cxrodgers
quelle
Dies ist ein wirklich sauberer Weg und der, den ich benutzt habe. Um es von einem Skript ausführen zu lassen, müssen Sie einige Mods ausführen. Fügen Sie ganz oben im Skript in den ersten Zeilen Folgendes hinzu: import matplotlibSetzen Sie dann das Backend mit. matplotlib.use('agg', warn = False, force = True)Der einzige andere Mod besteht darin, plt.draw()den obigen Originalcode durch f.canvas.draw()Diese zu ersetzen. Diese sind erforderlich, damit es in einem Skript funktioniert. Ansonsten ist der Code so wie er ist einfach dandy.
JHarchanko
5

Das ist toll! Ich wollte das gleiche tun. Aber ich konnte die gepatchte ffmpeg-Quelle (0.6.1) in Vista mit MingW32 + MSYS + pr enviroment nie kompilieren ... png_parser.c erzeugte Fehler1 während der Kompilierung.

Also habe ich mit PIL eine JPEG-Lösung dafür gefunden. Legen Sie einfach Ihre ffmpeg.exe in den gleichen Ordner wie dieses Skript. Dies sollte mit ffmpeg ohne den Patch unter Windows funktionieren. Ich musste die Methode stdin.write anstelle der Kommunikationsmethode verwenden, die in der offiziellen Dokumentation zum Unterprozess empfohlen wird. Beachten Sie, dass die 2. Option -vcodec den Codierungscodec angibt. Die Pipe wird mit p.stdin.close () geschlossen.

import subprocess
import numpy as np
from PIL import Image

rate = 1
outf = 'test.avi'

cmdstring = ('ffmpeg.exe',
             '-y',
             '-r', '%d' % rate,
             '-f','image2pipe',
             '-vcodec', 'mjpeg',
             '-i', 'pipe:', 
             '-vcodec', 'libxvid',
             outf
             )
p = subprocess.Popen(cmdstring, stdin=subprocess.PIPE, shell=False)

for i in range(10):
    im = Image.fromarray(np.uint8(np.random.randn(100,100)))
    p.stdin.write(im.tostring('jpeg','L'))
    #p.communicate(im.tostring('jpeg','L'))

p.stdin.close()
otterb
quelle
1

Hier ist eine modifizierte Version von @tacaswells Antwort. Folgendes geändert:

  1. Benötigen Sie nicht die pylabAbhängigkeit
  2. Fix mehrere Stellen, an denen diese Funktion direkt ausgeführt werden kann. (Das Original kann nicht direkt kopiert und eingefügt und ausgeführt werden und muss mehrere Stellen reparieren.)

Vielen Dank für die wundervolle Antwort von @tacaswell !!!

def ani_frame():
    def gen_frame():
        return np.random.rand(300, 300)

    fig = plt.figure()
    ax = fig.add_subplot(111)
    ax.set_aspect('equal')
    ax.get_xaxis().set_visible(False)
    ax.get_yaxis().set_visible(False)

    im = ax.imshow(gen_frame(), cmap='gray', interpolation='nearest')
    im.set_clim([0, 1])
    fig.set_size_inches([5, 5])

    plt.tight_layout()

    def update_img(n):
        tmp = gen_frame()
        im.set_data(tmp)
        return im

    # legend(loc=0)
    ani = animation.FuncAnimation(fig, update_img, 300, interval=30)
    writer = animation.writers['ffmpeg'](fps=30)

    ani.save('demo.mp4', writer=writer, dpi=72)
    return ani
ch271828n
quelle