Rendering più veloce utilizzando il blitting #

Il blitting è una tecnica standard nella grafica raster che, nel contesto di Matplotlib, può essere utilizzata per migliorare (drasticamente) le prestazioni delle figure interattive. Ad esempio, i moduli animatione widgetsutilizzano il blitting internamente. Qui dimostriamo come implementare il tuo blitting, al di fuori di queste classi.

Il blitting velocizza il disegno ripetitivo eseguendo il rendering di tutti gli elementi grafici immutabili in un'immagine di sfondo una volta. Quindi, per ogni disegno, solo gli elementi che cambiano devono essere disegnati su questo sfondo. Ad esempio, se i limiti di un Axes non sono cambiati, possiamo eseguire il rendering degli Axes vuoti includendo tutti i segni di spunta e le etichette una volta e disegnare i dati che cambiano solo in un secondo momento.

La strategia è

  • Preparare lo sfondo costante:

    • Disegna la figura, ma escludi tutti gli artisti che desideri animare contrassegnandoli come animati (vedi Artist.set_animated).

    • Salva una copia del buffer RBGA.

  • Rendering delle singole immagini:

Una conseguenza di questa procedura è che i tuoi artisti animati sono sempre disegnati sopra gli artisti statici.

Non tutti i backend supportano il blitting. Puoi verificare se una determinata tela funziona tramite la FigureCanvasBase.supports_blitproprietà.

Avvertimento

Questo codice non funziona con il backend OSX (ma funziona con altri backend GUI su Mac).

Esempio minimo #

Possiamo utilizzare i FigureCanvasAggmetodi copy_from_bboxe restore_regionin combinazione con l'impostazione animated=Truedel nostro artista per implementare un esempio minimo che utilizza il blitting per accelerare il 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 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)
blitting

Questo esempio funziona e mostra una semplice animazione, tuttavia poiché stiamo catturando lo sfondo solo una volta, se la dimensione della figura in pixel cambia (a causa della modifica della dimensione o del dpi della figura), lo sfondo non sarà valido e risulterà in immagini errate (ma a volte interessanti!). C'è anche una variabile globale e una discreta quantità di boilerplate che suggerisce che dovremmo racchiuderlo in una classe.

Esempio basato sulla classe #

Possiamo utilizzare una classe per incapsulare la logica standard e lo stato di ripristino dello sfondo, disegnare gli artisti e quindi trasferire il risultato sullo schermo. Inoltre, possiamo utilizzare il 'draw_event' callback per acquisire un nuovo sfondo ogni volta che si verifica un ridisegno completo per gestire correttamente i ridimensionamenti.

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

Ecco come useremmo la nostra classe. Questo è un esempio leggermente più complicato rispetto al primo caso in quanto aggiungiamo anche un contatore di frame di testo.

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

Questa classe non dipende da pyploted è adatta per essere incorporata in un'applicazione GUI più grande.

Tempo di esecuzione totale dello script: (0 minuti 1,185 secondi)

Galleria generata da Sphinx-Gallery