从 python 生成电影而不将单个帧保存到文件
Posted
技术标签:
【中文标题】从 python 生成电影而不将单个帧保存到文件【英文标题】:Generating movie from python without saving individual frames to files 【发布时间】:2011-05-04 19:45:36 【问题描述】:我想根据我在 matplotlib 中的 python 脚本中生成的帧创建一个 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]How does one encode a series of images into H264 using the x264 C API?
[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
等)
好的,谢谢。这很有用。我想知道缓冲区的某些转换是否可以通过管道传输到 ffmpeg。 pyffmpeg 有一个复杂的 Cython 包装器,最近更新,用于逐帧读取 avi。但不会写。对于熟悉 ffmpeg 库的人来说,这听起来像是一个可能的起点。即使像 matlab 的 im2frame 这样的东西也会很棒。
我正在尝试让 ffmpeg 从输入管道(使用 -f image2pipe
选项,以便它需要一系列图像)或本地套接字(例如 udp://localhost:some_port
)读取并在python中写入套接字......到目前为止,只有部分成功......我觉得我快到了,虽然......我只是对ffmpeg不够熟悉......
对于它的价值,我的问题是由于 ffmpeg 接受 .png 流或原始 RGB 缓冲区的问题,(已经提交了一个错误:roundup.ffmpeg.org/issue1854)如果你使用 jpegs 它可以工作. (使用ffmpeg -f image2pipe -vcodec mjpeg -i - ouput.whatever
。你可以打开一个subprocess.Popen(cmdstring.split(), stdin=subprocess.PIPE)
并将每一帧写入它的stdin
)如果有机会我会发布一个更详细的例子......
太好了!我明天试试这个。
【参考方案1】:
修补 ffmpeg 后(请参阅 Joe Kington cmets 到我的问题),我能够将 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
。最后,我从Makefile
中删除了“-number”,以便构建手册页。使用编译选项,
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/… 嗨@Paul,补丁链接已失效。你知道它是否已经被吸收到主分支中了吗?如果没有,有什么办法可以得到那个补丁? @Gabe,我猜这个补丁已经被以下帖子吸收了:superuser.com/questions/426193/… @tcaswell,我把答案改成了你的答案(我不知道这是可能的。)你能做必要的编辑吗? 我的意思是让您编辑您的问题以反映新功能,但这有效。我已经回滚了我的编辑。您对现状满意吗?【参考方案2】:这太棒了!我也想做同样的事情。但是,我永远无法使用 MingW32+MSYS+pr 环境在 Vista 中编译修补的 ffmpeg 源代码(0.6.1)... png_parser.c 在编译过程中产生了 Error1。
所以,我想出了一个使用 PIL 的 jpeg 解决方案。只需将您的 ffmpeg.exe 放在与此脚本相同的文件夹中。这应该适用于没有 Windows 下的补丁的 ffmpeg。我不得不使用 stdin.write 方法,而不是在关于子进程的官方文档中推荐的通信方法。请注意,第二个 -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()
【讨论】:
【参考方案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()
我得到了一些不错的加速。
【讨论】:
我为 ffmpeg 修改了这个,如果你仍然想要它,请参阅下面的答案【参考方案4】:这个功能现在(至少从 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
Documentation for animation
【讨论】:
有没有办法记录某些轴,而不是整个图形?特别是FFMpegFileWriter
?
@Alex 不,可以保存帧的范围是 Figure 范围(savefig
也是如此)。【参考方案5】:
这些都是非常好的答案。这是另一个建议。 @user621442 是正确的,瓶颈通常是图像的写入,因此如果您将 png 文件写入视频压缩器,它会非常慢(即使您通过管道发送它们而不是写入磁盘)。我找到了一个使用纯 ffmpeg 的解决方案,我个人觉得它比 matplotlib.animation 或 mencoder 更易于使用。
另外,就我而言,我只想将图像保存在轴中,而不是保存所有刻度标签、图形标题、图形背景等。基本上我想使用 matplotlib 代码制作电影/动画,但不要让它“看起来像一个图表”。我在此处包含了that code,但如果您愿意,您可以制作标准图表并将它们通过管道传输到 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
# https://***.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()
【讨论】:
这是一种非常干净的方式,也是我使用的方式。要让它从脚本运行,你需要做几个模组。在脚本的顶部,第一行,添加以下内容:import matplotlib
然后将后端设置为matplotlib.use('agg', warn = False, force = True)
唯一的另一个模块是将上面原始代码中的plt.draw()
替换为f.canvas.draw()
这些是必要的它在脚本中工作。否则,代码只是花花公子。【参考方案6】:
这是@tacaswell 答案的修改版本。修改如下:
-
不需要
pylab
依赖项
修复几个地方 s.t.这个函数可以直接运行。 (原来的不能直接复制粘贴运行,要修复好几个地方。)
非常感谢@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
【讨论】:
以上是关于从 python 生成电影而不将单个帧保存到文件的主要内容,如果未能解决你的问题,请参考以下文章
将 python 对象上传到 Google Cloud Storage 而不将其保存到文件