使用Python生成电影而不将单帧保存到文件中。

78
我想从在matplotlib中生成的帧中创建一个h264或divx电影,这个电影大约有100k帧。在网络上的示例[例如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,但它不再维护,并且在我使用的任何系统上都无法构建...)如果有帮助的话,您可以通过使用 buffer = fig.canvas.tostring_rgb() 获取matplotlib图形的RGB缓冲区,以及图形的像素宽度和高度 fig.canvas.get_width_height() (或 fig.bbox.width等) - Joe Kington
好的,谢谢。这很有用。我想知道是否可以将缓冲区的某些转换管道传输到ffmpeg。pyffmpeg具有复杂的Cython包装器,最近更新,用于逐帧读取avi。但是不支持写入。对于熟悉ffmpeg库的人来说,这听起来像一个可能的起点。即使是类似matlab的im2frame也很棒。 - Paul
1
我正在尝试让ffmpeg从输入管道(使用-f image2pipe选项,以便它期望一系列图像)或本地套接字(例如udp://localhost:some_port)读取,并在Python中将其写入套接字...到目前为止,只有部分成功...虽然我感觉我已经接近成功了...但我对ffmpeg还不够熟悉... - Joe Kington
2
就我个人而言,我的问题是由于ffmpeg接受.png或原始RGB缓冲区流的问题引起的,(已经有一个错误报告:https://roundup.ffmpeg.org/issue1854)。 如果您使用jpeg,则可以正常工作。(使用ffmpeg-f image2pipe-vcodec mjpeg-i-output.whatever。您可以打开subprocess.Popen(cmdstring.split(),stdin = subprocess.PIPE)并将每个帧写入其stdin)如果我有机会,我会发布更详细的示例... - Joe Kington
作为注释,这现在已经整合到 matplotlib 中了(请参见下面的答案)。 - tacaswell
6个回答

58

这个功能现在已经整合到matplotlib中(至少从1.2.0开始,可能1.1也有),通过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


有没有办法记录某些轴,而不是整个图形?特别是使用 FFMpegFileWriter - Alex
@Alex 不,你可以保存帧的范围是图形范围(savefig也是如此)。 - tacaswell

23

在修补了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。最后,我从Makefile中删除了“-number”以使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

干得好!+1(我从来没能让ffmpeg接受.png流,我想我需要更新我的ffmpeg版本...)而且,如果你在想,将你的答案标记为问题的答案是完全可以接受的。请参见此处的讨论:http://meta.stackexchange.com/questions/17845/stack-overflow-etiquette-for-answering-your-own-question - Joe Kington
1
嗨@Paul,补丁链接已失效。你知道它是否已被合并到主分支中吗?如果没有,有什么方法可以获取该补丁吗? - Gabe
@Gabe,我猜测这个补丁已经从以下帖子中吸收了:http://superuser.com/questions/426193/multiple-png-images-over-a-single-pipe-to-ffmpeg-no-file-writes-to-disk - Paul
@tcaswell,我改成了你的答案(我不知道那是可能的)。你能否请进行必要的编辑? - Paul
我想表达的是让您编辑您的问题以反映新的功能,但这也可以。我已经回滚了我的修改。您对事情的状态满意吗? - tacaswell
我明白了。好的,现在看起来很不错。任何试图解决这个问题的人都会被引导到你的答案,而不是试图修补ffmpeg。感谢你的解决方案。 - Paul

16

将图像转换为格式相当缓慢且需要添加依赖项。在查看了这些页面和其他页面后,我使用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

15
这些都是非常好的回答。这里有一个建议。@user621442 正确指出瓶颈通常是图像的编写,因此如果您将 png 文件写入视频压缩器,它会变得非常慢(即使您通过管道发送它们而不是写入磁盘)。我发现使用纯 ffmpeg 的解决方案比使用 matplotlib.animation 或 mencoder 更容易。
此外,在我的情况下,我只想在轴中保存图像,而不是保存所有刻度标签、图形标题、图形背景等。基本上,我想使用 matplotlib 代码制作电影/动画,但不希望它“看起来像一个图表”。我在这里包含了 该代码,但如果您想要,也可以制作标准图表并将其导出到 ffmpeg。
import matplotlib
matplotlib.use('agg', warn = False, force = True)

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://dev59.com/sGUp5IYBdhLWcg3w-LQJ
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()

3
这是一种非常干净的方法,也是我所使用的方法。如果要从脚本中运行它,你需要进行一些修改。在脚本的顶部第一行,添加以下内容:import matplotlib ,然后使用 matplotlib.use('agg', warn = False, force = True) 来设置后端。另一个修改是将原始代码中的 plt.draw() 替换为 f.canvas.draw() 。这些都是使它在脚本中工作所必需的。除此之外,代码就和原来一样好。 - JHarchanko
@JodyKlymak 你能分享一下为什么需要添加 matplotlib.use(..) 这行代码吗?我可以理解 'agg' 可以节省处理时间,但我不会禁用警告。但是我也从未使用过 'agg',所以我不确定。 - cxrodgers
其他后端的画布对象可以根据画布的dpiRatio进行缩放。 - Jody Klymak

6
这很棒!我也想做同样的事情。但是,我无法在Vista上使用MingW32+MSYS+pr环境编译已打补丁的ffmpeg源代码(0.6.1),png_parser.c在编译过程中产生了Error1错误。
所以,我想到了使用PIL来解决这个问题。只需将您的ffmpeg.exe放在与此脚本相同的文件夹中即可。这应该适用于Windows下没有打补丁的ffmpeg。我不得不使用stdin.write方法而不是官方文档中有关子进程的推荐通信方法communicate。请注意,第二个-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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接