Skip to content

Commit 70b87e9

Browse files
committed
DOC: add a blitting tutorial
1 parent c3bfeb9 commit 70b87e9

File tree

2 files changed

+184
-1
lines changed

2 files changed

+184
-1
lines changed

doc/api/animation_api.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ performance), to be non-blocking, not repeatedly start/stop the GUI
5555
event loop, handle repeats, multiple animated axes, and easily save
5656
the animation to a movie file.
5757

58-
'Blitting' is a `old technique
58+
'Blitting' is a `standard technique
5959
<https://en.wikipedia.org/wiki/Bit_blit>`__ in computer graphics. The
6060
general gist is to take an existing bit map (in our case a mostly
6161
rasterized figure) and then 'blit' one more artist on top. Thus, by

tutorials/intermediate/blitting.py

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
"""
2+
=================
3+
Blitting Tutorial
4+
=================
5+
6+
'Blitting' is a `standard technique
7+
<https://en.wikipedia.org/wiki/Bit_blit>`__ in computer graphics that
8+
in the context of matplotlib can be used to (drastically) improve
9+
performance of interactive figures. It is used internally by the
10+
:mod:`~.animation` and :mod:`~.widgets` modules for this reason.
11+
12+
The source of the performance gains is simply not re-doing work we do
13+
not have to. For example, if the limits of an Axes have not changed,
14+
then there is no reason we should re-draw all of the ticks and
15+
tick-labels (particularly because text is one of the more expensive
16+
things to render).
17+
18+
The procedure to save our work is roughly:
19+
20+
- draw the figure, but exclude an artists marked as 'animated'
21+
- save a copy of the Agg RBGA buffer
22+
23+
In the future, to update the 'animated' artists we
24+
25+
- restore our copy of the RGBA buffer
26+
- redraw only the animated artists
27+
- show the resulting image on the screen
28+
29+
thus saving us from having to re-draw everything which is _not_
30+
animated.
31+
32+
Simple Example
33+
--------------
34+
35+
We can implement this via methods on `.CanvasAgg` and setting
36+
``animated=True`` on our artist.
37+
"""
38+
39+
import matplotlib.pyplot as plt
40+
import numpy as np
41+
42+
x = np.linspace(0, 2*np.pi, 100)
43+
44+
fig, ax = plt.subplots()
45+
# animated=True makes the artist be excluded from normal draw tree
46+
ln, = ax.plot(x, np.sin(x), animated=True)
47+
48+
# stop to admire our empty window axes and ensure it is drawn
49+
plt.pause(.1)
50+
51+
# save a copy of the image sans animated artist
52+
bg = fig.canvas.copy_from_bbox(fig.bbox)
53+
# draw the animated artist
54+
ax.draw_artist(ln)
55+
# show the result to the screen
56+
fig.canvas.blit(fig.bbox)
57+
58+
for j in range(100):
59+
# put the un-changed background back
60+
fig.canvas.restore_region(bg)
61+
# update the artist.
62+
ln.set_ydata(np.sin(x + (j / 100) * np.pi))
63+
# re-render the artist
64+
ax.draw_artist(ln)
65+
# copy the result to the screen
66+
fig.canvas.blit(fig.bbox)
67+
68+
69+
###############################################################################
70+
# This example works and shows a simple animation, however because we are only
71+
# grabbing the background once, if the size of dpi of the figure change, the
72+
# background will be invalid and result in incorrect images. There is also a
73+
# global variable and a fair amount of boiler plate which suggests we should
74+
# wrap this in a class.
75+
#
76+
# Class-based example
77+
# -------------------
78+
#
79+
# We can use a class to encapsulate the boilerplate logic and state of
80+
# restoring the background, drawing the artists, and then blitting the
81+
# result to the screen. Additionally, we can use the ``'draw_event'``
82+
# callback to capture a new background whenever a full re-draw
83+
# happens.
84+
85+
86+
class BlitManager:
87+
88+
def __init__(self, canvas, animated_artists):
89+
"""
90+
Parameters
91+
----------
92+
canvas : CanvasAgg
93+
The canvas to work with, this only works for sub-classes of the Agg
94+
canvas which have the `~CanvasAgg.copy_from_bbox` and
95+
`~CanvasAgg.restore_region` methods.
96+
97+
animated_artists : Optional[List[Artist]]
98+
List of the artists to manage
99+
"""
100+
self.canvas = canvas
101+
self._bg = None
102+
self._artists = []
103+
104+
for a in animated_artists:
105+
self.add_artist(a)
106+
# grab the background on every draw
107+
self.cid = canvas.mpl_connect('draw_event', self.on_draw)
108+
109+
def on_draw(self, event):
110+
"""Callback to register with 'draw_event'
111+
"""
112+
cv = self.canvas
113+
if event is not None:
114+
if event.canvas != cv:
115+
raise RuntimeError
116+
self._bg = cv.copy_from_bbox(cv.figure.bbox)
117+
self._draw_animated()
118+
119+
def add_artist(self, art):
120+
"""Add a artist to be managed
121+
122+
Parameters
123+
----------
124+
art : Artist
125+
The artist to be added. Will be set to 'animated' (just to be safe).
126+
*art* must be in the figure associated with the canvas this class
127+
is managing.
128+
"""
129+
if art.figure != self.canvas.figure:
130+
raise RuntimeError
131+
art.set_animated(True)
132+
self._artists.append(art)
133+
134+
def _draw_animated(self):
135+
"""Draw all of the animated artists
136+
"""
137+
fig = self.canvas.figure
138+
for a in self._artists:
139+
fig.draw_artist(a)
140+
141+
def update(self):
142+
"""Update the screen with animated artists
143+
"""
144+
cv = self.canvas
145+
fig = cv.figure
146+
# paranoia in case we missed the draw event,
147+
if self._bg is None:
148+
self.on_draw(None)
149+
else:
150+
# restore the old background
151+
cv.restore_region(self._bg)
152+
# draw all of the animated artists
153+
self._draw_animated()
154+
# update the screen
155+
cv.blit(fig.bbox)
156+
# let the GUI event loop process anything it has to do
157+
cv.flush_events()
158+
159+
160+
###############################################################################
161+
# And now use our class. This is a slightly more complicated example of the
162+
# first case as we add a text frame counter as well.
163+
164+
# make a new figure
165+
fig, ax = plt.subplots()
166+
# add a line
167+
ln, = ax.plot(x, np.sin(x), animated=True)
168+
# add a frame number
169+
fr_number = ax.annotate('0', (0, 1),
170+
xycoords='axes fraction',
171+
xytext=(10, -10),
172+
textcoords='offset points',
173+
ha='left', va='top',
174+
animated=True)
175+
bm = BlitManager(fig.canvas, [ln, fr_number])
176+
plt.pause(.1)
177+
178+
for j in range(100):
179+
# update the artists
180+
ln.set_ydata(np.sin(x + (j / 100) * np.pi))
181+
fr_number.set_text('frame: {j}'.format(j=j))
182+
# tell the blitting manager to do it's thing
183+
bm.update()

0 commit comments

Comments
 (0)