从python生成电影而无需将单个帧保存到文件


73

我想根据我在matplotlib中的python脚本中生成的帧创建h264或divx电影。这部电影大约有10万张。

在网络上的示例中[例如 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]如何使用x264 C API将一系列图像编码为H264?

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


我还没有找到一种方法来对当前维护的库执行此操作...(过去我使用pymedia,但是不再对其进行维护,并且不会在我使用的任何系统上构建...)如果有帮助,您可以使用来获得matplotlib图形的RGB缓冲区buffer = fig.canvas.tostring_rgb(),并使用fig.canvas.get_width_height()(或fig.bbox.width等)以像素为单位显示图形的宽度和高度
Joe Kington 2010年

好,谢谢。这很有用。我想知道是否可以将一些缓冲区转换传递给ffmpeg。pyffmpeg具有完善的Cython包装器,最近进行了更新,可逐帧读取avi。但不写作。对于熟悉ffmpeg库的人来说,这似乎是一个起点。甚至像matlab的im2frame之类的东西也很棒。
保罗2010年

1
我正在尝试让ffmpeg从输入管道(带有-f image2pipe选项,以便它期望一系列图像)或本地套接字(例如udp://localhost:some_port)中读取并以python写入套接字...部分成功...我感觉自己快要到了,但是...我对ffmpeg不够了解...
Joe Kington 2010年

2
就其价值而言,我的问题是由于ffmpeg接受.png或原始RGB缓冲区流的问题引起的(已经存在一个错误:roundup.ffmpeg.org/issue1854),如果您使用jpeg,它可以工作。(使用ffmpeg -f image2pipe -vcodec mjpeg -i - ouput.whatever。您可以打开一个,subprocess.Popen(cmdstring.split(), stdin=subprocess.PIPE)然后将每个框架都写入其中。stdin)如果有机会,我将发布更详细的示例……
Joe Kington 2010年

那很棒!我明天试试。
保罗2010年

Answers:


56

现在,该功能(至少从1.2.0起,可能从1.1起)通过MovieWriter类及其animation模块中的子类引入了matplotlib中。您还需要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


有没有办法记录某些轴而不是整个图形?特别是,带有FFMpegFileWriter
亚历克斯

@Alex否,您可以在其中保存框架的范围是Figure范围(savefig)也是如此。
tacaswell'1

22

修补ffmpeg之后(请参阅Joe Kington对我的问题的评论),我能够将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')

没有补丁,它将无法正常工作,因为它会修改两个文件并添加libavcodec/png_parser.c。我必须手动将修补程序应用于libavcodec/Makefile。最后,我从中删除了“ -number”Makefile来构建手册页。使用编译选项

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

做得很好!+1(我从来没有能够得到的ffmpeg接受png格式的流,我想我需要更新我的版本的ffmpeg ...)和,只是你不知道的情况下,这完全可以接受的,以纪念你的答案您问题的答案。看到这里的讨论:meta.stackexchange.com/questions/17845/...
乔金顿

1
嗨@Paul,补丁链接已死。您知道它是否已被吸收到主分支中吗?如果没有,有什么办法可以得到那个补丁?
加布

@Gabe,我想该补丁已从以下帖子中吸收:superuser.com/questions/426193/…–
Paul

@tcaswell,我将答案更改为您的答案(我不知道那是可能的。)您能进行所需的编辑吗?
保罗,

我的意思是让您编辑问题以反映新功能,但这是可行的。我已撤消编辑。您对事物的状态感到满意吗?
塔卡斯韦尔

14

转换为图像格式非常慢,并且增加了依赖性。看完这些页面和其他页面后,我使用了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()

我得到了一些不错的加速。


我为ffmpeg修改了它,如果您仍然想要它,请参见下面的答案
cxrodgers 2015年

10

这些都是非常好的答案。这是另一个建议。@ 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
# /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()

这是我使用的一种真正干净的方法。要使其从脚本运行,您需要执行几个修改。在脚本顶部的第一行,添加以下内容:import matplotlib然后将后端设置为matplotlib.use('agg', warn = False, force = True)唯一的mod是plt.draw()在上面的原始代码中替换为f.canvas.draw()这些,才能使其在脚本中工作。否则,代码就是原样。
JHarchanko

5

这很棒!我也想这样做。但是,我永远无法使用MingW32 + MSYS + pr环境在Vista中编译修补的ffmpeg源(0.6.1)... png_parser.c在编译过程中产生了Error1。

因此,我想出了一个使用PIL的jpeg解决方案。只需将ffmpeg.exe放在与此脚本相同的文件夹中即可。在Windows下,没有补丁的ffmpeg应该可以使用。我必须使用stdin.write方法,而不是在有关子进程的官方文档中推荐的communication方法。请注意,第二个-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()

1

这是@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
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.