Создание фильма из 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. Изменено следующее:
- Не требует зависимости от
pylab
- Исправить несколько мест, чтобы эта функция работала напрямую. (Оригинал не может быть скопирован и вставлен и запущен напрямую и должен исправить несколько мест.)
Большое спасибо за прекрасный ответ @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