matplotlib animation动画保存(save函数)详解

本文主要介绍matplotlib中animation如何保存动画,从matplotlib的一些基础代码说起,并在最后附上了解决save()函数报错的代码,其中的一些代码涉及到__getitem__()方法和注解修饰的知识,如果没有了解的朋友希望先去查一下相关的知识了解一下

一些介绍


rcParams

我们知道matplotlib函数绘制时如果不指定参数,会使用一系列的默认值去绘制图像,这些默认值保存在matplotlib,rcParams中,以字典的形式保存,这其中,设计到animation部分的有一下部分

RcParams({'_internal.classic_mode': False,
          'agg.path.chunksize': 0,
          'animation.avconv_args': [],
          'animation.avconv_path': 'avconv',
          'animation.bitrate': -1,
          'animation.codec': 'h264',
          'animation.convert_args': [],
          'animation.convert_path': '',
          'animation.embed_limit': 20.0,
          'animation.ffmpeg_args': [],
          'animation.ffmpeg_path': 'ffmpeg',
          'animation.frame_format': 'png',
          'animation.html': 'none',
          'animation.html_args': [],
          'animation.mencoder_args': [],
          'animation.mencoder_path': 'mencoder',
          'animation.writer': 'ffmpeg',
          ...

其中最重要的是后面的几行,我们稍后再提


MovieWriter:class for writing movies

在这里先看一下save()函数的参数要求吧

    def save(self, 
             filename, 
             writer=None,  
             fps=None,  
             dpi=None,  
             codec=None,
             bitrate=None,  
             extra_args=None,  
             metadata=None,  
             extra_anim=None,
             savefig_kwargs=None):

这其中最重要的参数是writer,来看一下对writer的要求

writer : :class:MovieWriter or str, optional
A MovieWriter instance to use or a key that identifies a
class to use, such as ‘ffmpeg’ or ‘mencoder’. If None,
defaults to rcParams['animation.writer'].

这里要求writer必须是MovieWriter类或者字符串,详细的同样之后再说,我们要知道的是MovieWriter是一个基类,如果要实现写动画,必须由它的子类来实现


animation.py中的一些代码片段

首先看一下save()函数中对writer的处理

if writer is None:
            writer = rcParams['animation.writer']

如果wirter不指定,那么writer就从matplotlib的默认值中取,翻一下上面的默认值可以看到 rcParams['animation.writer'] = "ffmpeg",也即writer会成为一个指定编码程序的字符串
继续往下:是writer从str到MovieWriter类的一个转变

if isinstance(writer, six.string_types):
     if writer in writers.avail:
         writer = writers[writer](fps,  
                                  codec, bitrate,
                                  extra_args=extra_args,
                                  metadata=metadata)
     else:
         warnings.warn(
             "MovieWriter %s unavailable" % writer)

我们经常报MovieWriter ffmpeg unavailable的错误原因就是在这里了,如果我们不指定writer或者给writer赋的值为str,那么writer就会从writers中找对应的MovieWriter
那么writers又是什么?在animation.py的第174行有定义:

    writers = MovieWriterRegistry()

它是MovieWriterRegistry类建立的一个对象,用于Registry of available writer classes by human readable name.(通过人能够理解的名字注册有用的writer类),在该类的初始化方法里定义了两个空字典,用来存放注册的writer类和相应的名字,代码如下:

class MovieWriterRegistry(object):
    '''Registry of available writer classes by human readable name.'''
    def __init__(self):
        self.avail = dict()
        self._registered = dict()
        self._dirty = False

我们看到之前writer类是从writers[writer]中取出来,MovieWriterRegistry中定义了__getitem__()方法,writers[writer]实际上返回的是self.avail[writer]

def __getitem__(self, name):
    self.ensure_not_dirty()
    if not self.avail:
        raise RuntimeError("No MovieWriters available!")
    return self.avail[name]

self.avail是什么时候往里面添加元素的?是通过注解
看一下以下几个MovieWriter类的子类的定义吧:(未列举全)

@writers.register('ffmpeg')
class FFMpegWriter(FFMpegBase, MovieWriter):
    '''Pipe-based ffmpeg writer.

    Frames are streamed directly to ffmpeg via a pipe and written in a single
    pass.
    '''
    def _args(self):
        # Returns the command line parameters for subprocess to use
        # ffmpeg to create a movie using a pipe.
        args = [self.bin_path(), '-f', 'rawvideo', '-vcodec', 'rawvideo',
                '-s', '%dx%d' % self.frame_size, '-pix_fmt', self.frame_format,
                '-r', str(self.fps)]
        # Logging is quieted because subprocess.PIPE has limited buffer size.
        if not verbose.ge('debug'):
            args += ['-loglevel', 'quiet']
        args += ['-i', 'pipe:'] + self.output_args
        return args

@writers.register('ffmpeg_file')
class FFMpegFileWriter(FFMpegBase, FileMovieWriter):
    '''File-based ffmpeg writer.

    Frames are written to temporary files on disk and then stitched
    together at the end.

    '''
    supported_formats = ['png', 'jpeg', 'ppm', 'tiff', 'sgi', 'bmp',
                         'pbm', 'raw', 'rgba']

    def _args(self):
        # Returns the command line parameters for subprocess to use
        # ffmpeg to create a movie using a collection of temp images
        return [self.bin_path(), '-r', str(self.fps),
                '-i', self._base_temp_name(),
                '-vframes', str(self._frame_counter)] + self.output_args

@writers.register('avconv')
class AVConvWriter(AVConvBase, FFMpegWriter):
    '''Pipe-based avconv writer.

    Frames are streamed directly to avconv via a pipe and written in a single
    pass.
    '''

这下逻辑就明了了,在定义MovieWriter的这些子类的时候,会同时调用writers.register('name')使writers.avail中添加相应的类,在定义之后,如果save()函数的writer参数为空,则转化为字符串,如果是字符串,则从writers.avail中找到相应的类,如果是类,则直接使用该类


save()函数中保存动画的部分

这一块理解了很有意思,而且能够用于你写的代码上,来看一下:

with writer.saving(self._fig, filename, dpi):
    for anim in all_anim:
        # Clear the initial frame
        anim._init_draw()
    for data in zip(*[a.new_saved_frame_seq()
                      for a in all_anim]):
        for anim, d in zip(all_anim, data):
            # TODO: See if turning off blit is really necessary
            anim._draw_next_frame(d, blit=False)
        writer.grab_frame(**savefig_kwargs)

开头with writer.saving(self._fig, filename, dpi):用于开启输送到视频的管道
结尾writer.grab_frame(**savefig_kwargs)由函数名就可以看出来是保存当前figure上画的图像
也就是代码中间是更新figure的代码
然后我们来看一下grab_frame()

def grab_frame(self, **savefig_kwargs):
    '''
    Grab the image information from the figure and save as a movie frame.

    All keyword arguments in savefig_kwargs are passed on to the `savefig`
    command that saves the figure.
    '''
    verbose.report('MovieWriter.grab_frame: Grabbing frame.',
                   level='debug')
    try:
        # re-adjust the figure size 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)
        # Tell the figure to save its data to the sink, using the
        # frame format and dpi.
        self.fig.savefig(self._frame_sink(), format=self.frame_format,
                         dpi=self.dpi, **savefig_kwargs)
    except (RuntimeError, IOError) as e:
        out, err = self._proc.communicate()
        verbose.report('MovieWriter -- Error '
                       'running proc:\n%s\n%s' % (out, err),
                       level='helpful')
        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))

