From 00851f775ccfe6e78eab1ac020b48a38614e2307 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sat, 6 Jan 2018 23:04:57 -0500 Subject: [PATCH 1/3] DOC: add a blitting tutorial --- doc/api/animation_api.rst | 2 +- tutorials/intermediate/blitting.py | 183 +++++++++++++++++++++++++++++ 2 files changed, 184 insertions(+), 1 deletion(-) create mode 100644 tutorials/intermediate/blitting.py diff --git a/doc/api/animation_api.rst b/doc/api/animation_api.rst index 9486dfe289c7..c8edde884046 100644 --- a/doc/api/animation_api.rst +++ b/doc/api/animation_api.rst @@ -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 `__ 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 diff --git a/tutorials/intermediate/blitting.py b/tutorials/intermediate/blitting.py new file mode 100644 index 000000000000..892eeca87911 --- /dev/null +++ b/tutorials/intermediate/blitting.py @@ -0,0 +1,183 @@ +""" +================= +Blitting Tutorial +================= + +'Blitting' is a `standard technique +`__ 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() From a549e107a90f8c1265bb1f86161e88a2590a8c61 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sat, 16 May 2020 18:27:40 -0400 Subject: [PATCH 2/3] DOC: copy edit tutorial Co-authored-by: Elliott Sales de Andrade Co-authored-by: Bruno Beltran Co-authored-by: Tim Hoffmann<2836374+timhoffm@users.noreply.github.com> --- lib/matplotlib/backend_bases.py | 1 + tutorials/intermediate/blitting.py | 167 ++++++++++++++++++----------- 2 files changed, 105 insertions(+), 63 deletions(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 43dc285f4fb9..85dd5f9c4366 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -1671,6 +1671,7 @@ class FigureCanvasBase: @cbook._classproperty def supports_blit(cls): + """If this Canvas sub-class supports blitting.""" return (hasattr(cls, "copy_from_bbox") and hasattr(cls, "restore_region")) diff --git a/tutorials/intermediate/blitting.py b/tutorials/intermediate/blitting.py index 892eeca87911..25b430fa2f29 100644 --- a/tutorials/intermediate/blitting.py +++ b/tutorials/intermediate/blitting.py @@ -1,24 +1,25 @@ """ ================= -Blitting Tutorial +Blitting tutorial ================= 'Blitting' is a `standard technique -`__ 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. +`__ 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. 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). +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 an artists marked as 'animated' -- save a copy of the Agg RBGA buffer +- 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 @@ -27,50 +28,81 @@ - show the resulting image on the screen thus saving us from having to re-draw everything which is _not_ -animated. +animated. One consequence of this procedure is that your animated +artists are always drawn at a higher z-order than the static artists. -Simple Example --------------- +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 -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) +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) +# animated=True tells matplotlib to only draw the artist when we +# explicitly request it +(ln,) = ax.plot(x, np.sin(x), animated=True) -# save a copy of the image sans animated artist +# 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 +# draw the animated artist, this uses a cached renderer ax.draw_artist(ln) -# show the result to the screen +# 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): - # put the un-changed background back + # reset the background back in the canvas state, screen unchanged fig.canvas.restore_region(bg) - # update the artist. + # 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 + # re-render the artist, updating the canvas state, but not the screen ax.draw_artist(ln) - # copy the result to the screen + # 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 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 +# 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 @@ -80,21 +112,20 @@ # 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. +# happens to handle resizes correctly. class BlitManager: - - def __init__(self, canvas, animated_artists): + def __init__(self, canvas, animated_artists=()): """ Parameters ---------- - canvas : CanvasAgg + canvas : FigureCanvasAgg 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. + canvas which have the `~FigureCanvasAgg.copy_from_bbox` and + `~FigureCanvasAgg.restore_region` methods. - animated_artists : Optional[List[Artist]] + animated_artists : Iterable[Artist] List of the artists to manage """ self.canvas = canvas @@ -104,11 +135,10 @@ def __init__(self, canvas, animated_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) + self.cid = canvas.mpl_connect("draw_event", self.on_draw) def on_draw(self, event): - """Callback to register with 'draw_event' - """ + """Callback to register with 'draw_event'.""" cv = self.canvas if event is not None: if event.canvas != cv: @@ -117,14 +147,17 @@ def on_draw(self, event): self._draw_animated() def add_artist(self, art): - """Add a artist to be managed + """ + 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. + + 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 @@ -132,52 +165,60 @@ def add_artist(self, art): self._artists.append(art) def _draw_animated(self): - """Draw all of the animated artists - """ + """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 - """ + """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 + # restore the background cv.restore_region(self._bg) # draw all of the animated artists self._draw_animated() - # update the screen + # update the GUI state 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. +# 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) +(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) +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)) + 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. From bb9afe7df0fc87a4a36d4b57b3548684323186bb Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Mon, 15 Jun 2020 22:59:00 -0400 Subject: [PATCH 3/3] DOC: move blitting tutorial from intermediate to advanced --- tutorials/{intermediate => advanced}/blitting.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tutorials/{intermediate => advanced}/blitting.py (100%) diff --git a/tutorials/intermediate/blitting.py b/tutorials/advanced/blitting.py similarity index 100% rename from tutorials/intermediate/blitting.py rename to tutorials/advanced/blitting.py