This article mainly introduces how animation in matplotlib saves animation, starting from some basic codes of matplotlib, and attaching the code to solve the error of save() function at the end, some of which involve __getitem__()
the knowledge of methods and annotation modification, if you don't understand Friends hope to check the relevant knowledge first
some introduction
rcParams
We know that if the matplotlib function does not specify parameters when drawing, it will use a series of default values to draw the image. These default values are saved in matplotlib, rcParams and saved in the form of a dictionary. Among them, there are the following parts designed to the animation part
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',
...
The most important of these are the following lines, which we'll come to later
MovieWriter:class for writing movies
Let's take a look at the parameter requirements of the save() function first.
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):
The most important parameter of this is the writer, let's take a look at the requirements for the 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']
.
Here, the writer must be a MovieWriter class or a string. The same will be described later. What we need to know is that MovieWriter is a base class. If we want to implement writing animation, it must be implemented by its subclasses.
Some code snippets in animation.py
First look at the processing of the writer in the save() function
if writer is None:
writer = rcParams['animation.writer']
If the wirter is not specified, then the writer will be taken from the default value of matplotlib. If you look at the default value above, you can see rcParams['animation.writer'] = "ffmpeg"
that the writer will become a string that specifies the encoding program.
Continue down: it is writer
from str to the MovieWriter class. change
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)
The reason for the error we often report MovieWriter ffmpeg unavailable
is here. If we don't specify writer
or writer
assign the value str
, then we willwriter
find the corresponding class. So what are writers? It is defined on line 174 of animation.py :writers
MovieWriter
writers = MovieWriterRegistry()
It is an object created by the MovieWriterRegistry class, which is used for Registry of available writer classes by human readable name. (Registration of useful writer classes by human-readable names), two empty dictionaries are defined in the initialization method of this class, using To store the registered writer class and the corresponding name, the code is as follows:
class MovieWriterRegistry(object):
'''Registry of available writer classes by human readable name.'''
def __init__(self):
self.avail = dict()
self._registered = dict()
self._dirty = False
We saw that the writer
class was taken out writers[writer]
from it before, and the method MovieWriterRegistry
was defined in it, but what was actually returned is__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]
When was the self.avail
element added to it? Let's take a look at the definitions of the subclasses of the following classes
through annotations : (not all listed)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.
'''
The logic is clear. When MovieWriter
these subclasses are defined, the corresponding classes will be called at writers.register('name')
the same time writers.avail
. After the definition, if the writer parameter of the save() function is empty, it will be converted into a string, if it is a character string, find the corresponding class from writers.avail, if it is a class, use the class directly
The part that saves the animation in the save() function
This piece is very interesting to understand and can be used in the code you write. Let's take a look:
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)
The beginning with writer.saving(self._fig, filename, dpi):
is used to open the pipeline to the video. At the
end writer.grab_frame(**savefig_kwargs)
, it can be seen from the function name that the image drawn on the current figure is saved,
that is, the code for updating the figure is in the middle of the code.
Then let's take a lookgrab_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))
See the most critical code in the middle? ? ?
self.fig.savefig(self._frame_sink(), format=self.frame_format,
dpi=self.dpi, **savefig_kwargs)
The writer in turn saves the current image of the figure to the location it specifies, and then merges it into a video.
The logic is basically clear here, let's speed up
self._frame_sink()
the save location, the only purpose of this method is to return self._proc.stdin
and self._proc
it MovieWriter
is defined in
self._proc = subprocess.Popen(command, shell=False,
stdout=output, stderr=output,
stdin=subprocess.PIPE,
creationflags=subprocess_creation_flags)
That is, the location where fig saves the image is the input end of the pipeline opened by the subprocess.Popen command, and the output end is naturally the video file.
Back to MovieWriter
We now know how the MovieWriter class is obtained, and we also know how save() saves the video. Let's continue to look at the MovieWriter class. The definitions of several subclasses of the MovieWriter class are given
before , and they are only more Implemented a _args() function, what does it return? It is not difficult to think that it is used to return the corresponding cmd command
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()
It is the first string of characters of the command, that is to say, it represents the program to be run. For FFMpegWriter, it should be ffmpeg. Specifically, let's take a look. This method is defined in the MovieWriter class.
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])
Returns rcParams[cls.exec_key]
and exec_key
, in turn FFMpegBase(FFMpegWriter的父类之一)
, is defined in and some other classes
exec_key = 'animation.ffmpeg_path'
Looking back at rcParams, rcParams[cls.exec_key]
does it return the name of the corresponding encoder?
At this point, the save() function can be said to have been clarified inside and out. Next, we use it to get the video we want.
start solving
Let's start to solve various errors of the save() function
Download FFmpeg
Windows version: https://ffmpeg.zeranoe.com/builds/
(For the rest of the systems, please see the official website)
Click on it and download the static version, extract it to any location, and add path/ffmpeg/bin
it to the environment variablePATH
Output video in mp4 format
#anim = animation.ArtistAnimation(fig, ims, interval=interval, repeat_delay=repeat_delay,repeat = repeat,
# blit=True)
writer = animation.FFMpegWriter()
anim.save(fname,writer = writer)
It stands to reason that the environment variables are configured, and ffmpeg can be displayed in the cmd command. The call should be fine, but I will report the following error, indicating that the system does not find 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] 系统找不到指定的文件。
Therefore, the absolute path should be used to represent ffmpeg, and the above code should be changed to:
ffmpegpath = os.path.abspath("./ffmpeg/bin/ffmpeg.exe")
matplotlib.rcParams["animation.ffmpeg_path"] = ffmpegpath
writer = animation.FFMpegWriter()
anim.save(fname,writer = writer)
Run the program again, the output is successful! (I put ffmpeg in the program directory to ensure that there will be no errors in the program, and I don't need to explain the rest?)
A small port of the code in animation.py
What if I want to save all the pictures in the animation as pictures? Remember what the writer did before processing each frame of animation? Simply change it
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
Note here that all_anim is a collection of multiple animations
Summarize
This article parses most of the code of the animation's save() function, and parses its associated code together. It can be said that after reading these codes, I finally understand why calling save() directly will report an error and why it is installed . After ffmpeg still can't successfully output these problems, there is still a problem left, that is, I can't understand why the ffmpeg command is still not recognized after adding environment variables. I have read the code of subprocess.py, but this code uses a _winapi module (It seems to be a directly built-in module, and the code cannot be viewed), causing the problem to stagnate. If you encounter similar problems in the future, continue to study