看到中间最关键的代码了吗???

self.fig.savefig(self._frame_sink(), format=self.frame_format,
                         dpi=self.dpi, **savefig_kwargs)

writer依次让figure当前的图像保存到它指定的位置,然后合并为视频。
到这里逻辑基本上明了了,下面我们加快速度
self._frame_sink()是保存的位置,这个方法唯一的用途是返回self._proc.stdin
self._procMovieWriter中定义了

self._proc = subprocess.Popen(command, shell=False,
                                      stdout=output, stderr=output,
                                      stdin=subprocess.PIPE,
                                      creationflags=subprocess_creation_flags)

也即fig保存图像的位置是subprocess.Popen命令开的管道的输入端,而输出端,自然就是视频文件了


重回MovieWriter

我们现在知道了MovieWriter类是怎样获取的了,也知道save()是通过什么方式保存视频的了,我们继续来看一下MovieWriter类
之前给出了MovieWriter类的几个子类的定义,它们都只多实现了一个_args()函数,用来返回什么呢?不难想到是用来返回相应的cmd命令

def _args(self):
    # Returns the command line parameters for subprocess to use
    # ffmpeg to create a movie using a pipe.
    args = [self.bin_path(), '-f', 'rawvideo', '-vcodec', 'rawvideo',
            '-s', '%dx%d' % self.frame_size, '-pix_fmt', self.frame_format,
            '-r', str(self.fps)]
    # Logging is quieted because subprocess.PIPE has limited buffer size.
    if not verbose.ge('debug'):
        args += ['-loglevel', 'quiet']
    args += ['-i', 'pipe:'] + self.output_args
    return args

bin_path()是命令的第一串字符,也就是说,它代表着要运行的程序,对于FFMpegWriter来说,它应该就是ffmpeg,具体是不是,来看一下吧,在MovieWriter类中定义了这个方法

    def bin_path(cls):
        '''
        Returns the binary path to the commandline tool used by a specific
        subclass. This is a class method so that the tool can be looked for
        before making a particular MovieWriter subclass available.
        '''
        return str(rcParams[cls.exec_key])

