Renderizado más rápido usando blitting #

Blitting es una técnica estándar en gráficos de trama que, en el contexto de Matplotlib, se puede utilizar para mejorar (drásticamente) el rendimiento de las figuras interactivas. Por ejemplo, los módulos animationy widgetsusan blitting internamente. Aquí, demostramos cómo implementar su propio blitting, fuera de estas clases.

Blit acelera el dibujo repetitivo al convertir todos los elementos gráficos que no cambian en una imagen de fondo una vez. Luego, para cada dibujo, solo los elementos cambiantes deben dibujarse en este fondo. Por ejemplo, si los límites de un eje no han cambiado, podemos representar los ejes vacíos, incluidas todas las marcas y etiquetas, una vez, y solo dibujar los datos cambiantes más tarde.

la estrategia es

  • Prepare el fondo constante:

    • Dibuja la figura, pero excluye a todos los artistas que quieras animar marcándolos como animados (ver Artist.set_animated).

    • Guarde una copia del búfer RBGA.

  • Renderice las imágenes individuales:

Una consecuencia de este procedimiento es que sus artistas animados siempre se dibujan encima de los artistas estáticos.

No todos los backends admiten blitting. Puede verificar si un lienzo determinado lo hace a través de la FigureCanvasBase.supports_blitpropiedad.

Advertencia

Este código no funciona con el backend de OSX (pero sí funciona con otros backends de GUI en Mac).

Ejemplo mínimo #

Podemos usar los FigureCanvasAggmétodos copy_from_bboxy restore_regionjunto con la configuración animated=Truede nuestro artista para implementar un ejemplo mínimo que use blitting para acelerar el renderizado.

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 be 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)
borrando

Este ejemplo funciona y muestra una animación simple, sin embargo, debido a que solo tomamos el fondo una vez, si el tamaño de la figura en píxeles cambia (ya sea porque cambia el tamaño o los dpi de la figura), el fondo no será válido y dará como resultado imágenes incorrectas (¡pero a veces geniales!). También hay una variable global y una buena cantidad de placa de caldera que sugiere que deberíamos envolver esto en una clase.

Ejemplo basado en clases #

Podemos usar una clase para encapsular la lógica repetitiva y el estado de restaurar el fondo, dibujar a los artistas y luego mostrar el resultado en la pantalla. Además, podemos usar la 'draw_event' devolución de llamada para capturar un nuevo fondo siempre que se redibuje por completo para manejar los cambios de tamaño correctamente.

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()

Así es como usaríamos nuestra clase. Este es un ejemplo un poco más complicado que el primer caso, ya que también agregamos un contador de marco de texto.

# 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 its thing
    bm.update()
borrando

Esta clase no depende pyploty es adecuada para integrarse en una aplicación GUI más grande.

Tiempo total de ejecución del script: (0 minutos 1,185 segundos)

Galería generada por Sphinx-Gallery