{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "\n.. redirect-from:: /tutorials/advanced/blitting\n\n\n# Faster rendering by using blitting\n\n*Blitting* is a [standard technique](https://en.wikipedia.org/wiki/Bit_blit)_ in raster graphics that,\nin the context of Matplotlib, can be used to (drastically) improve\nperformance of interactive figures. For example, the\n:mod:`.animation` and :mod:`.widgets` modules use blitting\ninternally. Here, we demonstrate how to implement your own blitting, outside\nof these classes.\n\nBlitting speeds up repetitive drawing by rendering all non-changing\ngraphic elements into a background image once. Then, for every draw, only the\nchanging elements need to be drawn onto this background. For example,\nif the limits of an Axes have not changed, we can render the empty Axes\nincluding all ticks and labels once, and only draw the changing data later.\n\nThe strategy is\n\n- Prepare the constant background:\n\n - Draw the figure, but exclude all artists that you want to animate by\n marking them as *animated* (see `.Artist.set_animated`).\n - Save a copy of the RBGA buffer.\n\n- Render the individual images:\n\n - Restore the copy of the RGBA buffer.\n - Redraw the animated artists using `.Axes.draw_artist` /\n `.Figure.draw_artist`.\n - Show the resulting image on the screen.\n\nOne consequence of this procedure is that your animated artists are always\ndrawn on top of the static artists.\n\nNot all backends support blitting. You can check if a given canvas does via\nthe `.FigureCanvasBase.supports_blit` property.\n\n

Warning

This code does not work with the macosx backend (but does work with other\n GUI backends on Mac).

\n\n## Minimal example\n\nWe can use the `.FigureCanvasAgg` methods\n`~.FigureCanvasAgg.copy_from_bbox` and\n`~.FigureCanvasAgg.restore_region` in conjunction with setting\n``animated=True`` on our artist to implement a minimal example that\nuses blitting to accelerate rendering\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "import matplotlib.pyplot as plt\nimport numpy as np\n\nx = np.linspace(0, 2 * np.pi, 100)\n\nfig, ax = plt.subplots()\n\n# animated=True tells matplotlib to only draw the artist when we\n# explicitly request it\n(ln,) = ax.plot(x, np.sin(x), animated=True)\n\n# make sure the window is raised, but the script keeps going\nplt.show(block=False)\n\n# stop to admire our empty window axes and ensure it is rendered at\n# least once.\n#\n# We need to fully draw the figure at its final size on the screen\n# before we continue on so that :\n# a) we have the correctly sized and drawn background to grab\n# b) we have a cached renderer so that ``ax.draw_artist`` works\n# so we spin the event loop to let the backend process any pending operations\nplt.pause(0.1)\n\n# get copy of entire figure (everything inside fig.bbox) sans animated artist\nbg = fig.canvas.copy_from_bbox(fig.bbox)\n# draw the animated artist, this uses a cached renderer\nax.draw_artist(ln)\n# show the result to the screen, this pushes the updated RGBA buffer from the\n# renderer to the GUI framework so you can see it\nfig.canvas.blit(fig.bbox)\n\nfor j in range(100):\n # reset the background back in the canvas state, screen unchanged\n fig.canvas.restore_region(bg)\n # update the artist, neither the canvas state nor the screen have changed\n ln.set_ydata(np.sin(x + (j / 100) * np.pi))\n # re-render the artist, updating the canvas state, but not the screen\n ax.draw_artist(ln)\n # copy the image to the GUI state, but screen might not be changed yet\n fig.canvas.blit(fig.bbox)\n # flush any pending GUI events, re-painting the screen if needed\n fig.canvas.flush_events()\n # you can put a pause in if you want to slow things down\n # plt.pause(.1)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This example works and shows a simple animation, however because we\nare only grabbing the background once, if the size of the figure in\npixels changes (due to either the size or dpi of the figure\nchanging) , the background will be invalid and result in incorrect\n(but sometimes cool looking!) images. There is also a global\nvariable and a fair amount of boilerplate which suggests we should\nwrap this in a class.\n\n## Class-based example\n\nWe can use a class to encapsulate the boilerplate logic and state of\nrestoring the background, drawing the artists, and then blitting the\nresult to the screen. Additionally, we can use the ``'draw_event'``\ncallback to capture a new background whenever a full re-draw\nhappens to handle resizes correctly.\n\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "class BlitManager:\n def __init__(self, canvas, animated_artists=()):\n \"\"\"\n Parameters\n ----------\n canvas : FigureCanvasAgg\n The canvas to work with, this only works for subclasses of the Agg\n canvas which have the `~FigureCanvasAgg.copy_from_bbox` and\n `~FigureCanvasAgg.restore_region` methods.\n\n animated_artists : Iterable[Artist]\n List of the artists to manage\n \"\"\"\n self.canvas = canvas\n self._bg = None\n self._artists = []\n\n for a in animated_artists:\n self.add_artist(a)\n # grab the background on every draw\n self.cid = canvas.mpl_connect(\"draw_event\", self.on_draw)\n\n def on_draw(self, event):\n \"\"\"Callback to register with 'draw_event'.\"\"\"\n cv = self.canvas\n if event is not None:\n if event.canvas != cv:\n raise RuntimeError\n self._bg = cv.copy_from_bbox(cv.figure.bbox)\n self._draw_animated()\n\n def add_artist(self, art):\n \"\"\"\n Add an artist to be managed.\n\n Parameters\n ----------\n art : Artist\n\n The artist to be added. Will be set to 'animated' (just\n to be safe). *art* must be in the figure associated with\n the canvas this class is managing.\n\n \"\"\"\n if art.figure != self.canvas.figure:\n raise RuntimeError\n art.set_animated(True)\n self._artists.append(art)\n\n def _draw_animated(self):\n \"\"\"Draw all of the animated artists.\"\"\"\n fig = self.canvas.figure\n for a in self._artists:\n fig.draw_artist(a)\n\n def update(self):\n \"\"\"Update the screen with animated artists.\"\"\"\n cv = self.canvas\n fig = cv.figure\n # paranoia in case we missed the draw event,\n if self._bg is None:\n self.on_draw(None)\n else:\n # restore the background\n cv.restore_region(self._bg)\n # draw all of the animated artists\n self._draw_animated()\n # update the GUI state\n cv.blit(fig.bbox)\n # let the GUI event loop process anything it has to do\n cv.flush_events()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Here is how we would use our class. This is a slightly more complicated\nexample than the first case as we add a text frame counter as well.\n\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "# make a new figure\nfig, ax = plt.subplots()\n# add a line\n(ln,) = ax.plot(x, np.sin(x), animated=True)\n# add a frame number\nfr_number = ax.annotate(\n \"0\",\n (0, 1),\n xycoords=\"axes fraction\",\n xytext=(10, -10),\n textcoords=\"offset points\",\n ha=\"left\",\n va=\"top\",\n animated=True,\n)\nbm = BlitManager(fig.canvas, [ln, fr_number])\n# make sure our window is on the screen and drawn\nplt.show(block=False)\nplt.pause(.1)\n\nfor j in range(100):\n # update the artists\n ln.set_ydata(np.sin(x + (j / 100) * np.pi))\n fr_number.set_text(f\"frame: {j}\")\n # tell the blitting manager to do its thing\n bm.update()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This class does not depend on `.pyplot` and is suitable to embed\ninto larger GUI application.\n\n" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.13.2" } }, "nbformat": 4, "nbformat_minor": 0 }