返回rcParams[cls.exec_key]
exec_key,又在FFMpegBase(FFMpegWriter的父类之一)和其他一些类中定义了

exec_key = 'animation.ffmpeg_path'

回头看看rcParams,rcParams[cls.exec_key]是不是返回的是相应的编码器的名称?

到这里,save()函数可以说里里外外都已经理清楚了,接下来,就是用它得到我们想要的视频了


开始解决

下面开始解决save()函数的各种错误


下载FFmpeg

windows版本:https://ffmpeg.zeranoe.com/builds/
(其余系统请看官网)
点开后下static版本,解压到任意位置,并添加path/ffmpeg/bin到环境变量PATH


输出mp4格式的视频

#anim = animation.ArtistAnimation(fig, ims, interval=interval, repeat_delay=repeat_delay,repeat = repeat,
#                                   blit=True)
writer = animation.FFMpegWriter()
anim.save(fname,writer = writer)

按理说环境变量都配置好了,cmd命令中输入ffmpeg也能显示了,调用应该就没问题了,但我会报以下的错,表明系统没有找到ffmpeg
Exception in Tkinter callback
Traceback (most recent call last):
File "C:\Users\Sailist\AppData\Local\Programs\Python\Python36\lib\tkinter\__init__.py", line 1699, in __call__
return self.func(*args)
File "E:\Python\sort_vision\main_gui.py", line 68, in save_sort
run_sort(True,fname)
File "E:\Python\sort_vision\main_gui.py", line 24, in run_sort
start_sort(to_sort,sort_data,repeat = repeat,repeat_delay=repeat_delay,interval=interval,colors = colors,tosave=tosave,fname = fname,dpi = int(dpi_var.get()))
File "E:\Python\sort_vision\sort_gui.py", line 408, in start_sort
start_save(fname,fig,[im_ani])
File "E:\Python\sort_vision\sort_gui.py", line 439, in start_save
all_anim[0].save(fname,writer = writer)
File "C:\Users\Sailist\AppData\Local\Programs\Python\Python36\lib\site-packages\matplotlib\animation.py", line 1252, in save
with writer.saving(self._fig, filename, dpi):
File "C:\Users\Sailist\AppData\Local\Programs\Python\Python36\lib\contextlib.py", line 81, in __enter__
return next(self.gen)
File "C:\Users\Sailist\AppData\Local\Programs\Python\Python36\lib\site-packages\matplotlib\animation.py", line 233, in saving
self.setup(fig, outfile, dpi, *args, **kwargs)
File "C:\Users\Sailist\AppData\Local\Programs\Python\Python36\lib\site-packages\matplotlib\animation.py", line 349, in setup
self._run()
File "C:\Users\Sailist\AppData\Local\Programs\Python\Python36\lib\site-packages\matplotlib\animation.py", line 366, in _run
creationflags=subprocess_creation_flags)
File "C:\Users\Sailist\AppData\Local\Programs\Python\Python36\lib\subprocess.py", line 709, in __init__
restore_signals, start_new_session)
File "C:\Users\Sailist\AppData\Local\Programs\Python\Python36\lib\subprocess.py", line 998, in _execute_child
startupinfo)
FileNotFoundError: [WinError 2] 系统找不到指定的文件。

因此,应该改用绝对路径表示ffmpeg,上述代码改为:

ffmpegpath = os.path.abspath("./ffmpeg/bin/ffmpeg.exe")
matplotlib.rcParams["animation.ffmpeg_path"] = ffmpegpath
writer = animation.FFMpegWriter()
anim.save(fname,writer = writer)

再次运行程序,输出成功!(我是将ffmpeg放到了程序目录下,保证程序不会出错误,其余的不用我解释了吧?)


animation.py中代码的一个小移植

如果我想讲动画中的图片全都保存为一张张图片怎么办?还记得之前提到的writer处理每一帧动画时候的操作吗?将其简单改改就可以了

 i = 0
 for anim in all_anim:
     anim._init_draw()
 for data in zip(*[a.new_saved_frame_seq() for a in all_anim]):
     for anim, d in zip(all_anim, data):
         anim._draw_next_frame(d, blit=False)
         fig.savefig(fname.replace('index',str(i)),dip = 600)
         i = i+1

注意这里all_anim是多个animation的集合


总结

本文将animation的save()函数的多数代码都解析了一遍,并将其关联的代码也一同解析了一遍,可以说读懂了这些代码才最终理解了为何直接调用save()会报错为何安装了ffmpeg依然不能成功输出这些问题,最后仍然留下了一个问题,即无法理解为何添加了环境变量依然无法识别ffmpeg命令,我去读过subprocess.py的代码,但这个代码使用到了一个_winapi模块(貌似是直接内置的模块,无法查看代码),导致问题陷入停滞,如果日后还碰到类似的问题,那再继续研究吧

猜你喜欢

转载自blog.csdn.net/sailist/article/details/79502007