Есть ли способ в matplotlib проверить, какие исполнители находятся в отображаемой области осей?

9

У меня есть программа с интерактивной фигурой, в которой иногда рисуются художники. На этом рисунке вы также можете масштабировать и панорамировать с помощью мыши. Однако исполнение при масштабировании панорамирования не очень хорошее, потому что каждый художник всегда перерисовывается. Есть ли способ проверить, какие художники находятся в отображаемой области и только перерисовать их? (В приведенном ниже примере перфорация все еще относительно хороша, но ее можно сделать произвольно хуже, используя более или более сложных художников)

У меня была похожая проблема производительности с hoverметодом, который всякий раз, когда он вызывался, запускался canvas.draw()в конце. Но, как вы можете видеть, я нашел хороший обходной путь для этого, используя кеширование и восстановление фона осей (основываясь на этом ). Это значительно улучшило представление, и теперь даже у многих артистов все идет очень гладко. Может быть, есть похожий способ сделать это, но для panи zoomметод?

Извините за длинный пример кода, большая часть которого не имеет прямого отношения к вопросу, но необходима для рабочего примера, чтобы выделить проблему.

РЕДАКТИРОВАТЬ

Я обновил MWE до чего-то, что больше отражает мой настоящий код.

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
источник
Я не понимаю проблемы. Поскольку художники, находящиеся вне осей, в любом случае не рисуются, они также ничего не замедляют.
ImportanceOfBeingErnest
Итак, вы говорите, что уже есть рутина, которая проверяет, кого из художников можно увидеть, так что на самом деле нарисованы только видимые? Может быть, эта процедура является то, что в вычислительном отношении очень дорого? Потому что вы можете легко увидеть разницу в производительности, если вы попробуете следующее, например: с моим 1000 исполнителей WME выше, увеличьте одного исполнителя и перемещайтесь. Вы заметите значительную задержку. Теперь сделайте то же самое, но нанесите только 1 (или даже 100) исполнителей, и вы увидите, что задержки практически нет.
mapf
Вопрос в том, можете ли вы написать более эффективную программу? В простом случае, может быть. Таким образом, вы можете проверить, какие художники находятся в пределах видимости и установить все остальные невидимые. Если проверка просто сравнивает центральные координаты точек, это быстрее. Но это привело бы к потере точки, если бы только ее центр находился снаружи, но чуть меньше половины ее все равно находилось бы внутри вида. При этом главная проблема в том, что в топорах 1000 художников. Если бы вместо этого вы использовали только один единственный plotсо всеми точками, проблема не возникнет.
ImportanceOfBeingErnest
Да, абсолютно верно. Просто моя предпосылка была неверной. Я думал, что причиной плохого выступления было то, что все художники были нарисованы независимо от того, их можно увидеть или нет. Таким образом, я думал, что умная рутина, которая привлекает только тех художников, которых предстоит увидеть, улучшит представление, но, очевидно, такая рутина уже существует, поэтому я думаю, что здесь мало что можно сделать. Я почти уверен, что не смогу написать более эффективную процедуру, по крайней мере, для общего случая.
mapf
В моем случае, однако, я имею дело с коллекциями линий (плюс изображение на заднем плане) и, как вы уже сказали, даже если это были просто точки, как в моем MWE, просто проверить, находятся ли координаты внутри осей, недостаточно. Возможно я должен обновить MWE соответственно, чтобы сделать это более ясным.
mapf

Ответы:

0

Вы можете определить, какие художники находятся в текущей области осей, если сосредоточитесь на данных, которые наносят художники.

Например, если вы поместите ваши данные точек ( aи bмассивы) в массив numpy следующим образом:

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

Вы можете получить список точек внутри текущих ограничений x и y:

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

Вы можете использовать indices_of_visible_pointsдля индексирования вашего связанного self.artistsсписка

Guglie
источник
Спасибо за ваш ответ! К сожалению, это работает только в том случае, если у артистов одинаковые баллы. Это уже не работает, если художники - линии. Например, изобразите линию, определяемую только двумя точками, где точки лежат за пределами осей, однако линия, соединяющая точки, пересекает рамку осей. Может быть, я должен отредактировать MWE соответственно, чтобы оно было более очевидным.
mapf
Для меня подход тот же, сосредоточиться на данных . Если художники являются линиями, вы можете дополнительно проверить на пересечение с прямоугольником вида. Если вы строите кривые, возможно, вы выбираете их с фиксированными интервалами, сокращая их до отрезков. Кстати, вы можете дать более реалистичный пример того, что вы планируете?
Гугли
Я обновился до MWE
mapf