本文主要介绍matplotlib中animation如何保存动画,从matplotlib的一些基础代码说起,并在最后附上了解决save()函数报错的代码,其中的一些代码涉及到__getitem__()
方法和注解修饰的知识,如果没有了解的朋友希望先去查一下相关的知识了解一下
文章目录
一些介绍rcParamsMovieWriter:`class for writing movies`animation.py中的一些代码片段save()函数中保存动画的部分重回MovieWriter开始解决输出mp4格式的视频animation.py中代码的一个小移植总结一些介绍
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, optionalA
MovieWriter
instance to use or a key that identifies aclass 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 singlepass.'''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_argsreturn args@writers.register('ffmpeg_file')class FFMpegFileWriter(FFMpegBase, FileMovieWriter):'''File-based ffmpeg writer.Frames are written to temporary files on disk and then stitchedtogether 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 imagesreturn [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 singlepass.'''
这下逻辑就明了了,在定义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 frameanim._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 necessaryanim._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 = municate()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._proc
在MovieWriter
中定义了
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_argsreturn args
bin_path()
是命令的第一串字符,也就是说,它代表着要运行的程序,对于FFMpegWriter来说,它应该就是ffmpeg,具体是不是,来看一下吧,在MovieWriter类中定义了这个方法
def bin_path(cls):'''Returns the binary path to the commandline tool used by a specificsubclass. This is a class method so that the tool can be looked forbefore 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版本:/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"] = ffmpegpathwriter = animation.FFMpegWriter()anim.save(fname,writer = writer)
再次运行程序,输出成功!(我是将ffmpeg放到了程序目录下,保证程序不会出错误,其余的不用我解释了吧?)
animation.py中代码的一个小移植
如果我想讲动画中的图片全都保存为一张张图片怎么办?还记得之前提到的writer处理每一帧动画时候的操作吗?将其简单改改就可以了
i = 0for 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模块(貌似是直接内置的模块,无法查看代码),导致问题陷入停滞,如果日后还碰到类似的问题,那再继续研究吧
9月7日更新:环境变量在Python中好像需要重启电脑才会更新,最近在尝试另外一个库的时候发现了,通过os.envi查看Path的时候,发现并没有获取到我当次开机新增的目录,需要重启。可能会有刷新方法吧,但没找。
如果觉得《matplotlib animation动画保存(save函数)详解》对你有帮助,请点赞、收藏,并留下你的观点哦!