Apakah ada cara di matplotlib untuk memeriksa artis yang berada di area sumbu yang saat ini ditampilkan?

9

Saya memiliki program dengan tokoh interaktif di mana kadang-kadang banyak seniman digambar. Dalam gambar ini, Anda juga dapat memperbesar dan menggeser menggunakan mouse. Namun, performace selama zoom panning tidak terlalu bagus karena setiap artis selalu digambar ulang. Apakah ada cara untuk memeriksa artis mana yang ada di area yang sedang ditampilkan dan hanya menggambar ulang mereka? (Dalam contoh di bawah ini perfomace masih relatif baik, tetapi dapat diperburuk dengan menggunakan seniman yang lebih kompleks atau lebih rumit)

Saya memiliki masalah performa yang sama dengan hovermetode yang setiap kali dipanggil berlari canvas.draw()pada akhirnya. Tapi seperti yang Anda lihat, saya menemukan solusi yang rapi untuk itu dengan memanfaatkan caching dan mengembalikan latar belakang sumbu (berdasarkan ini ). Ini secara signifikan meningkatkan kinerja dan sekarang bahkan dengan banyak artis berjalan sangat lancar. Mungkin ada cara serupa untuk melakukan ini tetapi untuk pandan zoommetode?

Maaf untuk sampel kode panjang, sebagian besar tidak relevan secara langsung untuk pertanyaan tetapi diperlukan untuk contoh yang berfungsi untuk menyoroti masalah ini.

EDIT

Saya memperbarui MWE ke sesuatu yang lebih mewakili kode saya yang sebenarnya.

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
sumber
Saya tidak mengerti masalahnya. Karena seniman yang berada di luar sumbu tidak ditarik, mereka juga tidak akan memperlambat apa pun.
ImportanceOfBeingErnest
Jadi Anda mengatakan sudah ada rutinitas yang memeriksa artis mana yang dapat dilihat sehingga hanya yang terlihat yang benar-benar digambar? Mungkin rutinitas ini apa yang komputasi sangat mahal? Karena Anda dapat dengan mudah melihat perbedaan dalam kinerja jika Anda mencoba yang berikut ini misalnya: dengan 1000 artis WME saya di atas, memperbesar satu artis dan menjelajah. Anda akan melihat penundaan yang signifikan. Sekarang lakukan hal yang sama tetapi plot hanya 1 (atau bahkan 100) artis (s) dan Anda akan melihat bahwa hampir tidak ada penundaan.
mapf
Nah, pertanyaannya adalah, apakah Anda bisa menulis rutin yang lebih efisien? Dalam kasus sederhana, mungkin. Jadi, Anda dapat memeriksa artis mana yang berada dalam batas tampilan dan mengatur semua yang lainnya tidak terlihat. Jika centang hanya membandingkan koordinat titik tengah, itu lebih cepat. Tapi itu akan membuat Anda kehilangan titik jika hanya bagian tengahnya di luar tetapi sedikit kurang dari setengahnya masih berada di dalam tampilan. Yang sedang berkata, masalah utama di sini adalah bahwa ada 1000 seniman di kapak. Jika sebaliknya, Anda hanya menggunakan satu tunggal plotdengan semua poin, masalah tidak akan terjadi.
ImportanceOfBeingErnest
Ya benar sekali. Hanya saja premis saya salah. Saya pikir alasan untuk kinerja buruk adalah bahwa semua seniman yang tertarik terlepas dari apakah mereka dapat dilihat atau tidak. Jadi saya pikir rutin cerdas yang hanya menarik para artis yang akan dilihat akan meningkatkan kinerja tetapi ternyata rutin seperti itu sudah ada, jadi saya kira tidak banyak yang bisa dilakukan di sini. Saya cukup yakin saya tidak akan bisa menulis rutin yang lebih efisien, setidaknya untuk kasus umum.
mapf
Namun dalam kasus saya, saya sebenarnya berurusan dengan koleksi linecollection (ditambah gambar di latar belakang) dan seperti yang sudah Anda katakan, bahkan jika itu hanya titik-titik seperti pada MWE saya, cukup memeriksa apakah koordinat berada di dalam sumbu tidak cukup. Mungkin saya harus memperbarui MWE sesuai untuk membuatnya lebih jelas.
mapf

Jawaban:

0

Anda dapat menemukan artis mana yang berada di area sumbu saat ini jika Anda fokus pada data yang direncanakan oleh artis.

Sebagai contoh jika Anda meletakkan data poin Anda ( adan barray) dalam array numpy seperti ini:

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

Anda bisa mendapatkan daftar poin di dalam batas x dan y saat ini:

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

Anda dapat menggunakan indices_of_visible_pointsindeks terkait Anda self.artistsdaftar

Guglie
sumber
Terima kasih atas jawaban Anda! Sayangnya, ini hanya berfungsi jika artis adalah poin tunggal. Sudah tidak berfungsi lagi jika para seniman adalah garis. Misalnya gambar garis yang didefinisikan oleh hanya dua titik di mana titik-titiknya berada di luar batas sumbu, namun garis yang menghubungkan titik-titik tersebut memotong bingkai sumbu. Mungkin saya harus mengedit MWE sesuai sehingga lebih jelas.
mapf
Bagi saya pendekatannya sama, fokus pada data . Jika artis adalah garis, Anda juga dapat memeriksa persimpangan dengan tampilan persegi panjang. Jika Anda merencanakan kurva, mungkin Anda mengambil sampelnya pada interval yang tetap dan menguranginya menjadi segmen garis. Omong-omong, dapatkah Anda memberikan sampel yang lebih realistis tentang apa yang Anda rencanakan?
Guglie
Saya memperbarui ke MWE
mapf