1

我正在使用 Matplotlib 来允许用户查找和调整图形的上下包络线,以便将其标准化为 [0, 1] 区间。我遵循这个答案以及这个原始的 matplotlib 示例,但不幸的是我还没有找到解决方案。

规则:

  • 'd' 键删除光标处的一个点(如果附近有任何点)
  • 'i' 键在光标处插入一个点
  • 鼠标拖动移动点
  • 关闭图形记录了信封的当前状态,因此它是一种确定按钮。

我想使用self.fig.canvas.restore_region(self.background)而不是self.fig.canvas.draw()更有效的重绘。问题是当我拖动一个点时,原始线和点也停留在绘图上,因为它被解释为 self.background. 我只想self.basedata成为背景,并动态更改self.linesself.peakplot. 目前,当我移动数据点时,线和点将加倍。

当我调整 matplotlib 窗口的大小时,使用safe_draw前面提到的 SO 答案中的函数不起作用(这在处理现实生活中的数据集时是必需的。)

这是代码:

from matplotlib import pyplot as plt
import numpy as np

def calc_envelope(x, ind, mode='u'):
    '''https://stackoverflow.com/a/39662343/11751294'''
    x_abs = np.abs(x)
    if mode == 'u':
        loc = np.where(np.diff(np.sign(np.diff(x_abs))) < 0)[0] + 1
    elif mode == 'l':
        loc = np.where(np.diff(np.sign(np.diff(x_abs))) > 0)[0] + 1
    else:
        raise ValueError('mode must be u or l.')
    peak = x_abs[loc]
    envelope = np.interp(ind, loc, peak)
    return envelope, peak, loc


class DraggableEnvelope:

    # this should be pixel distance later, because x and y can be differently scaled.
    epsilon = 2 # max absolute distance to count as a hit

    def __init__(self, x, y, mode='l'):
        self.fig, self.ax = plt.subplots()
        self.x = x
        self.y = y
        self.mode = mode
        if self.mode == 'l':
            self.envelope, self.y_env, loc = calc_envelope(
                self.y, np.arange(len(self.y)), 'l'
                )
            plt.title('Adjust the lower envelope.')
        elif self.mode == 'u':
            self.envelope, self.y_env, loc = calc_envelope(
                self.y, np.arange(len(self.y)), 'u'
                )
            plt.title('Adjust the upper envelope.')
        else:
            raise ValueError('mode must be u or l.')
        self._ind = None # the active point index
        self.basedata, = self.ax.plot(self.x, self.y)
        self.lines, = self.ax.plot(self.x, self.envelope, 'r')
        self.x_env = self.x[loc]
        self.peakplot, = self.ax.plot(self.x_env, self.y_env, 'ko')
        self.fig.canvas.mpl_connect('button_press_event', self.button_press_callback)
        self.fig.canvas.mpl_connect('key_press_event', self.key_press_callback)
        self.fig.canvas.mpl_connect('draw_event', self.draw_callback)
        self.fig.canvas.mpl_connect('button_release_event', self.button_release_callback)
        self.fig.canvas.mpl_connect('motion_notify_event', self.motion_notify_callback)
        plt.grid()
        plt.show()


    def button_release_callback(self, event):
        '''whenever a mouse button is released'''
        if event.button != 1:
            return
        self._ind = None


    def get_ind_under_point(self, event):
        '''Get the index of the selected point within the given epsilon tolerance.'''
        d = np.hypot(self.x_env - event.xdata, self.y_env - event.ydata)
        indseq, = np.nonzero(d == d.min())
        ind = indseq[0]

        if d[ind] >= self.epsilon:
            ind = None
        return ind


    def button_press_callback(self, event):
        '''whenever a mouse button is pressed we get the index'''
        if event.inaxes is None:
            return
        if event.button != 1:
            return
        self._ind = self.get_ind_under_point(event)


    def button_release_callback(self, event):
        '''whenever a mouse button is released'''
        if event.button != 1:
            return
        self._ind = None


    def key_press_callback(self, event):
        '''whenever a key is pressed'''
        if not event.inaxes:
            return
        if event.key == 'd':
            ind = self.get_ind_under_point(event)
            if ind is not None:
                self.x_env = np.delete(self.x_env,
                                         ind)
                self.y_env = np.delete(self.y_env, ind)
                self.interpolate()
                self.peakplot.set_data(self.x_env, self.y_env)
                self.lines.set_data(self.x, self.envelope)
        elif event.key == 'i':
            self.y_env = np.append(self.y_env, event.ydata)
            self.x_env = np.append(self.x_env, event.xdata)
            self.interpolate()
            self.peakplot.set_data(self.x_env, self.y_env)
            self.lines.set_data(self.x, self.envelope)
        if self.peakplot.stale:
            self.fig.canvas.draw_idle()

    def get_data(self):
        if self.mode == 'l':
            return self.y-self.envelope
        elif self.mode == 'u':
            return self.y/self.envelope

    def draw_callback(self, event):
        self.background = self.fig.canvas.copy_from_bbox(self.ax.bbox)
        self.ax.draw_artist(self.peakplot)
        self.ax.draw_artist(self.lines)

    def motion_notify_callback(self, event):
        '''on mouse movement we move the selected point'''
        if self._ind is None:
            return
        if event.inaxes is None:
            return
        if event.button != 1:
            return
        x, y = event.xdata, event.ydata
        self.x_env[self._ind], self.y_env[self._ind] = x, y
        self.interpolate()
        self.peakplot.set_data(self.x_env, self.y_env)
        self.lines.set_data(self.x, self.envelope)

        self.fig.canvas.restore_region(self.background)
        self.ax.draw_artist(self.lines)
        self.ax.draw_artist(self.peakplot)
        self.fig.canvas.blit(self.ax.bbox)
        # self.fig.canvas.draw() <-- redrawing the whole figure slowly

    def interpolate(self):
        idx = np.argsort(self.x_env)
        self.y_env, self.x_env = self.y_env[idx], self.x_env[idx]
        self.envelope = np.interp(self.x, self.x_env, self.y_env)


if __name__ == '__main__':
    # example data
    x = np.arange(0, 100, 0.1)
    y = 4 * np.sin(x) + np.cos(x / 2) + 5
    d = DraggableEnvelope(x, y, 'l')
    yt = d.get_data()
    d2 = DraggableEnvelope(x, yt, 'u')
    y_final = d2.get_data()
    plt.plot(x, y_final)
    plt.title('Final')
    plt.grid()
    plt.show()

我该如何解决这个问题?

4

0 回答 0