インタラクティブフィギュアを使ったプログラムで、たまに多くのアーティストが描かれることがあります。この図では、マウスを使用してズームおよびパンすることもできます。ただし、パンのズーム中のパフォーマンスは、すべてのアーティストが常に再描画されるため、あまり良くありません。現在表示されている領域にいるアーティストを確認して、それらを再描画する方法はありますか?(以下の例では、パフォーマンスはまだ比較的良好ですが、より複雑なアーティストを使用することで、パフォーマンスを任意に悪化させることができます)
このhover
メソッドには、呼び出されるたびにcanvas.draw()
最後に実行されるという同様のパフォーマンス問題がありました。しかし、ご覧のとおり、キャッシングを利用し、(これに基づいて)軸の背景を復元することで、その適切な回避策を見つけました。これによりパフォーマンスが大幅に向上し、多くのアーティストでも非常にスムーズに動作します。多分これを行うための同様の方法がありますが、pan
and 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人のアーティストがいることです。代わりに、
—
ImportanceOfBeingErnest
plot
すべてのポイントで1つのシングルのみを使用した場合、問題は発生しません。
そうだね。それは私の前提が間違っていただけです。パフォーマンスの悪さの理由は、すべてのアーティストが、彼らが見られるかどうかに関係なく離れて描かれているためだと思いました。したがって、見られるアーティストを描くだけのスマートルーチンはパフォーマンスを向上させるだろうと考えましたが、明らかにそのようなルーチンはすでに用意されているため、ここで実行できることはあまりないと思います。少なくとも一般的なケースでは、もっと効率的なルーチンを作成できないと確信しています。
—
mapf
ただし、私の場合は、実際にはラインコレクション(および背景の画像)を扱っており、すでに述べたように、MWEのように単なるドットであったとしても、座標が軸の内側にあるかどうかを確認するだけでは不十分です。多分私はそれをより明確にするためにそれに応じてMWEを更新する必要があります。
—
mapf