【问题标题】:Speedup matplotlib animation to video file将 matplotlib 动画加速到视频文件
【发布时间】:2015-09-07 01:01:58
【问题描述】:

在 Raspbian (Raspberry Pi 2) 上,从我的脚本中删除的以下最小示例正确地生成了一个 mp4 文件:

import numpy as np
import matplotlib.pyplot as plt
from matplotlib import animation

def anim_lift(x, y):

    #set up the figure
    fig = plt.figure(figsize=(15, 9))

    def animate(i):
        # update plot
        pointplot.set_data(x[i], y[i])

        return  pointplot

    # First frame
    ax0 = plt.plot(x,y)
    pointplot, = ax0.plot(x[0], y[0], 'or')

    anim = animation.FuncAnimation(fig, animate, repeat = False,
                                   frames=range(1,len(x)), 
                                   interval=200,
                                   blit=True, repeat_delay=1000)

    anim.save('out.mp4')
    plt.close(fig)

# Number of frames
nframes = 200

# Generate data
x = np.linspace(0, 100, num=nframes)
y = np.random.random_sample(np.size(x))

anim_lift(x, y)

现在,制作的文件质量很好,而且文件大小非常小,但是制作 170 帧的电影需要 15 分钟,这对我的应用程序来说是不可接受的。我正在寻找显着的加速,视频文件大小增加不是问题。

我认为视频制作的瓶颈在于将帧临时保存为 png 格式。在处理过程中,我可以看到 png 文件出现在我的工作目录中,CPU 负载仅为 25%。

请提出一个解决方案,该解决方案也可能基于不同的包,而不是简单的 matplotlib.animation,例如 OpenCV(无论如何已经导入到我的项目中)或 moviepy

正在使用的版本:

  • python 2.7.3
  • matplotlib 1.1.1rc2
  • ffmpeg 0.8.17-6:0.8.17-1+rpi1

