Renderização mais rápida usando blitting #

Blitting é uma técnica padrão em gráficos raster que, no contexto do Matplotlib, pode ser usado para (drasticamente) melhorar o desempenho de figuras interativas. Por exemplo, os módulos animatione widgetsusam blitting internamente. Aqui, demonstramos como implementar seu próprio blitting, fora dessas classes.

O blitting acelera o desenho repetitivo renderizando todos os elementos gráficos inalteráveis ​​em uma imagem de fundo uma vez. Então, para cada desenho, apenas os elementos que mudam precisam ser desenhados neste fundo. Por exemplo, se os limites de um Axes não foram alterados, podemos renderizar os Axes vazios, incluindo todos os ticks e rótulos uma vez, e apenas desenhar os dados alterados mais tarde.

A estratégia é

  • Prepare o fundo constante:

    • Desenhe a figura, mas exclua todos os artistas que deseja animar marcando-os como animados (consulte Recursos Artist.set_animated).

    • Salve uma cópia do buffer RBGA.

  • Renderize as imagens individuais:

Uma consequência desse procedimento é que seus artistas animados são sempre desenhados em cima dos artistas estáticos.

Nem todos os back-ends suportam blitting. Você pode verificar se uma determinada tela funciona por meio da FigureCanvasBase.supports_blitpropriedade.

Aviso

Este código não funciona com o back-end do OSX (mas funciona com outros back-ends da GUI no mac).

Exemplo mínimo #

Podemos usar os FigureCanvasAggmétodos copy_from_bboxe restore_regionem conjunto com a configuração animated=Trueem nosso artista para implementar um exemplo mínimo que usa blitting para acelerar a renderização

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

Este exemplo funciona e mostra uma animação simples, no entanto, como estamos capturando o plano de fundo apenas uma vez, se o tamanho da figura em pixels mudar (devido à mudança de tamanho ou dpi da figura), o plano de fundo será inválido e resultará em imagens incorretas (mas às vezes legais!). Há também uma variável global e uma boa quantidade de clichê que sugere que devemos agrupar isso em uma classe.

Exemplo baseado em classe #

Podemos usar uma classe para encapsular a lógica padronizada e o estado de restauração do plano de fundo, desenhando os artistas e, em seguida, exibindo o resultado na tela. Além disso, podemos usar o 'draw_event' retorno de chamada para capturar um novo plano de fundo sempre que um redesenho completo acontecer para lidar com os redimensionamentos corretamente.

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

Aqui está como usaríamos nossa classe. Este é um exemplo um pouco mais complicado do que o primeiro caso, pois também adicionamos um contador de quadro 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()
blitting

Esta classe não depende pyplote é adequada para incorporar em um aplicativo GUI maior.

Tempo total de execução do script: ( 0 minutos 1,185 segundos)

Galeria gerada por Sphinx-Gallery