軸の現在表示されている領域にいるアーティストを確認する方法はmatplotlibにありますか?


9

インタラクティブフィギュアを使ったプログラムで、たまに多くのアーティストが描かれることがあります。この図では、マウスを使用してズームおよびパンすることもできます。ただし、パンのズーム中のパフォーマンスは、すべてのアーティストが常に再描画されるため、あまり良くありません。現在表示されている領域にいるアーティストを確認して、それらを再描画する方法はありますか?(以下の例では、パフォーマンスはまだ比較的良好ですが、より複雑なアーティストを使用することで、パフォーマンスを任意に悪化させることができます)

このhoverメソッドには、呼び出されるたびにcanvas.draw()最後に実行されるという同様のパフォーマンス問題がありました。しかし、ご覧のとおり、キャッシングを利用し、(これに基づいて)軸の背景を復元することで、その適切な回避策を見つけました。これによりパフォーマンスが大幅に向上し、多くのアーティストでも非常にスムーズに動作します。多分これを行うための同様の方法がありますが、panand 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_())

私は問題を理解していません。軸の外にいるアーティストはとにかく描かれないので、彼らはまた何も遅くしません。
ImportanceOfBeingErnest

では、どのアーティストが表示されるかを確認するルーチンがすでに存在するため、表示されているアーティストのみが実際に描画されるということですか。たぶん、このルーチンは計算上非常に高価なものですか?次の例を試してみると、パフォーマンスの違いを簡単に確認できるためです。たとえば、上の1000アーティストのWMEで、1人のアーティストにズームインし、パンします。大幅な遅延が発生します。同じことを行って、1人(または100人)のアーティストのみをプロットします。ほとんど遅延がないことがわかります。
mapf

さて、問題は、より効率的なルーチンを記述できるかどうかです。単純なケースでは、たぶん。したがって、どのアーティストが表示制限内にあるかを確認し、他のすべてを非表示に設定できます。チェックがドットの中心座標を比較するだけの場合、それはより高速です。しかし、その中心だけが外にあるが、半分よりわずかに少ない場合でもドットがビューの内側にある場合は、ドットが失われます。とはいえ、ここでの主な問題は、軸に1000人のアーティストがいることです。代わりに、plotすべてのポイントで1つのシングルのみを使用した場合、問題は発生しません。
ImportanceOfBeingErnest

そうだね。それは私の前提が間違っていただけです。パフォーマンスの悪さの理由は、すべてのアーティストが、彼らが見られるかどうかに関係なく離れて描かれているためだと思いました。したがって、見られるアーティストを描くだけのスマートルーチンはパフォーマンスを向上させるだろうと考えましたが、明らかにそのようなルーチンはすでに用意されているため、ここで実行できることはあまりないと思います。少なくとも一般的なケースでは、もっと効率的なルーチンを作成できないと確信しています。
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リストのインデックス作成に使用できます


回答ありがとうございます!残念ながら、これはアーティストがシングルポイントの場合にのみ機能します。アーティストがラインの場合、すでに機能していません。たとえば、軸が軸の範囲外にある2つの点のみで定義された線をイメージしますが、点を結ぶ線が軸のフレームと交差しています。多分私はそれに応じてMWEを編集する必要があります。
mapf

私にとってはアプローチは同じですが、データ焦点を当てます。アーティストがラインの場合は、ビューの長方形との交差をさらに確認できます。曲線をプロットする場合、おそらくそれらを一定の間隔でサンプリングして、線分に減らします。ところで、あなたは何をプロットしているのか、より現実的なサンプルを与えることができますか?
グーリエ

MWEに更新しました
mapf
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.