Gibt es in matplotlib eine Möglichkeit zu überprüfen, welche Künstler sich im aktuell angezeigten Bereich der Achsen befinden?

9

Ich habe ein Programm mit einer interaktiven Figur, in der gelegentlich viele Künstler gezeichnet werden. In dieser Abbildung können Sie auch mit der Maus zoomen und schwenken. Die Leistung beim Zoomen und Schwenken ist jedoch nicht sehr gut, da jeder Künstler immer neu gezeichnet wird. Gibt es eine Möglichkeit zu überprüfen, welche Künstler sich im aktuell angezeigten Bereich befinden und nur diese neu zu zeichnen? (Im folgenden Beispiel ist die Leistung immer noch relativ gut, kann jedoch durch die Verwendung von mehr oder komplexeren Künstlern beliebig verschlechtert werden.)

Ich hatte ein ähnliches Leistungsproblem mit der hoverMethode, dass sie canvas.draw()am Ende immer dann ausgeführt wurde, wenn sie aufgerufen wurde . Aber wie Sie sehen, habe ich eine gute Lösung gefunden, indem ich das Caching verwendet und den Hintergrund der Achsen wiederhergestellt habe (basierend darauf ). Dies hat die Performance erheblich verbessert und läuft auch bei vielen Künstlern sehr flüssig. Vielleicht gibt es einen ähnlichen Weg, dies zu tun, außer für die Methode panund zoom?

Entschuldigen Sie das lange Codebeispiel. Das meiste davon ist für die Frage nicht direkt relevant, aber für ein funktionierendes Beispiel erforderlich, um das Problem hervorzuheben.

BEARBEITEN

Ich habe die MWE auf etwas aktualisiert, das repräsentativer für meinen tatsächlichen Code ist.

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
quelle
Ich verstehe das Problem nicht. Da Künstler, die sich außerhalb der Achsen befinden, sowieso nicht gezeichnet werden, werden sie auch nichts verlangsamen.
ImportanceOfBeingErnest
Sie sagen also, es gibt bereits eine Routine, die prüft, welcher der Künstler zu sehen ist, sodass nur die sichtbaren tatsächlich gezeichnet werden? Vielleicht ist diese Routine rechenintensiv? Da Sie einen Leistungsunterschied leicht erkennen können, wenn Sie Folgendes versuchen, z. B.: Zoomen Sie mit meinem 1000-Künstler-WME oben auf einen einzelnen Künstler und schwenken Sie herum. Sie werden eine erhebliche Verzögerung bemerken. Machen Sie jetzt dasselbe, aber zeichnen Sie nur 1 (oder sogar 100) Künstler, und Sie werden sehen, dass es fast keine Verzögerung gibt.
mapf
Die Frage ist, können Sie eine effizientere Routine schreiben? In einem einfachen Fall vielleicht. So können Sie überprüfen, welche Künstler sich innerhalb der Ansichtsgrenzen befinden, und alle anderen Künstler unsichtbar machen. Wenn die Prüfung nur die Mittelkoordinaten der Punkte vergleicht, ist das schneller. Aber das würde dazu führen, dass Sie den Punkt verlieren, wenn nur seine Mitte außerhalb liegt, aber etwas weniger als die Hälfte davon sich noch in der Ansicht befindet. Das Hauptproblem hierbei ist jedoch, dass sich 1000 Künstler in den Achsen befinden. Wenn Sie stattdessen nur eine einzige plotmit allen Punkten verwenden würden, würde das Problem nicht auftreten.
ImportanceOfBeingErnest
Ja absolut wahr. Es ist nur so, dass meine Prämisse falsch war. Ich dachte, der Grund für die schlechte Leistung sei, dass alle Künstler immer unabhängig davon gezeichnet sind, ob sie gesehen werden können oder nicht. Daher dachte ich, eine intelligente Routine, die nur die Künstler anzieht, die gesehen werden sollen, würde die Leistung verbessern, aber anscheinend ist eine solche Routine bereits vorhanden, daher denke ich, dass hier nicht viel getan werden kann. Ich bin mir ziemlich sicher, dass ich zumindest für einen allgemeinen Fall keine effizientere Routine schreiben kann.
mapf
In meinem Fall habe ich es jedoch tatsächlich mit Liniensammlungen (plus einem Bild im Hintergrund) zu tun, und wie Sie bereits sagten, reicht es nicht aus, nur zu überprüfen, ob die Koordinaten innerhalb der Achsen liegen, auch wenn es nur Punkte wie in meinem MWE sind. Vielleicht sollte ich das MWE entsprechend aktualisieren, um es klarer zu machen.
mapf

Antworten:

0

Sie können herausfinden, welche Künstler sich im aktuellen Bereich der Achsen befinden, wenn Sie sich auf die Daten konzentrieren, die die Künstler zeichnen.

Zum Beispiel, wenn Sie Ihre Punktedaten ( aund bArrays) in ein Numpy-Array wie folgt einfügen:

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

Sie können die Liste der Punkte innerhalb der aktuellen x- und y-Grenzen abrufen:

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

Sie verwenden können indices_of_visible_pointsIhre im Zusammenhang mit self.artistsIndexliste

Guglie
quelle
Vielen Dank für Ihre Antwort! Leider funktioniert dies nur, wenn die Künstler einzelne Punkte sind. Es funktioniert schon nicht mehr, wenn die Künstler Linien sind. Stellen Sie sich beispielsweise eine Linie vor, die nur durch zwei Punkte definiert ist, bei denen die Punkte außerhalb der Achsengrenzen liegen. Die Verbindungslinie zwischen den Punkten schneidet jedoch den Achsenrahmen. Vielleicht sollte ich die MWE entsprechend bearbeiten, damit es offensichtlicher wird.
mapf
Für mich ist der Ansatz der gleiche, konzentrieren Sie sich auf die Daten . Wenn es sich bei den Künstlern um Linien handelt, können Sie zusätzlich prüfen, ob sich das Ansichtsrechteck schneidet. Wenn Sie Kurven zeichnen, werden sie wahrscheinlich in festen Intervallen abgetastet und auf Liniensegmente reduziert. Können Sie übrigens eine realistischere Auswahl dessen geben, was Sie planen?
Guglie
Ich habe auf MWE
mapf