-
-
Notifications
You must be signed in to change notification settings - Fork 7.9k
DOC: add a blitting tutorial #10187
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
DOC: add a blitting tutorial #10187
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,224 @@ | ||
""" | ||
================= | ||
Blitting tutorial | ||
================= | ||
|
||
'Blitting' is a `standard technique | ||
<https://en.wikipedia.org/wiki/Bit_blit>`__ in raster graphics that, | ||
in the context of Matplotlib, can be used to (drastically) improve | ||
performance of interactive figures. For example, the | ||
:mod:`~.animation` and :mod:`~.widgets` modules use blitting | ||
internally. Here, we demonstrate how to implement your own blitting, outside | ||
of these classes. | ||
|
||
The source of the performance gains is simply not re-doing work we do | ||
not have to. If the limits of an Axes have not changed, then there is | ||
no need to 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 any artists marked as 'animated' | ||
- save a copy of the 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. One consequence of this procedure is that your animated | ||
artists are always drawn at a higher z-order than the static artists. | ||
|
||
Not all backends support blitting. You can check if a given canvas does via | ||
the `.FigureCanvasBase.supports_blit` property. | ||
|
||
.. warning:: | ||
|
||
This code does not work with the OSX backend (but does work with other | ||
GUI backends on mac). | ||
|
||
Minimal example | ||
--------------- | ||
|
||
We can use the `.FigureCanvasAgg` methods | ||
`~.FigureCanvasAgg.copy_from_bbox` and | ||
`~.FigureCanvasAgg.restore_region` in conjunction with setting | ||
``animated=True`` on our artist to implement a minimal example that | ||
uses blitting to accelerate rendering | ||
|
||
""" | ||
|
||
import matplotlib.pyplot as plt | ||
import numpy as np | ||
|
||
x = np.linspace(0, 2 * np.pi, 100) | ||
|
||
fig, ax = plt.subplots() | ||
|
||
# animated=True tells matplotlib to only draw the artist when we | ||
# explicitly request it | ||
(ln,) = ax.plot(x, np.sin(x), animated=True) | ||
|
||
# make sure the window is raised, but the script keeps going | ||
plt.show(block=False) | ||
|
||
# stop to admire our empty window axes and ensure it is rendered at | ||
# least once. | ||
# | ||
# We need to fully draw the figure at its final size on the screen | ||
# before we continue on so that : | ||
# a) we have the correctly sized and drawn background to grab | ||
# b) we have a cached renderer so that ``ax.draw_artist`` works | ||
# so we spin the event loop to let the backend process any pending operations | ||
plt.pause(0.1) | ||
|
||
# get copy of entire figure (everything inside fig.bbox) sans animated artist | ||
bg = fig.canvas.copy_from_bbox(fig.bbox) | ||
# draw the animated artist, this uses a cached renderer | ||
ax.draw_artist(ln) | ||
# show the result to the screen, this pushes the updated RGBA buffer from the | ||
# renderer to the GUI framework so you can see it | ||
fig.canvas.blit(fig.bbox) | ||
|
||
for j in range(100): | ||
# reset the background back in the canvas state, screen unchanged | ||
fig.canvas.restore_region(bg) | ||
# update the artist, neither the canvas state nor the screen have changed | ||
ln.set_ydata(np.sin(x + (j / 100) * np.pi)) | ||
# re-render the artist, updating the canvas state, but not the screen | ||
ax.draw_artist(ln) | ||
# copy the image to the GUI state, but screen might not changed yet | ||
fig.canvas.blit(fig.bbox) | ||
# flush any pending GUI events, re-painting the screen if needed | ||
fig.canvas.flush_events() | ||
# you can put a pause in if you want to slow things down | ||
# plt.pause(.1) | ||
|
||
############################################################################### | ||
# This example works and shows a simple animation, however because we | ||
# are only grabbing the background once, if the size of the figure in | ||
# pixels changes (due to either the size or dpi of the figure | ||
# changing) , the background will be invalid and result in incorrect | ||
# (but sometimes cool looking!) 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 to handle resizes correctly. | ||
|
||
|
||
class BlitManager: | ||
def __init__(self, canvas, animated_artists=()): | ||
""" | ||
Parameters | ||
---------- | ||
canvas : FigureCanvasAgg | ||
The canvas to work with, this only works for sub-classes of the Agg | ||
canvas which have the `~FigureCanvasAgg.copy_from_bbox` and | ||
`~FigureCanvasAgg.restore_region` methods. | ||
|
||
animated_artists : Iterable[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 an 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 background | ||
cv.restore_region(self._bg) | ||
# draw all of the animated artists | ||
self._draw_animated() | ||
# update the GUI state | ||
cv.blit(fig.bbox) | ||
# let the GUI event loop process anything it has to do | ||
cv.flush_events() | ||
|
||
|
||
############################################################################### | ||
# Here is how we would use our class. This is a slightly more complicated | ||
# example than 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]) | ||
# make sure our window is on the screen and drawn | ||
plt.show(block=False) | ||
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() | ||
|
||
############################################################################### | ||
# This class does not depend on `.pyplot` and is suitable to embed | ||
# into larger GUI application. |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since this example code cannot really be actually used (it would fail if the screen is resized, for example), and now we explain blitting much more in-depth in a separate tutorial, what is the purpose of having a more complicated version of what amounts to pseudocode here?
Either way, we should make "handle 'blitting'" above a link to the blitting tutorial. But once that link is there, does the average
Animation
user gain anything except for an unsolicited lesson on rendering by being shown an outline of how blitting works?I think if the point is to convince people to set
blit=True
, then we should just mention that we always suggest doing so, and to see the tutorial for details on the implementation if you're interested.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the goal of the mini-blitting intro here is to justify why the signature of the callback in
FuncAnimation
is what it is.I don't disagree that this should be re-written, but would rather not do it in this (already pretty old) PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The other ways of doing the callback function should also be written out...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I guess my top-level comment is that I don't feel like that sections adds anything that you wouldn't also get if you just changed
to
And, frankly, the docs feel like they be equally useful with that existing section totally removed now that you have a blitting tutorial, even without the above change.
Now that you've added a blitting tutorial, it feels like it detracts from the docs by serving as a lot of unnecessary "noise" to describe something that the user doesn't even need to understand to correctly use the function.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm going to stick to doing this removal in a second pass.