Existe-t-il un moyen dans matplotlib de vérifier quels artistes se trouvent dans la zone actuellement affichée des axes?

9

J'ai un programme avec une figure interactive où de temps en temps de nombreux artistes sont dessinés. Dans cette figure, vous pouvez également zoomer et effectuer un panoramique à l'aide de la souris. Cependant, la performance lors du zoom d'un panoramique n'est pas très bonne car chaque artiste est toujours redessiné. Existe-t-il un moyen de vérifier quels artistes se trouvent dans la zone actuellement affichée et de les redessiner uniquement? (Dans l'exemple ci-dessous, la perfomace est encore relativement bonne, mais elle peut être arbitrairement aggravée en utilisant des artistes plus ou plus complexes)

J'ai eu un problème de performance similaire avec la hoverméthode qui, à chaque fois qu'elle était appelée, s'exécutait canvas.draw()à la fin. Mais comme vous pouvez le voir, j'ai trouvé une solution de contournement pour cela en utilisant la mise en cache et la restauration de l'arrière-plan des axes (sur la base de cela ). Cela a considérablement amélioré la performance et maintenant, même avec de nombreux artistes, cela fonctionne très bien. Peut-être existe-t-il une manière similaire de procéder, mais pour la méthode panet zoom?

Désolé pour l'exemple de code long, la plupart n'est pas directement pertinent pour la question mais nécessaire pour un exemple de travail pour mettre en évidence le problème.

ÉDITER

J'ai mis à jour le MWE vers quelque chose qui est plus représentatif de mon code actuel.

import numpy as np
import numpy as np
import sys
import matplotlib.pyplot as plt
from matplotlib.backends.backend_qt5agg import \
    FigureCanvasQTAgg
import matplotlib.patheffects as PathEffects
from matplotlib.text import Annotation
from matplotlib.collections import LineCollection

from PyQt5.QtWidgets import QApplication, QVBoxLayout, QDialog


def check_limits(base_xlim, base_ylim, new_xlim, new_ylim):
    if new_xlim[0] < base_xlim[0]:
        overlap = base_xlim[0] - new_xlim[0]
        new_xlim[0] = base_xlim[0]
        if new_xlim[1] + overlap > base_xlim[1]:
            new_xlim[1] = base_xlim[1]
        else:
            new_xlim[1] += overlap
    if new_xlim[1] > base_xlim[1]:
        overlap = new_xlim[1] - base_xlim[1]
        new_xlim[1] = base_xlim[1]
        if new_xlim[0] - overlap < base_xlim[0]:
            new_xlim[0] = base_xlim[0]
        else:
            new_xlim[0] -= overlap
    if new_ylim[1] < base_ylim[1]:
        overlap = base_ylim[1] - new_ylim[1]
        new_ylim[1] = base_ylim[1]
        if new_ylim[0] + overlap > base_ylim[0]:
            new_ylim[0] = base_ylim[0]
        else:
            new_ylim[0] += overlap
    if new_ylim[0] > base_ylim[0]:
        overlap = new_ylim[0] - base_ylim[0]
        new_ylim[0] = base_ylim[0]
        if new_ylim[1] - overlap < base_ylim[1]:
            new_ylim[1] = base_ylim[1]
        else:
            new_ylim[1] -= overlap

    return new_xlim, new_ylim


class FigureCanvas(FigureCanvasQTAgg):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.bg_cache = None

    def draw(self):
        ax = self.figure.axes[0]
        hid_annotation = False
        if ax.annot.get_visible():
            ax.annot.set_visible(False)
            hid_annotation = True
        hid_highlight = False
        if ax.last_artist:
            ax.last_artist.set_path_effects([PathEffects.Normal()])
            hid_highlight = True
        super().draw()
        self.bg_cache = self.copy_from_bbox(self.figure.bbox)
        if hid_highlight:
            ax.last_artist.set_path_effects(
                [PathEffects.withStroke(
                    linewidth=7, foreground="c", alpha=0.4
                )]
            )
            ax.draw_artist(ax.last_artist)
        if hid_annotation:
            ax.annot.set_visible(True)
            ax.draw_artist(ax.annot)

        if hid_highlight:
            self.update()


def position(t_, coeff, var=0.1):
    x_ = np.random.normal(np.polyval(coeff[:, 0], t_), var)
    y_ = np.random.normal(np.polyval(coeff[:, 1], t_), var)

    return x_, y_


class Data:
    def __init__(self, times):
        self.length = np.random.randint(1, 20)
        self.t = np.sort(
            np.random.choice(times, size=self.length, replace=False)
        )
        self.vel = [np.random.uniform(-2, 2), np.random.uniform(-2, 2)]
        self.accel = [np.random.uniform(-0.01, 0.01), np.random.uniform(-0.01,
                                                                      0.01)]
        x0, y0 = np.random.uniform(0, 1000, 2)
        self.x, self.y = position(
            self.t, np.array([self.accel, self.vel, [x0, y0]])
        )


class Test(QDialog):
    def __init__(self):
        super().__init__()
        self.fig, self.ax = plt.subplots()
        self.canvas = FigureCanvas(self.fig)
        self.artists = []
        self.zoom_factor = 1.5
        self.x_press = None
        self.y_press = None
        self.annot = Annotation(
            "", xy=(0, 0), xytext=(-20, 20), textcoords="offset points",
            bbox=dict(boxstyle="round", fc="w", alpha=0.7), color='black',
            arrowprops=dict(arrowstyle="->"), zorder=6, visible=False,
            annotation_clip=False, in_layout=False,
        )
        self.annot.set_clip_on(False)
        setattr(self.ax, 'annot', self.annot)
        self.ax.add_artist(self.annot)
        self.last_artist = None
        setattr(self.ax, 'last_artist', self.last_artist)

        self.image = np.random.uniform(0, 100, 1000000).reshape((1000, 1000))
        self.ax.imshow(self.image, cmap='gray', interpolation='nearest')
        self.times = np.linspace(0, 20)
        for i in range(1000):
            data = Data(self.times)
            points = np.array([data.x, data.y]).T.reshape(-1, 1, 2)
            segments = np.concatenate([points[:-1], points[1:]], axis=1)
            z = np.linspace(0, 1, data.length)
            norm = plt.Normalize(z.min(), z.max())
            lc = LineCollection(
                segments, cmap='autumn', norm=norm, alpha=1,
                linewidths=2, picker=8, capstyle='round',
                joinstyle='round'
            )
            setattr(lc, 'data_id', i)
            lc.set_array(z)
            self.ax.add_artist(lc)
            self.artists.append(lc)
        self.default_xlim = self.ax.get_xlim()
        self.default_ylim = self.ax.get_ylim()

        self.canvas.draw()

        self.cid_motion = self.fig.canvas.mpl_connect(
            'motion_notify_event', self.motion_event
        )
        self.cid_button = self.fig.canvas.mpl_connect(
            'button_press_event', self.pan_press
        )
        self.cid_zoom = self.fig.canvas.mpl_connect(
            'scroll_event', self.zoom
        )

        layout = QVBoxLayout()
        layout.addWidget(self.canvas)
        self.setLayout(layout)

    def zoom(self, event):
        if event.inaxes == self.ax:
            scale_factor = np.power(self.zoom_factor, -event.step)
            xdata = event.xdata
            ydata = event.ydata
            cur_xlim = self.ax.get_xlim()
            cur_ylim = self.ax.get_ylim()
            x_left = xdata - cur_xlim[0]
            x_right = cur_xlim[1] - xdata
            y_top = ydata - cur_ylim[0]
            y_bottom = cur_ylim[1] - ydata

            new_xlim = [
                xdata - x_left * scale_factor, xdata + x_right * scale_factor
            ]
            new_ylim = [
                ydata - y_top * scale_factor, ydata + y_bottom * scale_factor
            ]
            # intercept new plot parameters if they are out of bounds
            new_xlim, new_ylim = check_limits(
                self.default_xlim, self.default_ylim, new_xlim, new_ylim
            )

            if cur_xlim != tuple(new_xlim) or cur_ylim != tuple(new_ylim):
                self.ax.set_xlim(new_xlim)
                self.ax.set_ylim(new_ylim)

                self.canvas.draw_idle()

    def motion_event(self, event):
        if event.button == 1:
            self.pan_move(event)
        else:
            self.hover(event)

    def pan_press(self, event):
        if event.inaxes == self.ax:
            self.x_press = event.xdata
            self.y_press = event.ydata

    def pan_move(self, event):
        if event.inaxes == self.ax:
            xdata = event.xdata
            ydata = event.ydata
            cur_xlim = self.ax.get_xlim()
            cur_ylim = self.ax.get_ylim()
            dx = xdata - self.x_press
            dy = ydata - self.y_press
            new_xlim = [cur_xlim[0] - dx, cur_xlim[1] - dx]
            new_ylim = [cur_ylim[0] - dy, cur_ylim[1] - dy]

            # intercept new plot parameters that are out of bound
            new_xlim, new_ylim = check_limits(
                self.default_xlim, self.default_ylim, new_xlim, new_ylim
            )

            if cur_xlim != tuple(new_xlim) or cur_ylim != tuple(new_ylim):
                self.ax.set_xlim(new_xlim)
                self.ax.set_ylim(new_ylim)

                self.canvas.draw_idle()

    def update_annot(self, event, artist):
        self.ax.annot.xy = (event.xdata, event.ydata)
        text = f'Data #{artist.data_id}'
        self.ax.annot.set_text(text)
        self.ax.annot.set_visible(True)
        self.ax.draw_artist(self.ax.annot)

    def hover(self, event):
        vis = self.ax.annot.get_visible()
        if event.inaxes == self.ax:
            ind = 0
            cont = None
            while (
                ind in range(len(self.artists))
                and not cont
            ):
                artist = self.artists[ind]
                cont, _ = artist.contains(event)
                if cont and artist is not self.ax.last_artist:
                    if self.ax.last_artist is not None:
                        self.canvas.restore_region(self.canvas.bg_cache)
                        self.ax.last_artist.set_path_effects(
                            [PathEffects.Normal()]
                        )
                        self.ax.last_artist = None
                    artist.set_path_effects(
                        [PathEffects.withStroke(
                            linewidth=7, foreground="c", alpha=0.4
                        )]
                    )
                    self.ax.last_artist = artist
                    self.ax.draw_artist(self.ax.last_artist)
                    self.update_annot(event, self.ax.last_artist)
                ind += 1

            if vis and not cont and self.ax.last_artist:
                self.canvas.restore_region(self.canvas.bg_cache)
                self.ax.last_artist.set_path_effects([PathEffects.Normal()])
                self.ax.last_artist = None
                self.ax.annot.set_visible(False)
        elif vis:
            self.canvas.restore_region(self.canvas.bg_cache)
            self.ax.last_artist.set_path_effects([PathEffects.Normal()])
            self.ax.last_artist = None
            self.ax.annot.set_visible(False)
        self.canvas.update()
        self.canvas.flush_events()


if __name__ == '__main__':
    app = QApplication(sys.argv)
    test = Test()
    test.show()
    sys.exit(app.exec_())
mapf
la source
Je ne comprends pas le problème. Comme les artistes en dehors des axes ne sont de toute façon pas dessinés, ils ne ralentiront pas non plus.
ImportanceOfBeingErnest
Donc, vous dites qu'il existe déjà une routine qui vérifie quels artistes peuvent être vus afin que seuls les visibles soient réellement dessinés? Peut-être que cette routine est ce qui est très coûteux en calcul? Parce que vous pouvez facilement voir une différence de performance si vous essayez par exemple: avec mon 1000 artiste WME ci-dessus, zoomez sur un seul artiste et faites un panoramique. Vous remarquerez un retard important. Faites maintenant la même chose, mais ne tracez qu'un seul (ou même 100) artiste (s) et vous verrez qu'il n'y a presque pas de retard.
mapf
Eh bien, la question est, êtes-vous capable d'écrire une routine plus efficace? Dans un cas simple, peut-être. Vous pouvez donc vérifier quels artistes sont dans les limites de la vue et définir tous les autres invisibles. Si la vérification compare simplement les coordonnées centrales des points, c'est plus rapide. Mais cela vous ferait perdre le point si seulement son centre était à l'extérieur mais un peu moins de la moitié de celui-ci serait toujours à l'intérieur de la vue. Cela dit, le principal problème ici est qu'il y a 1000 artistes dans les axes. Si à la place, vous n'utilisiez qu'un seul plotavec tous les points, le problème ne se produirait pas.
ImportanceOfBeingErnest
Ouais, tout à fait vrai. C'est juste que ma prémisse était fausse. Je pensais que la raison de la mauvaise performance était que tous les artistes sont toujours dessinés indépendamment du fait qu'ils puissent être vus ou non. J'ai donc pensé qu'une routine intelligente qui n'attirait que les artistes qui allaient être vus améliorerait la performance, mais apparemment une telle routine est déjà en place, donc je suppose qu'il n'y a pas grand-chose à faire ici. Je suis sûr que je ne serai pas en mesure d'écrire une routine plus efficace, au moins pour un cas général.
mapf
Dans mon cas, cependant, je traite en fait des collections de lignes (plus une image en arrière-plan) et comme vous l'avez déjà dit, même s'il ne s'agissait que de points comme dans mon MWE, il ne suffit pas de vérifier si les coordonnées sont à l'intérieur des axes. Je devrais peut-être mettre à jour le MWE en conséquence pour le rendre plus clair.
mapf

Réponses:

0

Vous pouvez trouver les artistes qui se trouvent dans la zone actuelle des axes si vous vous concentrez sur les données que les artistes tracent.

Par exemple, si vous placez vos données de points ( aet btableaux) dans un tableau numpy comme ceci:

self.points = np.random.randint(0, 100, (1000, 2))

vous pouvez obtenir la liste des points à l'intérieur des limites x et y actuelles:

xmin, xmax = self.ax.get_xlim()
ymin, ymax = self.ax.get_ylim()

p = self.points

indices_of_visible_points = (np.argwhere((p[:, 0] > xmin) & (p[:, 0] < xmax) & (p[:, 1] > ymin) &  (p[:, 1] < ymax))).flatten()

vous pouvez utiliser indices_of_visible_pointspour indexer votre self.artistsliste associée

Guglie
la source
Merci pour votre réponse! Malheureusement, cela ne fonctionne que si les artistes sont des points uniques. Ça ne marche déjà plus si les artistes sont des répliques. Par exemple, l'image d'une ligne définie par seulement deux points où les points se trouvent en dehors des limites des axes, mais la ligne reliant les points coupe le cadre des axes. Peut-être que je devrais éditer le MWE en conséquence pour que ce soit plus évident.
mapf
Pour moi, l'approche est la même, concentrez-vous sur les données . Si les artistes sont des lignes, vous pouvez également vérifier l'intersection avec le rectangle de vue. Si vous tracez des courbes, vous les échantillonnez probablement à intervalles fixes en les réduisant en segments de ligne. Soit dit en passant, pouvez-vous donner un échantillon plus réaliste de ce que vous traitez?
Guglie
J'ai mis à jour MWE
mapf