欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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

程序员文章站 2022-03-01 15:05:02
...

本文主要介绍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模块(貌似是直接内置的模块,无法查看代码),导致问题陷入停滞,如果日后还碰到类似的问题,那再继续研究吧