Detailed explanation of matplotlib animation animation save (save function)

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
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'].

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 writerfrom 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 unavailableis here. If we don't specify writeror writerassign the value str, then we willwriter find the corresponding class. So what are writers? It is defined on line 174 of animation.py :writersMovieWriter

    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 writerclass was taken out writers[writer]from it before, and the method MovieWriterRegistrywas 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.availelement 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 MovieWriterthese 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._procit MovieWriteris 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/binit 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

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325848876&siteId=291194637