8000 ENH: Create an abstract base class for movie writers. by WarrenWeckesser · Pull Request #5454 · matplotlib/matplotlib · GitHub
[go: up one dir, main page]

Skip to content

ENH: Create an abstract base class for movie writers. #5454

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Dec 29, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conve 8000 rsations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions doc/users/whats_new/abstract_movie_writer.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Abstract base class for movie writers
-------------------------------------

The new :class:`~matplotlib.animation.AbstractMovieWriter` class defines
the API required by a class that is to be used as the `writer` in the
`save` method of the :class:`~matplotlib.animation.Animation` class.
The existing :class:`~matplotlib.animation.MovieWriter` class now derives
from the new abstract base class.
86 changes: 67 additions & 19 deletions lib/matplotlib/animation.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
except ImportError:
# python2
from base64 import encodestring as encodebytes
import abc
import contextlib
import tempfile
from matplotlib.cbook import iterable, is_string_like
Expand Down Expand Up @@ -91,17 +92,76 @@ def __getitem__(self, name):
writers = MovieWriterRegistry()


class MovieWriter(object):
class AbstractMovieWriter(six.with_metaclass(abc.ABCMeta)):
'''
Abstract base class for writing movies. Fundamentally, what a MovieWriter
does is provide is a way to grab frames by calling grab_frame().

setup() is called to start the process and finish() is called afterwards.

This class is set up to provide for writing movie frame data to a pipe.
saving() is provided as a context manager to facilitate this process as::

with moviewriter.saving(fig, outfile='myfile.mp4', dpi=100):
# Iterate over frames
moviewriter.grab_frame(**savefig_kwargs)

The use of the context manager ensures that setup() and finish() are
performed as necessary.

An instance of a concrete subclass of this class can be given as the
`writer` argument of `Animation.save()`.
'''

@abc.abstractmethod
def setup(self, fig, outfile, dpi, *args):
'''
Perform setup for writing the movie file.

fig: `matplotlib.Figure` instance
The figure object that contains the information for frames
outfile: string
The filename of the resulting movie file
dpi: int
The DPI (or resolution) for the file. This controls the size
in pixels of the resulting movie file.
'''

@abc.abstractmethod
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.
'''

@abc.abstractmethod
def finish(self):
'Finish any processing for writing the movie.'

@contextlib.contextmanager
def saving(self, fig, outfile, dpi, *args):
'''
Context manager to facilitate writing the movie file.

All arguments are passed on to `setup`.
'''
self.setup(fig, outfile, dpi, *args)
yield
self.finish()


class MovieWriter(AbstractMovieWriter):
'''
Base class for writing movies. Fundamentally, what a MovieWriter does
is provide is a way to grab frames by calling grab_frame(). setup()
is called to start the process and finish() is called afterwards.
This class is set up to provide for writing movie frame data to a pipe.
saving() is provided as a context manager to facilitate this process as::

with moviewriter.saving('myfile.mp4'):
with moviewriter.saving(fig, outfile='myfile.mp4', dpi=100):
# Iterate over frames
moviewriter.grab_frame()
moviewriter.grab_frame(**savefig_kwargs)

The use of the context manager ensures that setup and cleanup are
performed as necessary.
Expand Down Expand Up @@ -183,18 +243,6 @@ def setup(self, fig, outfile, dpi, *args):
# eliminates the need for temp files.
self._run()

@contextlib.contextmanager
def saving(self, *args):
'''
Context manager to facilitate writing the movie file.

``*args`` are any parameters that should be passed to `setup`.
'''
# This particular sequence is what contextlib.contextmanager wants
self.setup(*args)
yield
self.finish()

def _run(self):
# Uses subprocess to call the program for assembling frames into a
# movie file. *args* returns the sequence of command line arguments
Expand Down Expand Up @@ -669,10 +717,10 @@ def save(self, filename, writer=None, fps=None, dpi=None, codec=None,

*filename* is the output filename, e.g., :file:`mymovie.mp4`

*writer* is either an instance of :class:`MovieWriter` or a string
key that identifies a class to use, such as 'ffmpeg' or 'mencoder'.
If nothing is passed, the value of the rcparam `animation.writer` is
used.
*writer* is either an instance of :class:`AbstractMovieWriter` or
a string key that identifies a class to use, such as 'ffmpeg' or
'mencoder'. If nothing is passed, the value of the rcparam
`animation.writer` is used.

*fps* is the frames per second in the movie. Defaults to None,
which will use the animation's specified interval to set the frames
Expand Down
80 changes: 79 additions & 1 deletion lib/matplotlib/tests/test_animation.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import os
import tempfile
import numpy as np
from numpy.testing import assert_equal
from nose import with_setup
from matplotlib import pyplot as plt
from matplotlib import animation
Expand All @@ -14,10 +15,87 @@
from matplotlib.testing.decorators import CleanupTest


class NullMovieWriter(animation.AbstractMovieWriter):
"""
A minimal MovieWriter. It doesn't actually write anything.
It just saves the arguments that were given to the setup() and
grab_frame() methods as attributes, and counts how many times
grab_frame() is called.

This class doesn't have an __init__ method with the appropriate
signature, and it doesn't define an isAvailable() method, so
it cannot be added to the 'writers' registry.
"""

def setup(self, fig, outfile, dpi, *args):
self.fig = fig
self.outfile = outfile
self.dpi = dpi
self.args = args
self._count = 0

def grab_frame(self, **savefig_kwargs):
self.savefig_kwargs = savefig_kwargs
self._count += 1

def finish(self):
pass


def test_null_movie_writer():
# Test running an animation with NullMovieWriter.

fig = plt.figure()

def init():
pass

def animate(i):
pass

num_frames = 5
filename = "unused.null"
fps = 30
dpi = 50
savefig_kwargs = dict(foo=0)

anim = animation.FuncAnimation(fig, animate, init_func=init,
frames=num_frames)
writer = NullMovieWriter()
anim.save(filename, fps=fps, dpi=dpi, writer=writer,
savefig_kwargs=savefig_kwargs)

assert_equal(writer.fig, fig)
assert_equal(writer.outfile, filename)
assert_equal(writer.dpi, dpi)
assert_equal(writer.args, ())
assert_equal(writer.savefig_kwargs, savefig_kwargs)
assert_equal(writer._count, num_frames)


@animation.writers.register('null')
class RegisteredNullMovieWriter(NullMovieWriter):

# To be able to add NullMovieWriter to the 'writers' registry,
# we must define an __init__ method with a specific signature,
# and we must define the class method isAvailable().
# (These methods are not actually required to use an instance
# of this class as the 'writer' argument of Animation.save().)

def __init__(self, fps=None, codec=None, bitrate=None,
extra_args=None, metadata=None):
pass

@classmethod
def isAvailable(self):
return True


WRITER_OUTPUT = dict(ffmpeg='mp4', ffmpeg_file='mp4',
mencoder='mp4', mencoder_file='mp4',
avconv='mp4', avconv_file='mp4',
imagemagick='gif', imagemagick_file='gif')
imagemagick='gif', imagemagick_file='gif',
null='null')


# Smoke test for saving animations. In the future, we should probably
Expand Down
0