Skip to content

Commit

Permalink
DOC: add a blitting tutorial
Browse files Browse the repository at this point in the history
  • Loading branch information
tacaswell committed Jun 11, 2020
1 parent 21c3acb commit 4192f98
Show file tree
Hide file tree
Showing 2 changed files with 184 additions and 1 deletion.
2 changes: 1 addition & 1 deletion doc/api/animation_api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ performance), to be non-blocking, not repeatedly start/stop the GUI
event loop, handle repeats, multiple animated axes, and easily save
the animation to a movie file.

'Blitting' is a `old technique
'Blitting' is a `standard technique
<https://en.wikipedia.org/wiki/Bit_blit>`__ in computer graphics. The
general gist is to take an existing bit map (in our case a mostly
rasterized figure) and then 'blit' one more artist on top. Thus, by
Expand Down
183 changes: 183 additions & 0 deletions tutorials/intermediate/blitting.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
"""
=================
Blitting Tutorial
=================
'Blitting' is a `standard technique
<https://en.wikipedia.org/wiki/Bit_blit>`__ in computer graphics that
in the context of matplotlib can be used to (drastically) improve
performance of interactive figures. It is used internally by the
:mod:`~.animation` and :mod:`~.widgets` modules for this reason.
The source of the performance gains is simply not re-doing work we do
not have to. For example, if the limits of an Axes have not changed,
then there is no reason we should re-draw all of the ticks and
tick-labels (particularly because text is one of the more expensive
things to render).
The procedure to save our work is roughly:
- draw the figure, but exclude an artists marked as 'animated'
- save a copy of the Agg RBGA buffer
In the future, to update the 'animated' artists we
- restore our copy of the RGBA buffer
- redraw only the animated artists
- show the resulting image on the screen
thus saving us from having to re-draw everything which is _not_
animated.
Simple Example
--------------
We can implement this via methods on `.CanvasAgg` and setting
``animated=True`` on our artist.
"""

import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(0, 2*np.pi, 100)

fig, ax = plt.subplots()
# animated=True makes the artist be excluded from normal draw tree
ln, = ax.plot(x, np.sin(x), animated=True)

# stop to admire our empty window axes and ensure it is drawn
plt.pause(.1)

# save a copy of the image sans animated artist
bg = fig.canvas.copy_from_bbox(fig.bbox)
# draw the animated artist
ax.draw_artist(ln)
# show the result to the screen
fig.canvas.blit(fig.bbox)

for j in range(100):
# put the un-changed background back
fig.canvas.restore_region(bg)
# update the artist.
ln.set_ydata(np.sin(x + (j / 100) * np.pi))
# re-render the artist
ax.draw_artist(ln)
# copy the result to the screen
fig.canvas.blit(fig.bbox)


###############################################################################
# This example works and shows a simple animation, however because we are only
# grabbing the background once, if the size of dpi of the figure change, the
# background will be invalid and result in incorrect images. There is also a
# global variable and a fair amount of boiler plate which suggests we should
# wrap this in a class.
#
# Class-based example
# -------------------
#
# We can use a class to encapsulate the boilerplate logic and state of
# restoring the background, drawing the artists, and then blitting the
# result to the screen. Additionally, we can use the ``'draw_event'``
# callback to capture a new background whenever a full re-draw
# happens.


class BlitManager:

def __init__(self, canvas, animated_artists):
"""
Parameters
----------
canvas : CanvasAgg
The canvas to work with, this only works for sub-classes of the Agg
canvas which have the `~CanvasAgg.copy_from_bbox` and
`~CanvasAgg.restore_region` methods.
animated_artists : Optional[List[Artist]]
List of the artists to manage
"""
self.canvas = canvas
self._bg = None
self._artists = []

for a in animated_artists:
self.add_artist(a)
# grab the background on every draw
self.cid = canvas.mpl_connect('draw_event', self.on_draw)

def on_draw(self, event):
"""Callback to register with 'draw_event'
"""
cv = self.canvas
if event is not None:
if event.canvas != cv:
raise RuntimeError
self._bg = cv.copy_from_bbox(cv.figure.bbox)
self._draw_animated()

def add_artist(self, art):
"""Add a artist to be managed
Parameters
----------
art : Artist
The artist to be added. Will be set to 'animated' (just to be safe).
*art* must be in the figure associated with the canvas this class
is managing.
"""
if art.figure != self.canvas.figure:
raise RuntimeError
art.set_animated(True)
self._artists.append(art)

def _draw_animated(self):
"""Draw all of the animated artists
"""
fig = self.canvas.figure
for a in self._artists:
fig.draw_artist(a)

def update(self):
"""Update the screen with animated artists
"""
cv = self.canvas
fig = cv.figure
# paranoia in case we missed the draw event,
if self._bg is None:
self.on_draw(None)
else:
# restore the old background
cv.restore_region(self._bg)
# draw all of the animated artists
self._draw_animated()
# update the screen
cv.blit(fig.bbox)
# let the GUI event loop process anything it has to do
cv.flush_events()


###############################################################################
# And now use our class. This is a slightly more complicated example of the
# first case as we add a text frame counter as well.

# make a new figure
fig, ax = plt.subplots()
# add a line
ln, = ax.plot(x, np.sin(x), animated=True)
# add a frame number
fr_number = ax.annotate('0', (0, 1),
xycoords='axes fraction',
xytext=(10, -10),
textcoords='offset points',
ha='left', va='top',
animated=True)
bm = BlitManager(fig.canvas, [ln, fr_number])
plt.pause(.1)

for j in range(100):
# update the artists
ln.set_ydata(np.sin(x + (j / 100) * np.pi))
fr_number.set_text('frame: {j}'.format(j=j))
# tell the blitting manager to do it's thing
bm.update()

0 comments on commit 4192f98

Please sign in to comment.