Чи є в matplotlib спосіб перевірити, хто з артистів знаходиться в області відображення в даний час осей?


9

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

У мене була схожа проблема з виконанням hoverметоду, який, коли він його називав, запускався canvas.draw()в кінці. Але, як ви бачите, я знайшов чітке рішення для цього, використовуючи кешування та відновлення фону осей (виходячи з цього ). Це значно покращило перформанс, і зараз навіть у багатьох виконавців це працює дуже гладко. Можливо, існує подібний спосіб зробити це, але для методу panта zoomметоду?

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

EDIT

Я оновив 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_())

Я не розумію проблеми. Оскільки художників, які знаходяться поза осями, все одно не малюють, вони також нічого не сповільнюватимуть.
ВажливістьOfBeingErnest

Тож ви говорите, що вже існує звичайна програма, яка перевіряє, кого з художників можна побачити, щоб насправді намалювали лише видимі? Можливо, ця рутина є обчислювально дуже дорогою? Тому що ви можете легко побачити різницю у виконанні, якщо спробувати наступне: наприклад, з моїм 1000 WME-виконавцем вище, збільшуйте масштаб одного виконавця та обертайте навколо. Ви помітите значну затримку. Тепер зробіть те ж саме, але задумайте лише 1 (або навіть 100) виконавців, і ви побачите, що затримок майже немає.
mapf

Ну, питання в тому, чи вмієте ви написати більш ефективний розпорядок роботи? У простому випадку, можливо. Таким чином, ви можете перевірити, які виконавці знаходяться в межах перегляду, і встановити всі інші невидимі. Якщо чек просто порівнює центральні координати крапок, це швидше. Але це дозволить вам втратити крапку, якби тільки її центр знаходиться зовні, але трохи менше половини його все одно буде знаходитися всередині зору. Однак, головна проблема полягає в тому, що на осях 1000 художників. Якщо б замість цього ви використовували лише один сингл plotзі всіма пунктами, проблема не виникне.
ВажливістьOfBeingErnest

Так абсолютно правда. Просто моя помилка була неправильною. Я подумав, що причина поганого виконання - це те, що всі митці завжди є незалежними від того, бачити їх чи ні. Таким чином, я подумав, що розумний розпорядок, який притягує лише тих артистів, які будуть бачити, покращить продуктивність, але, мабуть, така рутина вже існує, тому, мабуть, тут не так багато можливо зробити. Я впевнений, що мені не вдасться написати більш ефективного розпорядку, принаймні для загального випадку.
mapf

Однак у моєму випадку я маю справу з лінійними колекціями (плюс зображення у фоновому режимі), і, як ви вже сказали, навіть якщо це були лише крапки, як у моєму MWE, просто перевіряючи, чи немає координат всередині осей недостатньо. Можливо, я повинен оновити MWE відповідно, щоб зробити його більш зрозумілим.
mapf

Відповіді:


0

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

Наприклад, якщо ви поміщаєте свої дані ( aта bмасиви) в масивний масив, як це:

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списку


Спасибі за вашу відповідь! На жаль, це працює лише в тому випадку, якщо у художників є одиничні бали. Це вже не працює, якщо художники - це рядки. Наприклад, зображення лінії, визначеної лише двома точками, де точки лежать поза межами осей, проте лінія, що з'єднує точки, перетинає осі кадру. Можливо, я повинен редагувати MWE відповідно, щоб це було більш очевидним.
mapf

Для мене підхід такий самий, фокус на даних . Якщо виконавці - це лінії, ви можете додатково перевірити перетин з прямокутником подання. Якщо ви плануєте криві, ймовірно, ви відбираєте їх через певні проміжки часу, зводячи їх до лінійних сегментів. До речі, чи можете ви дати більш реалістичний зразок того, що ви плануєте?
Гуглі

Я оновив до
MWE
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.