我正在使用 Matplotlib 来允许用户查找和调整图形的上下包络线,以便将其标准化为 [0, 1] 区间。我遵循这个答案以及这个原始的 matplotlib 示例,但不幸的是我还没有找到解决方案。
规则:
- 'd' 键删除光标处的一个点(如果附近有任何点)
- 'i' 键在光标处插入一个点
- 鼠标拖动移动点
- 关闭图形记录了信封的当前状态,因此它是一种确定按钮。
我想使用self.fig.canvas.restore_region(self.background)而不是self.fig.canvas.draw()更有效的重绘。问题是当我拖动一个点时,原始线和点也停留在绘图上,因为它被解释为
self.background. 我只想self.basedata成为背景,并动态更改self.lines和self.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()
我该如何解决这个问题?