Создание фильма из python без сохранения отдельных кадров в файлы

Я хотел бы создать h264 или DivX фильм из кадров, которые я генерирую в сценарии Python в Matplotlib. В этом фильме около 100 тыс. Кадров.

В примерах в Интернете [например. 1], я видел только способ сохранения каждого кадра в формате png и последующего запуска mencoder или ffmpeg для этих файлов. В моем случае сохранение каждого кадра нецелесообразно. Есть ли способ взять график, сгенерированный из matplotlib, и направить его напрямую в ffmpeg, не создавая промежуточных файлов?

Программирование с помощью ffmpeg C-api слишком сложно для меня [например, 2]. Кроме того, мне нужна кодировка с хорошим сжатием, такая как x264, поскольку файл фильма в противном случае будет слишком большим для следующего шага. Так что было бы здорово придерживаться mencoder/ffmpeg/x264.

Есть ли что-то, что можно сделать с помощью труб [3]?

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

[2] Как можно кодировать серию изображений в H264 с помощью x264 C API?

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

Ответ 1

Эта функциональность теперь (по крайней мере, начиная с 1.2.0, может быть, 1.1) включается в matplotlib через класс MovieWriter и его подклассы в модуле animation. Вам также необходимо установить ffmpeg заранее.

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

Документация для animation

Ответ 2

После исправления ffmpeg (см. комментарии Джо Кингтона к моему вопросу), мне удалось получить ping png для ffmpeg следующим образом:

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

Он не будет работать без patch, который тривиально изменяет два файла и добавляет libavcodec/png_parser.c. Мне пришлось вручную применить патч к libavcodec/Makefile. Наконец, я удалил '-number' из Makefile, чтобы собрать страницы man. С параметрами компиляции

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

Ответ 3

Преобразование в форматы изображений довольно медленно и добавляет зависимости. Посмотрев на эту страницу и другие, я получил ее работу с использованием необработанных необработанных буферов с использованием mencoder (решение ffmpeg все еще было необходимо).

Подробности: 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()

У меня получилось приятное ускорение.

Ответ 4

Все это действительно отличные ответы. Вот еще одно предложение. @user621442 правильно, что узким местом обычно является запись изображения, поэтому, если вы пишете png файлы на ваш видеокомпрессор, это будет довольно медленным (даже если вы отправляете их через трубу вместо записи на диск). Я нашел решение с использованием чистого ffmpeg, которое мне лично проще, чем matplotlib.animation или mencoder.

Кроме того, в моем случае я хотел просто сохранить изображение по оси, вместо сохранения всех ярлыков меток, названия фигур, фона рисунка и т.д. В принципе, я хотел сделать фильм/анимацию с использованием кода matplotlib, но не иметь его "похожий на график". Я включил этот код здесь, но вы можете сделать стандартные графики и поместить их в ffmpeg, если хотите.

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
# /questions/44150/how-to-remove-frame-from-matplotlib-pyplotfigure-vs-matplotlibfigure-frameon-false-problematic-in-matplotlib
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()

Ответ 5

Это здорово! Я хотел сделать то же самое. Но я никогда не смог бы скомпилировать исправленный источник ffmpeg (0.6.1) в Vista с MingW32 + MSYS + pr enviroment... png_parser.c создал Error1 во время компиляции.

Итак, я придумал решение jpeg для этого, используя PIL. Просто поместите файл ffmpeg.exe в ту же папку, что и этот script. Это должно работать с ffmpeg без патча под Windows. Мне пришлось использовать метод stdin.write, а не метод связи, который рекомендуется в официальной документации о подпроцессе. Обратите внимание, что опция 2nd -vcodec указывает кодирующий кодек. Труба закрыта 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()

Ответ 6

Вот модифицированная версия ответа @tacaswell. Изменено следующее:

  1. Не требует зависимости от pylab
  2. Исправить несколько мест, чтобы эта функция работала напрямую. (Оригинал не может быть скопирован и вставлен и запущен напрямую и должен исправить несколько мест.)

Большое спасибо за прекрасный ответ @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