【问题讨论】:

    标签: python matplotlib ffmpeg raspberry-pi raspbian


    【解决方案1】:

    Matplotlib 3.4 更新: 以下解决方案可以适应最新的 matplotlib 版本。 但是,自从首次编写此答案以来,性能似乎有了重大改进,并且 matplotlib 的 FFMpegWriter 的速度现在与此解决方案的作者相似。

    原答案: 将动画保存到文件的瓶颈在于figure.savefig()的使用。这是 matplotlib 的 FFMpegWriter 的自制子类,灵感来自 gaggio 的回答。它不使用savefig(因此忽略savefig_kwargs),但无论您的动画脚本是什么,都需要进行最少的更改。

    对于matplotlib
    from matplotlib.animation import FFMpegWriter
    
    class FasterFFMpegWriter(FFMpegWriter):
        '''FFMpeg-pipe writer bypassing figure.savefig.'''
        def __init__(self, **kwargs):
            '''Initialize the Writer object and sets the default frame_format.'''
            super().__init__(**kwargs)
            self.frame_format = 'argb'
    
        def grab_frame(self, **savefig_kwargs):
            '''Grab the image information from the figure and save as a movie frame.
    
            Doesn't use savefig to be faster: savefig_kwargs will be ignored.
            '''
            try:
                # re-adjust the figure size and dpi in case it has been changed by the
                # user.  We must ensure that every frame is the same size or
                # the movie will not save correctly.
                self.fig.set_size_inches(self._w, self._h)
                self.fig.set_dpi(self.dpi)
                # Draw and save the frame as an argb string to the pipe sink
                self.fig.canvas.draw()
                self._frame_sink().write(self.fig.canvas.tostring_argb()) 
            except (RuntimeError, IOError) as e:
                out, err = self._proc.communicate()
                raise IOError('Error saving animation to file (cause: {0}) '
                          'Stdout: {1} StdError: {2}. It may help to re-run '
                          'with --verbose-debug.'.format(e, out, err)) 
    

    与使用默认 FFMpegWriter 相比,我能够用一半或更少的时间创建动画。

    您可以按照this example 中的说明使用。

    对于 matplotlib >= 3.4

    如果您将try 块的最后一行更改为:

    ,上述代码将适用于 matplotlib 3.4 及更高版本
    self._proc.stdin.write(self.fig.canvas.tostring_argb())
    

    即使用_proc.stdin 而不是_frame_sink()

    【讨论】:

    • 这个答案是否仍然是最新的?我试图让它与当前的 matplotlib 版本一起使用,但 animation.py 自 2018 年以来显然已经发生了变化,例如_frame_sink() 不再存在。有什么建议可以在今天实施吗?
    • 我现在没有时间或设置来尝试这个,但我在 matplotlib 代码中看到对 self._frame_sink() 的调用被 self._proc.stdin 简单地替换了。你可以试试。编辑:如果我有时间,我会在下周尝试更新答案。
    【解决方案2】:

    一个大大改进的解决方案是基于对this post 的回答,将时间减少了大约 10 倍。

    import numpy as np
    import matplotlib.pylab as plt
    import matplotlib.animation as animation
    import subprocess
    
    def testSubprocess(x, y):
    
        #set up the figure
        fig = plt.figure(figsize=(15, 9))
        canvas_width, canvas_height = fig.canvas.get_width_height()
    
        # First frame
        ax0 = plt.plot(x,y)
        pointplot, = plt.plot(x[0], y[0], 'or')
    
        def update(frame):
            # your matplotlib code goes here
            pointplot.set_data(x[frame],y[frame])
    
        # Open an ffmpeg process
        outf = 'testSubprocess.mp4'
        cmdstring = ('ffmpeg', 
                     '-y', '-r', '1', # overwrite, 1fps
                     '-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 frames and write to the pipe
        for frame in range(nframes):
            # draw the frame
            update(frame)
            fig.canvas.draw()
    
            # extract the image as an ARGB string
            string = fig.canvas.tostring_argb()
    
            # write to pipe
            p.stdin.write(string)
    
        # Finish up
        p.communicate()
    
    # Number of frames
    nframes = 200
    
    # Generate data
    x = np.linspace(0, 100, num=nframes)
    y = np.random.random_sample(np.size(x))
    
    testSubprocess(x, y)
    

    我怀疑通过将原始图像数据传输到 gstreamer 可以类似地获得进一步的加速,现在可以从 Raspberry Pi 上的硬件编码中受益,请参阅 this discussion

    【讨论】:

    • 这非常有帮助。 writer 类使用fig.savefig() 保存到管道,这非常慢。
    【解决方案3】:

    您应该可以使用其中一位可以直接流式传输到 ffmpeg 的编写器,但其他一些事情会非常错误。

    import matplotlib.pyplot as plt
    from matplotlib import animation
    
    
    def anim_lift(x, y):
    
        #set up the figure
        fig, ax = plt.subplots(figsize=(15, 9))
    
        def animate(i):
            # update plot
            pointplot.set_data(x[i], y[i])
    
            return [pointplot, ]
    
        # First frame
        pointplot, = ax.plot(x[0], y[0], 'or')
        ax.set_xlim([0, 200])
        ax.set_ylim([0, 200])
        anim = animation.FuncAnimation(fig, animate, repeat = False,
                                       frames=range(1,len(x)),
                                       interval=200,
                                       blit=True, repeat_delay=1000)
    
        anim.save('out.mp4')
        plt.close(fig)
    
    
    x = list(range(170))
    y = list(range(170))
    anim_lift(x, y)
    

    将其保存为 test.py(这是您的代码的清理版本,我认为它实际上不会运行,因为 plt.plot 返回 line2D 对象的列表并且列表没有 plot 方法)给出:

    (dd_py3k) ✔ /tmp 
    14:45 $ time python test.py
    
    real    0m7.724s
    user    0m9.887s
    sys     0m0.547s
    

    【讨论】:

    • 感谢您的更正,实际上我的版本运行感谢pointplot[0],但您的版本更干净,我会相应地编辑原始版本。
    • 不管怎样,速度的问题依然存在……你注意到你的程序是否在工作目录中生成了临时的png文件吗?而且我假设您的版本也在树莓派 2、树莓派发行版上运行?也许我原来的问题还不够清楚。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2016-04-23
    • 2013-07-08
    • 2015-09-07
    • 1970-01-01
    • 2020-09-14
    • 2021-12-16
    • 2018-09-17
    相关资源
    最近更新 更多