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 명 (또는 100 명)의 아티스트 만 플롯하면 거의 지연이 없음을 알 수 있습니다.
mapf

문제는 더 효율적인 루틴을 작성할 수 있습니까? 간단한 경우에는 따라서 어떤 아티스트가보기 제한 내에 있는지 확인할 수 있고 다른 아티스트는 모두 보이지 않게 설정할 수 있습니다. 검사가 점의 중심 좌표를 비교하면 더 빠릅니다. 그러나 중심 만 밖에 있지만 점의 절반보다 약간 작 으면 여전히 점 내부에 있으면 점이 느슨해집니다. 즉, 여기서 주요 문제는 축에 1000 명의 아티스트가 있다는 것입니다. 대신 plot모든 점에 하나의 단일만을 사용 하면 문제가 발생하지 않습니다.
중요도

네, 사실입니다. 단지 내 전제가 잘못되었다는 것입니다. 나는 공연이 나쁜 이유는 모든 예술가들이 볼 수 있는지 여부에 관계없이 멀리 떨어져 있기 때문이라고 생각했다. 따라서 나는 보여 질 예술가들만 이끌어내는 현명한 일상이 공연을 향상시킬 것이라고 생각했지만 분명히 그런 일상이 이미 존재하고 있기 때문에 여기서 할 수있는 일은 많지 않다고 생각합니다. 적어도 일반적인 경우에는 더 효율적인 루틴을 작성할 수 없을 것이라고 확신합니다.
mapf

그러나 필자의 경우 실제로 실제로 라인 컬렉션 (배경에 이미지 포함)을 다루고 있으며 이미 말했듯이 MWE 에서처럼 점이더라도 좌표가 축 내부에 있는지 확인하는 것만으로는 충분하지 않습니다. 어쩌면 MWE를 더 명확하게 업데이트해야 할 수도 있습니다.
mapf

답변:


0

아티스트가 플로팅하는 데이터에 초점을 맞추면 축의 현재 영역에있는 아티스트를 찾을 수 있습니다.

예를 들어 포인트 데이터 ( ab배열)를 다음과 같이 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목록 을 색인 하는 데 사용할 수 있습니다


답변 주셔서 감사합니다! 불행히도, 이것은 아티스트가 싱글 포인트 인 경우에만 작동합니다. 아티스트가 선이라면 이미 더 이상 작동하지 않습니다. 예를 들어, 점이 축 한계 밖에있는 두 점만으로 정의 된 선을 이미지로 만들지 만 점을 연결하는 선이 축 프레임과 교차합니다. 어쩌면 MWE를 적절하게 편집해야하므로 더 분명합니다.
mapf

나에게 접근 방식은 동일 하며 데이터에 중점을 둡니다 . 아티스트가 선인 경우보기 사각형과의 교차를 추가로 확인할 수 있습니다. 커브를 플로팅하는 경우 일정 간격으로 커브를 샘플링하여 선분으로 줄입니다. 그건 그렇고, 당신은 당신이 무엇을 플롯하고 있는지보다 현실적인 샘플을 줄 수 있습니까?
Guglie

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.