diff --git a/examples/animation/basic_example_writer.py b/examples/animation/basic_example_writer.py index fef958942621..191d2dcca036 100644 --- a/examples/animation/basic_example_writer.py +++ b/examples/animation/basic_example_writer.py @@ -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) diff --git a/lib/matplotlib/animation.py b/lib/matplotlib/animation.py index 5f01b1cf8369..54a3a67321dd 100644 --- a/lib/matplotlib/animation.py +++ b/lib/matplotlib/animation.py @@ -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 @@ -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 + 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, @@ -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. ''' @@ -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): ''' Perform setup for writing the movie file. @@ -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() @@ -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): @@ -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': + args.extend(['-vcodec', self.codec]) if self.bitrate > 0: args.extend(['-b', '%dk' % self.bitrate]) if self.extra_args: @@ -354,11 +358,16 @@ 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 @@ -366,6 +375,11 @@ def _args(self): @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 @@ -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': + args.append('vcodec=%s' % self.codec) if self.bitrate > 0: args.append('vbitrate=%d' % self.bitrate) if self.extra_args: @@ -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 @@ -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) + def _args(self): # Returns the command line parameters for subprocess to use # mencoder to create a movie @@ -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): ''' Saves a movie file by drawing every frame. @@ -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 @@ -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) + ''' # Need to disconnect the first draw callback, since we'll be doing # draws. Otherwise, we'll end up starting the animation. @@ -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 @@ -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 + # 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) + return '