web-dev-qa-db-fra.com

Générer un film à partir de python sans enregistrer des images individuelles dans des fichiers

Je voudrais créer un film h264 ou divx à partir d'images que je génère dans un script python dans matplotlib. Il y a environ 100k images dans ce film.

Dans des exemples sur le Web [par exemple. 1], je n'ai vu que la méthode pour enregistrer chaque image au format png et ensuite exécuter mencoder ou ffmpeg sur ces fichiers. Dans mon cas, l'enregistrement de chaque image n'est pas pratique. Existe-t-il un moyen de prendre un tracé généré à partir de matplotlib et de le diriger directement vers ffmpeg, sans générer de fichiers intermédiaires?

La programmation avec le C-api de ffmpeg est trop difficile pour moi [par exemple. 2]. De plus, j'ai besoin d'un encodage qui a une bonne compression tel que x264 car le fichier vidéo sera sinon trop volumineux pour une étape ultérieure. Il serait donc formidable de rester avec mencoder/ffmpeg/x264.

Peut-on faire quelque chose avec des tuyaux [3]?

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

[2] Comment coder une série d'images en H264 en utilisant l'API x264 C?

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

65
Paul

Cette fonctionnalité est maintenant (au moins à partir de 1.2.0, peut-être 1.1) intégrée dans matplotlib via la classe MovieWriter et ses 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

50
tacaswell

Après avoir corrigé ffmpeg (voir les commentaires de Joe Kington sur ma question), j'ai pu obtenir des fichiers PNG de tuyauterie pour 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')

Cela ne fonctionnerait pas sans le patch , qui modifie trivialement deux fichiers et ajoute libavcodec/png_parser.c. J'ai dû appliquer manuellement le patch à libavcodec/Makefile. Enfin, j'ai supprimé '-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
Paul

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

Détails sur: 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 eu quelques accélérations de Nice.

13
user621442

Ce sont toutes de très bonnes réponses. Voici une autre suggestion. @ user621442 a raison: le goulot d'étranglement est généralement l'écriture de l'image, donc si vous écrivez des fichiers png sur votre compresseur vidéo, cela sera assez lent (même si vous les envoyez via un tube 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.

De plus, dans mon cas, je voulais simplement enregistrer l'image dans un axe, au lieu d'enregistrer toutes les étiquettes de coche, le titre de la figure, l'arrière-plan de la figure, etc. En gros, je voulais faire un film/animation en utilisant le code matplotlib, mais pas il "ressemble à un graphique". J'ai inclus ce code ici, mais vous pouvez créer des graphiques standard et les diriger vers ffmpeg à la place si vous le souhaitez.

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()
8
cxrodgers

C'est bien! Je voulais faire de même. Mais, je n'ai jamais pu compiler la source ffmpeg corrigée (0.6.1) dans Vista avec l'environnement MingW32 + MSYS + pr ... png_parser.c a produit l'erreur 1 lors de la compilation.

J'ai donc trouvé une solution jpeg pour cela en utilisant PIL. Placez simplement votre ffmpeg.exe dans le même dossier que ce script. Cela devrait fonctionner avec ffmpeg sans le correctif sous Windows. J'ai dû utiliser la méthode stdin.write plutôt que la méthode communic qui est recommandée dans la documentation officielle sur le sous-processus. Notez que l'option 2nd -vcodec spécifie le codec de codage. Le canal est fermé par p.stdin.close ().

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
otterb

Voici une version modifiée de la réponse de @tacaswell. Modifié les éléments suivants:

  1. Ne nécessite pas la dépendance pylab
  2. Fixer plusieurs endroits s.t. cette fonction est directement exécutable. (L'original ne peut pas être copié-collé et exécuté directement et doit corriger plusieurs emplacements.)

Merci beaucoup pour la merveilleuse réponse de @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
0
fzyzcjy