|
From: <ry...@us...> - 2010-08-26 03:11:48
|
Revision: 8658
http://matplotlib.svn.sourceforge.net/matplotlib/?rev=8658&view=rev
Author: ryanmay
Date: 2010-08-26 03:11:42 +0000 (Thu, 26 Aug 2010)
Log Message:
-----------
Add new animation framework.
Modified Paths:
--------------
trunk/matplotlib/CHANGELOG
Added Paths:
-----------
trunk/matplotlib/lib/matplotlib/animation.py
Modified: trunk/matplotlib/CHANGELOG
===================================================================
--- trunk/matplotlib/CHANGELOG 2010-08-25 22:13:56 UTC (rev 8657)
+++ trunk/matplotlib/CHANGELOG 2010-08-26 03:11:42 UTC (rev 8658)
@@ -1,3 +1,5 @@
+2010-08-25 Add new framework for doing animations with examples.- RM
+
2010-08-21 Remove unused and inappropriate methods from Tick classes:
set_view_interval, get_minpos, and get_data_interval are
properly found in the Axis class and don't need to be
Added: trunk/matplotlib/lib/matplotlib/animation.py
===================================================================
--- trunk/matplotlib/lib/matplotlib/animation.py (rev 0)
+++ trunk/matplotlib/lib/matplotlib/animation.py 2010-08-26 03:11:42 UTC (rev 8658)
@@ -0,0 +1,451 @@
+# TODO:
+# * Loop Delay is broken on GTKAgg. This is because source_remove() is not
+# working as we want. PyGTK bug?
+# * Documentation -- this will need a new section of the User's Guide.
+# Both for Animations and just timers.
+# - Also need to update http://www.scipy.org/Cookbook/Matplotlib/Animations
+# * Blit
+# * Currently broken with Qt4 for widgets that don't start on screen
+# * Still a few edge cases that aren't working correctly
+# * Can this integrate better with existing matplotlib animation artist flag?
+# * Example
+# * Frameless animation - pure procedural with no loop
+# * Need example that uses something like inotify or subprocess
+# * Complex syncing examples
+# * Movies
+# * Library to make movies?
+# * RC parameter for config?
+# * Need to consider event sources to allow clicking through multiple figures
+from datetime import datetime
+
+def traceme(func):
+ def wrapper(*args):
+ print '%s -- Calling: %s %s' % (datetime.now(), func.__name__, str(args))
+ ret = func(*args)
+ print 'Returned: %s' % func.__name__
+ return ret
+ return wrapper
+
+from matplotlib.cbook import iterable
+
+class Animation(object):
+ '''
+ This class wraps the creation of an animation using matplotlib. It is
+ only a base class which should be subclassed to provide needed behavior.
+
+ *fig* is the figure object that is used to get draw, resize, and any
+ other needed events.
+
+ *event_source* is a class that can run a callback when desired events
+ are generated, as well as be stopped and started. Examples include timers
+ (see :class:`TimedAnimation`) and file system notifications.
+
+ *blit* is a boolean that controls whether blitting is used to optimize
+ drawing.
+ '''
+ def __init__(self, fig, event_source=None, blit=False):
+ self._fig = fig
+ self._blit = blit
+
+ # These are the basics of the animation. The frame sequence represents
+ # information for each frame of the animation and depends on how the
+ # drawing is handled by the subclasses. The event source fires events
+ # that cause the frame sequence to be iterated.
+ self.frame_seq = self.new_frame_seq()
+ self.event_source = event_source
+
+ # Clear the initial frame
+ self._init_draw()
+
+ # Instead of starting the event source now, we connect to the figure's
+ # draw_event, so that we only start once the figure has been drawn.
+ self._first_draw_id = fig.canvas.mpl_connect('draw_event', self._start)
+
+ # Connect to the figure's close_event so that we don't continue to
+ # fire events and try to draw to a deleted figure.
+ self._close_id = self._fig.canvas.mpl_connect('close_event', self._stop)
+ if blit:
+ self._setup_blit()
+
+ def _start(self, *args):
+ '''
+ Starts interactive animation. Adds the draw frame command to the GUI
+ handler, calls show to start the event loop.
+ '''
+ # On start, we add our callback for stepping the animation and
+ # actually start the event_source. We also disconnect _start
+ # from the draw_events
+ self.event_source.add_callback(self._step)
+ self.event_source.start()
+ self._fig.canvas.mpl_disconnect(self._first_draw_id)
+
+ def _stop(self, *args):
+ # On stop we disconnect all of our events.
+ if self._blit:
+ self._fig.canvas.mpl_disconnect(self._resize_id)
+ self._fig.canvas.mpl_disconnect(self._close_id)
+ self.event_source.remove_callback(self._step)
+ self.event_source = None
+
+ def save(self, filename, fps=5, codec='mpeg4', clear_temp=True,
+ frame_prefix='_tmp'):
+ '''
+ Saves a movie file by drawing every frame.
+
+ *fps* is the frames per second in the movie
+
+ *codec* is the codec to be used,if it is supported by the output method.
+
+ *clear_temp* specifies whether the temporary image files should be
+ deleted.
+
+ *frame_prefix* gives the prefix that should be used for individual
+ image files. This prefix will have a frame number (i.e. 0001) appended
+ when saving individual frames.
+ '''
+ fnames = []
+ # Create a new sequence of frames for saved data. This is different
+ # from new_frame_seq() to give the ability to save 'live' generated
+ # frame information to be saved later.
+ for idx,data in enumerate(self.new_saved_frame_seq()):
+ self._draw_next_frame(data, blit=False)
+ fname = '%s%04d.png' % (frame_prefix, idx)
+ fnames.append(fname)
+ self._fig.savefig(fname)
+
+ self._make_movie(filename, fps, codec, frame_prefix)
+
+ #Delete temporary files
+ if clear_temp:
+ import os
+ for fname in fnames:
+ os.remove(fname)
+
+ def ffmpeg_cmd(self, fname, fps, codec, frame_prefix):
+ # Returns the command line parameters for subprocess to use
+ # ffmpeg to create a movie
+ return ['ffmpeg', '-y', '-r', str(fps), '-b', '1800k', '-i',
+ '%s%%04d.png' % frame_prefix, fname]
+
+ def mencoder_cmd(self, fname, fps, codec, frame_prefix):
+ # Returns the command line parameters for subprocess to use
+ # mencoder to create a movie
+ return ['mencoder', 'mf://%s*.png' % frame_prefix, '-mf',
+ 'type=png:fps=%d' % fps, '-ovc', 'lavc', '-lavcopts',
+ 'vcodec=%s' % codec, '-oac', 'copy', '-o', fname]
+
+ def _make_movie(self, fname, fps, codec, frame_prefix, cmd_gen=None):
+ # Uses subprocess to call the program for assembling frames into a
+ # movie file. *cmd_gen* is a callable that generates the sequence
+ # of command line arguments from a few configuration options.
+ from subprocess import Popen, PIPE
+ if cmd_gen is None:
+ cmd_gen = self.ffmpeg_cmd
+ proc = Popen(cmd_gen(fname, fps, codec, frame_prefix), shell=False,
+ stdout=PIPE, stderr=PIPE)
+ proc.wait()
+
+ def _step(self, *args):
+ '''
+ Handler for getting events. By default, gets the next frame in the
+ sequence and hands the data off to be drawn.
+ '''
+ # Returns True to indicate that the event source should continue to
+ # call _step, until the frame sequence reaches the end of iteration,
+ # at which point False will be returned.
+ try:
+ framedata = self.frame_seq.next()
+ self._draw_next_frame(framedata, self._blit)
+ return True
+ except StopIteration:
+ return False
+
+ def new_frame_seq(self):
+ 'Creates a new sequence of frame information.'
+ # Default implementation is just an iterator over self._framedata
+ return iter(self._framedata)
+
+ def new_saved_frame_seq(self):
+ 'Creates a new sequence of saved/cached frame information.'
+ # Default is the same as the regular frame sequence
+ return self.new_frame_seq()
+
+ def _draw_next_frame(self, framedata, blit):
+ # Breaks down the drawing of the next frame into steps of pre- and
+ # post- draw, as well as the drawing of the frame itself.
+ self._pre_draw(framedata, blit)
+ self._draw_frame(framedata)
+ self._post_draw(framedata, blit)
+
+ def _init_draw(self):
+ # Initial draw to clear the frame. Also used by the blitting code
+ # when a clean base is required.
+ pass
+
+ def _pre_draw(self, framedata, blit):
+ # Perform any cleaning or whatnot before the drawing of the frame.
+ # This default implementation allows blit to clear the frame.
+ if blit:
+ self._blit_clear(self._drawn_artists, self._blit_cache)
+
+ def _draw_frame(self, framedata):
+ # Performs actual drawing of the frame.
+ raise NotImplementedError('Needs to be implemented by subclasses to'
+ ' actually make an animation.')
+
+ def _post_draw(self, framedata, blit):
+ # After the frame is rendered, this handles the actual flushing of
+ # the draw, which can be a direct draw_idle() or make use of the
+ # blitting.
+ if blit and self._drawn_artists:
+ self._blit_draw(self._drawn_artists, self._blit_cache)
+ else:
+ self._fig.canvas.draw_idle()
+
+ # The rest of the code in this class is to facilitate easy blitting
+ def _blit_draw(self, artists, bg_cache):
+ # Handles blitted drawing, which renders only the artists given instead
+ # of the entire figure.
+ updated_ax = []
+ for a in artists:
+ # If we haven't cached the background for this axes object, do
+ # so now. This might not always be reliable, but it's an attempt
+ # to automate the process.
+ if a.axes not in bg_cache:
+ bg_cache[a.axes] = a.figure.canvas.copy_from_bbox(a.axes.bbox)
+ a.axes.draw_artist(a)
+ updated_ax.append(a.axes)
+
+ # After rendering all the needed artists, blit each axes individually.
+ for ax in set(updated_ax):
+ ax.figure.canvas.blit(ax.bbox)
+
+ def _blit_clear(self, artists, bg_cache):
+ # Get a list of the axes that need clearing from the artists that
+ # have been drawn. Grab the appropriate saved background from the
+ # cache and restore.
+ axes = set(a.axes for a in artists)
+ for a in axes:
+ a.figure.canvas.restore_region(bg_cache[a])
+
+ def _setup_blit(self):
+ # Setting up the blit requires: a cache of the background for the
+ # axes
+ self._blit_cache = dict()
+ self._drawn_artists = []
+ self._resize_id = self._fig.canvas.mpl_connect('resize_event',
+ self._handle_resize)
+ self._post_draw(None, self._blit)
+
+ def _handle_resize(self, *args):
+ # On resize, we need to disable the resize event handling so we don't
+ # get too many events. Also stop the animation events, so that
+ # we're paused. Reset the cache and re-init. Set up an event handler
+ # to catch once the draw has actually taken place.
+ self._fig.canvas.mpl_disconnect(self._resize_id)
+ self.event_source.stop()
+ self._blit_cache.clear()
+ self._init_draw()
+ self._resize_id = self._fig.canvas.mpl_connect('draw_event', self._end_redraw)
+
+ def _end_redraw(self, evt):
+ # Now that the redraw has happened, do the post draw flushing and
+ # blit handling. Then re-enable all of the original events.
+ self._post_draw(None, self._blit)
+ self.event_source.start()
+ self._fig.canvas.mpl_disconnect(self._resize_id)
+ self._resize_id = self._fig.canvas.mpl_connect('resize_event',
+ self._handle_resize)
+
+
+class TimedAnimation(Animation):
+ '''
+ :class:`Animation` subclass that supports time-based animation, drawing
+ a new frame every *interval* milliseconds.
+
+ *repeat* controls whether the animation should repeat when the sequence
+ of frames is completed.
+
+ *repeat_delay* optionally adds a delay in milliseconds before repeating
+ the animation.
+ '''
+ def __init__(self, fig, interval=200, repeat_delay=None, repeat=True,
+ event_source=None, *args, **kwargs):
+ # Store the timing information
+ self._interval = interval
+ self._repeat_delay = repeat_delay
+ self.repeat = repeat
+
+ # If we're not given an event source, create a new timer. This permits
+ # sharing timers between animation objects for syncing animations.
+ if event_source is None:
+ event_source = fig.canvas.new_timer()
+ event_source.interval = self._interval
+
+ Animation.__init__(self, fig, event_source=event_source, *args, **kwargs)
+
+ def _step(self, *args):
+ '''
+ Handler for getting events.
+ '''
+ # Extends the _step() method for the Animation class. If
+ # Animation._step signals that it reached the end and we want to repeat,
+ # we refresh the frame sequence and return True. If _repeat_delay is
+ # set, change the event_source's interval to our loop delay and set the
+ # callback to one which will then set the interval back.
+ still_going = Animation._step(self, *args)
+ if not still_going and self.repeat:
+ if self._repeat_delay:
+ self.event_source.remove_callback(self._step)
+ self.event_source.add_callback(self._loop_delay)
+ self.event_source.interval = self._repeat_delay
+ self.frame_seq = self.new_frame_seq()
+ return True
+ else:
+ return still_going
+
+ def _stop(self, *args):
+ # If we stop in the middle of a loop delay (which is relatively likely
+ # given the potential pause here, remove the loop_delay callback as
+ # well.
+ self.event_source.remove_callback(self._loop_delay)
+ Animation._stop(self)
+
+ def _loop_delay(self, *args):
+ # Reset the interval and change callbacks after the delay.
+ self.event_source.remove_callback(self._loop_delay)
+ self.event_source.interval = self._interval
+ self.event_source.add_callback(self._step)
+
+
+class ArtistAnimation(TimedAnimation):
+ '''
+ Before calling this function, all plotting should have taken place
+ and the relevant artists saved.
+
+ frame_info is a list, with each list entry a collection of artists that
+ represent what needs to be enabled on each frame. These will be disabled
+ for other frames.
+ '''
+ def __init__(self, fig, artists, *args, **kwargs):
+ # Internal list of artists drawn in the most recent frame.
+ self._drawn_artists = []
+
+ # Use the list of artists as the framedata, which will be iterated
+ # over by the machinery.
+ self._framedata = artists
+ TimedAnimation.__init__(self, fig, *args, **kwargs)
+
+ def _init_draw(self):
+ # Make all the artists involved in *any* frame invisible
+ axes = []
+ for f in self.new_frame_seq():
+ for artist in f:
+ artist.set_visible(False)
+ # Assemble a list of unique axes that need flushing
+ if artist.axes not in axes:
+ axes.append(artist.axes)
+
+ # Flush the needed axes
+ for ax in axes:
+ ax.figure.canvas.draw()
+
+ def _pre_draw(self, framedata, blit):
+ '''
+ Clears artists from the last frame.
+ '''
+ if blit:
+ # Let blit handle clearing
+ self._blit_clear(self._drawn_artists, self._blit_cache)
+ else:
+ # Otherwise, make all the artists from the previous frame invisible
+ for artist in self._drawn_artists:
+ artist.set_visible(False)
+
+ def _draw_frame(self, artists):
+ # Save the artists that were passed in as framedata for the other
+ # steps (esp. blitting) to use.
+ self._drawn_artists = artists
+
+ # Make all the artists from the current frame visible
+ for artist in artists:
+ artist.set_visible(True)
+
+class FuncAnimation(TimedAnimation):
+ '''
+ Makes an animation by repeatedly calling a function *func*, passing in
+ (optional) arguments in *fargs*.
+
+ *frames* can be a generator, an iterable, or a number of frames.
+
+ *init_func* is a function used to draw a clear frame. If not given, the
+ results of drawing from the first item in the frames sequence will be
+ used.
+ '''
+ def __init__(self, fig, func, frames=None ,init_func=None, fargs=None,
+ save_count=None, **kwargs):
+ if fargs:
+ self._args = fargs
+ else:
+ self._args = ()
+ self._func = func
+
+ # Amount of framedata to keep around for saving movies. This is only
+ # used if we don't know how many frames there will be: in the case
+ # of no generator or in the case of a callable.
+ self.save_count = save_count
+
+ # Set up a function that creates a new iterable when needed. If nothing
+ # is passed in for frames, just use itertools.count, which will just
+ # keep counting from 0. A callable passed in for frames is assumed to
+ # be a generator. An iterable will be used as is, and anything else
+ # will be treated as a number of frames.
+ if frames is None:
+ import itertools
+ self._iter_gen = itertools.count
+ elif callable(frames):
+ self._iter_gen = frames
+ elif iterable(frames):
+ self._iter_gen = lambda: iter(frames)
+ self.save_count = len(frames)
+ else:
+ self._iter_gen = lambda: iter(range(frames))
+ self.save_count = frames
+
+ # If we're passed in and using the default, set it to 100.
+ if self.save_count is None:
+ self.save_count = 100
+
+ self._init_func = init_func
+ self._save_seq = []
+
+ TimedAnimation.__init__(self, fig, **kwargs)
+
+ def new_frame_seq(self):
+ # Use the generating function to generate a new frame sequence
+ return self._iter_gen()
+
+ def new_saved_frame_seq(self):
+ # Generate an iterator for the sequence of saved data.
+ return iter(self._save_seq)
+
+ def _init_draw(self):
+ # Initialize the drawing either using the given init_func or by
+ # calling the draw function with the first item of the frame sequence.
+ # For blitting, the init_func should return a sequence of modified
+ # artists.
+ if self._init_func is None:
+ self._draw_frame(self.new_frame_seq().next())
+ else:
+ self._drawn_artists = self._init_func()
+
+ def _draw_frame(self, framedata):
+ # Save the data for potential saving of movies.
+ self._save_seq.append(framedata)
+
+ # Make sure to respect save_count (keep only the last save_count around)
+ self._save_seq = self._save_seq[-self.save_count:]
+
+ # Call the func with framedata and args. If blitting is desired,
+ # func needs to return a sequence of any artists that were modified.
+ self._drawn_artists = self._func(framedata, *self._args)
This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site.
|