Skip to content

Animation fixes #1012

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

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 15 additions & 4 deletions examples/animation/basic_example_writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,26 @@ def update_line(num, data, line):
interval=50, blit=True)
line_ani.save('lines.mp4', writer=writer)

fig2 = plt.figure()
fig2, ax = plt.subplots()

x = np.arange(-9, 10)
y = np.arange(-9, 10).reshape(-1, 1)
base = np.hypot(x, y)
ims = []
for add in np.arange(15):
ims.append((plt.pcolor(x, y, base + add, norm=plt.Normalize(0, 30)),))
ax.cla() # clear the last frame
im = ax.pcolor(x, y, base + add, norm=plt.Normalize(0, 30))
ims.append([im])

im_ani = animation.ArtistAnimation(fig2, ims, interval=50, repeat_delay=3000,
blit=True)
im_ani = animation.ArtistAnimation(fig2, ims, interval=50, repeat_delay=3000)
im_ani.save('im.mp4', writer=writer)

# Set up formatting for the movie files
Writer = animation.writers['ffmpeg_file']

# we can force ffmpeg to make webm movies if we use -f webm and no
# codec. webm works by default on chrome and firefox; these will
# display inline in the ipython notebook

#writer = Writer(fps=15, codec='None', extra_args=['-f', 'webm'])
# im_ani.save('movies/im2.webm', writer=writer)
98 changes: 81 additions & 17 deletions lib/matplotlib/animation.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
# * Movies
# * Can blit be enabled for movies?
# * Need to consider event sources to allow clicking through multiple figures
import os
import itertools
import contextlib
import subprocess
Expand Down Expand Up @@ -88,9 +89,10 @@ def __init__(self, fps=5, codec=None, bitrate=None, extra_args=None,
----------
fps: int
Framerate for movie.
codec: string or None, optional
The codec to use. If None (the default) the setting in the
rcParam `animation.codec` is used.
codec: string or None, optional The codec to use. If None (the
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it was intended that "The codec to use" was supposed to be on the next line.

default) the setting in the rcParam `animation.codec` is
used. If codec is the special sentinel string 'None',
then no codec argument will be passed through.
bitrate: int or None, optional
The bitrate for the saved movie file, which is one way to control
the output file size and quality. The default value is None,
Expand All @@ -102,7 +104,7 @@ def __init__(self, fps=5, codec=None, bitrate=None, extra_args=None,
movie utiltiy. The default is None, which passes the additional
argurments in the 'animation.extra_args' rcParam.
metadata: dict of string:string or None
A dictionary of keys and values for metadata to include in the
A dictionary of keys and values for metadata to include in the
output file. Some keys that may be of use include:
title, artist, genre, subject, copyright, srcform, comment.
'''
Expand Down Expand Up @@ -135,7 +137,7 @@ def frame_size(self):
width_inches, height_inches = self.fig.get_size_inches()
return width_inches * self.dpi, height_inches * self.dpi

def setup(self, fig, outfile, dpi, *args):
def setup(self, fig, outfile, dpi, *args, **kwargs):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this method, both args and kwargs are ignored. Do they need to be there?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree. Is there any point to have it this way? My best guess is to purposely swallow extra args from a call to this function in a sub-classed animator, but I don't know if that would be a good idea.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note, I don't have a problem with the use of _args and *_kwargs in the saving() method below, just that given that setup() is acting as the terminus of the call tree should probably at least warn that there were extra args. Case-in-point, misspelled args to savefig() never get noticed.

'''
Perform setup for writing the movie file.

Expand All @@ -159,14 +161,14 @@ def setup(self, fig, outfile, dpi, *args):
self._run()

@contextlib.contextmanager
def saving(self, *args):
def saving(self, *args, **kwargs):
'''
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)
self.setup(*args, **kwargs)
yield
self.finish()

Expand Down Expand Up @@ -242,7 +244,7 @@ def isAvailable(cls):
class FileMovieWriter(MovieWriter):
'`MovieWriter` subclass that handles writing to a file.'
def __init__(self, *args, **kwargs):
MovieWriter.__init__(self, *args)
MovieWriter.__init__(self, *args, **kwargs)
self.frame_format = rcParams['animation.frame_format']

def setup(self, fig, outfile, dpi, frame_prefix='_tmp', clear_temp=True):
Expand Down Expand Up @@ -340,7 +342,9 @@ class FFMpegBase:
def output_args(self):
# The %dk adds 'k' as a suffix so that ffmpeg treats our bitrate as in
# kbps
args = ['-vcodec', self.codec]
args = []
if self.codec!='None':
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be None not 'None'?

args.extend(['-vcodec', self.codec])
if self.bitrate > 0:
args.extend(['-b', '%dk' % self.bitrate])
if self.extra_args:
Expand All @@ -354,18 +358,28 @@ def output_args(self):
# Combine FFMpeg options with pipe-based writing
@writers.register('ffmpeg')
class FFMpegWriter(MovieWriter, FFMpegBase):
def __init__(self, *args, **kwargs):
# FFMpegBase doesn't have an init method so we need to make
# sure the MovieWriter gets called with our args and kwargs
MovieWriter.__init__(self, *args, **kwargs)

def _args(self):
# Returns the command line parameters for subprocess to use
# ffmpeg to create a movie using a pipe
return [self.bin_path(), '-f', 'rawvideo', '-vcodec', 'rawvideo',
'-s', '%dx%d' % self.frame_size, '-pix_fmt', self.frame_format,
'-s', '%dx%d' % self.frame_size, '-pix_fmt', self.frame_format,
'-r', str(self.fps), '-i', 'pipe:'] + self.output_args


#Combine FFMpeg options with temp file-based writing
@writers.register('ffmpeg_file')
class FFMpegFileWriter(FileMovieWriter, FFMpegBase):
supported_formats = ['png', 'jpeg', 'ppm', 'tiff', 'sgi', 'bmp', 'pbm', 'raw', 'rgba']
def __init__(self, *args, **kwargs):
# FFMpegBase doesn't have an init method so we need to make
# sure the FileMovieWriter gets called with our args and kwargs
FileMovieWriter.__init__(self, *args, **kwargs)

def _args(self):
# Returns the command line parameters for subprocess to use
# ffmpeg to create a movie using a collection of temp images
Expand Down Expand Up @@ -393,7 +407,9 @@ def _remap_metadata(self):
@property
def output_args(self):
self._remap_metadata()
args = ['-o', self.outfile, '-ovc', 'lavc', 'vcodec=%s' % self.codec]
args = ['-o', self.outfile, '-ovc', 'lavc']
if self.codec!='None':
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Picky I know, but PEP8 encourages spaces between operators (i.e. if self.codec != 'None':).

args.append('vcodec=%s' % self.codec)
if self.bitrate > 0:
args.append('vbitrate=%d' % self.bitrate)
if self.extra_args:
Expand All @@ -406,6 +422,11 @@ def output_args(self):
# Combine Mencoder options with pipe-based writing
@writers.register('mencoder')
class MencoderWriter(MovieWriter, MencoderBase):
def __init__(self, *args, **kwargs):
# MencoderBase doesn't have an init method so we need to make
# sure the MovieWriter gets called with our args and kwargs
MovieWriter.__init__(self, *args, **kwargs)

def _args(self):
# Returns the command line parameters for subprocess to use
# mencoder to create a movie
Expand All @@ -418,6 +439,11 @@ def _args(self):
@writers.register('mencoder_file')
class MencoderFileWriter(FileMovieWriter, MencoderBase):
supported_formats = ['png', 'jpeg', 'tga', 'sgi']
def __init__(self, *args, **kwargs):
# MencoderBase doesn't have an init method so we need to make
# sure the MovieWriter gets called with our args and kwargs
MovieWriter.__init__(self, *args, **kwargs)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't you mean "FileMovieWriter" here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, I'll fix this


def _args(self):
# Returns the command line parameters for subprocess to use
# mencoder to create a movie
Expand Down Expand Up @@ -487,7 +513,8 @@ def _stop(self, *args):
self.event_source = None

def save(self, filename, writer=None, fps=None, dpi=None, codec=None,
bitrate=None, extra_args=None, metadata=None, extra_anim=None):
bitrate=None, extra_args=None, metadata=None, extra_anim=None,
writer_setup_kwargs=None):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seem to have an extra space at the beginning of this line.

'''
Saves a movie file by drawing every frame.

Expand All @@ -499,21 +526,27 @@ def save(self, filename, writer=None, fps=None, dpi=None, codec=None,
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
per second.
which will use the animation's specified interval to set the
frames per second. This is only respected if *writer* is a
string; if using a class instance just set the *fps* in the
writer.

*dpi* controls the dots per inch for the movie frames. This combined
with the figure's size in inches controls the size of the movie.

*codec* is the video codec to be used. Not all codecs are supported
by a given :class:`MovieWriter`. If none is given, this defaults to the
value specified by the rcparam `animation.codec`.
value specified by the rcparam `animation.codec`. This is only
respected if *writer* is a string; if using a class instance just set
the *codec* in the writer.

*bitrate* specifies the amount of bits used per second in the
compressed movie, in kilobits per second. A higher number means a
higher quality movie, but at the cost of increased file size. If no
value is given, this defaults to the value given by the rcparam
`animation.bitrate`.
`animation.bitrate`. This is only respected if *writer* is a
string; if using a class instance just set the *bitrate* in the
writer.

*extra_args* is a list of extra string arguments to be passed to the
underlying movie utiltiy. The default is None, which passes the
Expand All @@ -528,6 +561,11 @@ def save(self, filename, writer=None, fps=None, dpi=None, codec=None,
`matplotlib.Figure` instance. Also, animation frames will just be
simply combined, so there should be a 1:1 correspondence between
the frames from the different animations.

*writer_setup_kwargs* is a dictionary of keyword args to pass to
the writer saving/setup method (writer.saving passes them through
to writer.setup)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use sphinx rest notation.

:meth:`~MovieWriter.saving`
:meth:`~MovieWriter.setup`

should work.


'''
# Need to disconnect the first draw callback, since we'll be doing
# draws. Otherwise, we'll end up starting the animation.
Expand Down Expand Up @@ -578,7 +616,10 @@ def save(self, filename, writer=None, fps=None, dpi=None, codec=None,
# TODO: Right now, after closing the figure, saving a movie won't
# work since GUI widgets are gone. Either need to remove extra code
# to allow for this non-existant use case or find a way to make it work.
with writer.saving(self._fig, filename, dpi):

if writer_setup_kwargs is None:
writer_setup_kwargs = dict()
with writer.saving(self._fig, filename, dpi, **writer_setup_kwargs):
for data in itertools.izip(*[a.new_saved_frame_seq() for a in all_anim]):
for anim,d in zip(all_anim, data):
#TODO: Need to see if turning off blit is really necessary
Expand All @@ -590,6 +631,29 @@ def save(self, filename, writer=None, fps=None, dpi=None, codec=None,
self._first_draw_id = self._fig.canvas.mpl_connect('draw_event',
self._start)


# if fname is a relative path, return a custom object that
# supports the ipython display hook for embedding the video
# directly into an ipynb. The wrapper inherits from string so
# for normal users the class will just look like the filename
# when returned. But we define the custom ipython html
# display hook to embed HTML5 video for others, but only if
# webm is requested because the browsers do not currently
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note to self: this comment is not correct. Initially I was just going to do this for webm, but there are plugins for chrome at least that support mpeg4, so I am returning the EmbedHTML for all filetypes and leave it to the nb user to try and add support to their browser. Just need to clan up the comment

# support mp4, etc; the '/files/' prefix is an ipython
# notebook convention meaning the root of the notebook tree
# (where the nb lives)
class EmbedHTML(str):

def _repr_html_(self):
fname = os.path.join('/files/', filename)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and what if the user set a filename that is an absolute filepath that is outside the notebook's observable filesystem? I think some more thought needs to go into this.

Plus, I think this whole repr stuff needs to be in a separate PR.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This also isn't the full story, I think because we still need to trigger the save process, right? This is partly complicated by the fact that the save() method may need the "extra_animations" to get what the user wants. We might be getting to the point where we need to consider having the figure object hold a list of animations that have been added to the figure (except that has a whole world of a mess in-of-itself).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the user sets a absolute filepath, the HTMLEmbed is not returned. As for the separate PR< I think that is overkill. This is really minimally invasive. What is returned looks like a string (inherits from string) and only ipython will care about doing something with the repr_html

return '<video controls src="%s" />'%fname
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and here (operator spacing).


if os.path.isabs(filename):
return filename
else:
return EmbedHTML(filename)


def _step(self, *args):
'''
Handler for getting events. By default, gets the next frame in the
Expand Down