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
AMovieWriter
instance to use or a key that identifies a
class to use, such as ‘ffmpeg’ or ‘mencoder’. IfNone
,
defaults torcParams['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._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_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模块(貌似是直接内置的模块,无法查看代码),导致问题陷入停滞,如果日后还碰到类似的问题,那再继续研究吧
上一篇: matplotlib的学习16-animation动画
下一篇: CSS—动画之animation