8000 Merge pull request #10187 from tacaswell/doc_blitting_tutorial · matplotlib/matplotlib@4249e18 · GitHub
[go: up one dir, main page]

Skip to content

Commit 4249e18

Browse files
authored
Merge pull request #10187 from tacaswell/doc_blitting_tutorial
DOC: add a blitting tutorial
2 parents f8e4012 + bb9afe7 commit 4249e18

File tree

3 files changed

+226
-1
lines changed

3 files changed

+226
-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

lib/matplotlib/backend_bases.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1671,6 +1671,7 @@ class FigureCanvasBase:
16711671

16721672
@cbook._classproperty
16731673
def supports_blit(cls):
1674+
"""If this Canvas sub-class supports blitting."""
16741675
return (hasattr(cls, "copy_from_bbox")
16751676
and hasattr(cls, "restore_region"))
16761677

tutorials/advanced/blitting.py

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
"""
2+
=================
3+
Blitting tutorial
4+
=================
5+
6+
'Blitting' is a `standard technique
7+
<https://en.wikipedia.org/wiki/Bit_blit>`__ in raster graphics that,
8+
in the context of Matplotlib, can be used to (drastically) improve
9+
performance of interactive figures. For example, the
10+
:mod:`~.animation` and :mod:`~.widgets` modules use blitting
11+
internally. Here, we demonstrate how to implement your own blitting, outside
12+
of these classes.
13+
14+
The source of the performance gains is simply not re-doing work we do
15+
not have to. If the limits of an Axes have not changed, then there is
16+
no need to re-draw all of the ticks and tick-labels (particularly
17+
because text is one of the more expensive things to render).
18+
19+
The procedure to save our work is roughly:
20+
21+
- draw the figure, but exclude any artists marked as 'animated'
22+
- save a copy of the RBGA buffer
23+
24+
In the future, to update the 'animated' artists we
25+
26+
- restore our copy of the RGBA buffer
27+
- redraw only the animated artists
28+
- show the resulting image on the screen
29+
30+
thus saving us from having to re-draw everything which is _not_
31+
animated. One consequence of this procedure is that your animated
32+
artists are always drawn at a higher z-order than the static artists.
33+
34+
Not all backends support blitting. You can check if a given canvas does via
35+
the `.FigureCanvasBase.supports_blit` property.
36+
37+
.. warning::
38+
39+
This code does not work with the OSX backend (but does work with other
40+
GUI backends on mac).
41+
42+
Minimal example
43+
---------------
44+
45+
We can use the `.FigureCanvasAgg` methods
46+
`~.FigureCanvasAgg.copy_from_bbox` and
47+
`~.FigureCanvasAgg.restore_region` in conjunction with setting
48+
``animated=True`` on our artist to implement a minimal example that
49+
uses blitting to accelerate rendering
50+
51+
"""
52+
53+
import matplotlib.pyplot as plt
54+
import numpy as np
55+
56+
x = np.linspace(0, 2 67ED * np.pi, 100)
57+
58+
fig, ax = plt.subplots()
59+
60+
# animated=True tells matplotlib to only draw the artist when we
61+
# explicitly request it
62+
(ln,) = ax.plot(x, np.sin(x), animated=True)
63+
64+
# make sure the window is raised, but the script keeps going
65+
plt.show(block=False)
66+
67+
# stop to admire our empty window axes and ensure it is rendered at
68+
# least once.
69+
#
70+
# We need to fully draw the figure at its final size on the screen
71+
# before we continue on so that :
72+
# a) we have the correctly sized and drawn background to grab
73+
# b) we have a cached renderer so that ``ax.draw_artist`` works
74+
# so we spin the event loop to let the backend process any pending operations
75+
plt.pause(0.1)
76+
77+
# get copy of entire figure (everything inside fig.bbox) sans animated artist
78+
bg = fig.canvas.copy_from_bbox(fig.bbox)
79+
# draw the animated artist, this uses a cached renderer
80+
ax.draw_artist(ln)
81+
# show the result to the screen, this pushes the updated RGBA buffer from the
82+
# renderer to the GUI framework so you can see it
83+
fig.canvas.blit(fig.bbox)
84+
85+
for j in range(100):
86+
# reset the background back in the canvas state, screen unchanged
87+
fig.canvas.restore_region(bg)
88+
# update the artist, neither the canvas state nor the screen have changed
89+
ln.set_ydata(np.sin(x + (j / 100) * np.pi))
90+
# re-render the artist, updating the canvas state, but not the screen
91+
ax.draw_artist(ln)
92+
# copy the image to the GUI state, but screen might not changed yet
93+
fig.canvas.blit(fig.bbox)
94+
# flush any pending GUI events, re-painting the screen if needed
95+
fig.canvas.flush_events()
96+
# you can put a pause in if you want to slow things down
97+
# plt.pause(.1)
98+
99+
###############################################################################
100+
# This example works and shows a simple animation, however because we
101+
# are only grabbing the background once, if the size of the figure in
102+
# pixels changes (due to either the size or dpi of the figure
103+
# changing) , the background will be invalid and result in incorrect
104+
# (but sometimes cool looking!) images. There is also a global
105+
# variable and a fair amount of boiler plate which suggests we should
106+
# wrap this in a class.
107+
#
108+
# Class-based example
109+
# -------------------
110+
#
111+
# We can use a class to encapsulate the boilerplate logic and state of
112+
# restoring the background, drawing the artists, and then blitting the
113+
# result to the screen. Additionally, we can use the ``'draw_event'``
114+
# callback to capture a new background whenever a full re-draw
115+
# happens to handle resizes correctly.
116+
117+
118+
class BlitManager:
119+
def __init__(self, canvas, animated_artists=()):
120+
"""
121+
Parameters
122+
----------
123+
canvas : FigureCanvasAgg
124+
The canvas to work with, this only works for sub-classes of the Agg
125+
canvas which have the `~FigureCanvasAgg.copy_from_bbox` and
126+
`~FigureCanvasAgg.restore_region` methods.
127+
128+
animated_artists : Iterable[Artist]
129+
List of the artists to manage
130+
"""
131+
self.canvas = canvas
132+
self._bg = None
133+
self._artists = []
134+
135+
for a in animated_artists:
136+
self.add_artist(a)
137+
# grab the background on every draw
138+
self.cid = canvas.mpl_connect("draw_event", self.on_draw)
139+
140+
def on_draw(self, event):
141+
"""Callback to register with 'draw_event'."""
142+
cv = self.canvas
143+
if event is not None:
144+
if event.canvas != cv:
145+
raise RuntimeError
146+
self._bg = cv.copy_from_bbox(cv.figure.bbox)
147+
self._draw_animated()
148+
149+
def add_artist(self, art):
150+
"""
151+
Add an artist to be managed.
152+
153+
Parameters
154+
----------
155+
art : Artist
156+
157+
The artist to be added. Will be set to 'animated' (just
158+
to be safe). *art* must be in the figure associated with
159+
the canvas this class is managing.
160+
161+
"""
162+
if art.figure != self.canvas.figure:
163+
raise RuntimeError
164+
art.set_animated(True)
165+
self._artists.append(art)
166+
167+
def _draw_animated(self):
168+
"""Draw all of the animated artists."""
169+
fig = self.canvas.figure
170+
for a in self._artists:
171+
fig.draw_artist(a)
172+
173+
def update(self):
174+
"""Update the screen with animated artists."""
175+
cv = self.canvas
176+
fig = cv.figure
177+
# paranoia in case we missed the draw event,
178+
if self._bg is None:
179+
self.on_draw(None)
180+
else:
181+
# restore the background
182+
cv.restore_region(self._bg)
183+
# draw all of the animated artists
184+
self._draw_animated()
185+
# update the GUI state
186+
cv.blit(fig.bbox)
187+
# let the GUI event loop process anything it has to do
188+
cv.flush_events()
189+
190+
191+
###############################################################################
192+
# Here is how we would use our class. This is a slightly more complicated
193+
# example than the first case as we add a text frame counter as well.
194+
195+
# make a new figure
196+
fig, ax = plt.subplots()
197+
# add a line
198+
(ln,) = ax.plot(x, np.sin(x), animated=True)
199+
# add a frame number
200+
fr_number = ax.annotate(
201+
"0",
202+
(0, 1),
203+
xycoords="axes fraction",
204+
xytext=(10, -10),
205+
textcoords="offset points",
206+
ha="left",
207+
va="top",
208+
animated=True,
209+
)
210+
bm = BlitManager(fig.canvas, [ln, fr_number])
211+
# make sure our window is on the screen and drawn
212+
plt.show(block=False)
213+
plt.pause(.1)
214+
215+
for j in range(100):
216+
# update the artists
217+
ln.set_ydata(np.sin(x + (j / 100) * np.pi))
218+
fr_number.set_text("frame: {j}".format(j=j))
219+
# tell the blitting manager to do it's thing
220+
bm.update()
221+
222+
###############################################################################
223+
# This class does not depend on `.pyplot` and is suitable to embed
224+
# into larger GUI application.

0 commit comments

Comments
 (0)
0