Générer des films à partir de python sans sauvegarder des cadres individuels dans des fichiers

j'aimerais créer un film h264 ou divx à partir de cadres que je génère dans un script python en matplotlib. Il y a environ 100k images dans ce film.

Dans les exemples sur le web [par exemple. 1], Je n'ai vu que la méthode pour sauvegarder chaque image en png et ensuite lancer mencoder ou ffmpeg sur ces fichiers. Dans mon cas, sauver chaque image est impraticable. Y a-t-il un moyen de prendre une parcelle générée à partir de matplotlib et de la Piper directement à ffmpeg, ne générant aucun intermédiaire fichiers?

programmer avec l'api C de ffmpeg est trop difficile pour moi [par ex. 2]. En outre, j'ai besoin d'un encodage qui a une bonne compression comme x264 car le fichier du film sera autrement trop grand pour une étape ultérieure. Ce serait donc bien de s'en tenir à mencoder/ffmpeg/x264.

y a-t-il quelque chose à faire avec les pipes [3]?

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

[2] comment encoder une série d'images dans H264 en utilisant l'API x264 C?

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

60
demandé sur Community 2010-11-04 03:30:21

5 réponses

cette fonctionnalité est maintenant (au moins à partir de 1.2.0, peut-être 1.1) cuite dans matplotlib via la classe MovieWriter et il est sous-classes dans le module animation . Vous devez également installer ffmpeg à l'avance.

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

Documentation pour animation

46
répondu tacaswell 2017-11-26 19:35:34

après avoir corrigé la ffmpeg (voir les commentaires de Joe Kington à ma question), j'ai pu faire passer les png à la ffmpeg comme suit:

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')

il ne fonctionnerait pas sans le patch , qui modifie trivialement deux fichiers et ajoute libavcodec/png_parser.c . J'ai dû appliquer manuellement le patch sur libavcodec/Makefile . Enfin, j'ai enlevé '- number ' de Makefile pour obtenir les pages de manuel à construire. Avec les options de compilation,

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
20
répondu Paul 2013-08-13 18:43:54

la conversion aux formats d'image est assez lente et ajoute des dépendances. Après avoir regardé cette page et d'autres, je l'ai fait fonctionner en utilisant des tampons bruts non codés en utilisant mencoder (solution ffmpeg toujours désirée).

détails à: 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()

j'ai de belles vitesses.

11
répondu user621442 2011-12-16 12:28:47

ce sont de très bonnes réponses. Voici une autre suggestion. @user621442 est correct que le goulot d'étranglement est typiquement l'écriture de l'image, donc si vous écrivez des fichiers png à votre compresseur vidéo, il sera assez lent (même si vous les envoyez par un tuyau au lieu d'écrire sur le disque). J'ai trouvé une solution en utilisant ffmpeg pur, que je trouve personnellement plus facile à utiliser que matplotlib.animation ou mencoder.

Aussi, dans mon cas, je voulais juste sauver l'image dans un axe, au lieu de sauvegarder toutes les étiquettes, le titre de la figure, le fond de la figure, etc. Fondamentalement, je voulais faire un film / animation en utilisant le code matplotlib, mais pas l'avoir "ressembler à un graphique". J'ai inclus ce code ici, mais vous pouvez faire des graphiques standard et les Piper à ffmpeg à la place si vous voulez.

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
# https://stackoverflow.com/questions/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()
6
répondu cxrodgers 2017-05-23 12:26:21

C'est génial! Je voulais faire la même chose. Mais je n'ai jamais pu compiler la source corrigée ffmpeg (0.6.1) dans Vista avec MingW32+MSYS+pr environment... png_parser.c produit Error1 lors de la compilation.

donc, j'ai trouvé une solution jpeg à cela en utilisant PIL. Mettez juste votre ffmpeg.exe dans le même dossier que le script. Cela devrait fonctionner avec ffmpeg sans le patch sous Windows. J'ai dû utiliser l'entrée standard.méthode d'écriture plutôt que la méthode de communication qui est recommandé dans la documentation officielle sur le sous-processus. Notez que l'option 2nd-vcodec spécifie le codec d'encodage. Le tube est fermé par p.stdin.proche.)(

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()
5
répondu otterb 2011-01-14 18:46:15