为啥代码片段在 matplotlib 2.0.2 上运行良好,但在 matplotlib 2.1.0 上引发错误

Posted

技术标签:

【中文标题】为啥代码片段在 matplotlib 2.0.2 上运行良好,但在 matplotlib 2.1.0 上引发错误【英文标题】:Why a code snippet works fine with matplotlib 2.0.2 but raise an error with matplotlib 2.1.0为什么代码片段在 matplotlib 2.0.2 上运行良好,但在 matplotlib 2.1.0 上引发错误 【发布时间】:2017-11-22 15:49:42 【问题描述】:

我有以下代码可以很好地与 matplotlib 2.0.2(在 Python 3.6.3 下)一起使用,但会引发类型错误(TypeError: 'Rectangle' object不可调用)与 matplotlib 2.1.0。 它的目的是通过“press&move”在 PyQt5 窗口中嵌入的图形上交互地绘制一个矩形,当用户释放鼠标按钮 1 时,矩形给出了图形的新限制(即像 Matlab 一样的缩放行为)。

我做错了什么?

# Python 3.6
...
from PyQt5 import QtCore, QtGui, QtWidgets
...
from matplotlib.patches import Rectangle
...
class MplWidget(Canvas):
    zoom_in = False
    cid_zoom_P = None
    cid_zoom_R = None
    cid_zoom_M = None

    def __init__(self, parent, dpi = 100, hold = False, **kwargs):
        super().__init__(Figure())
        self.parent = parent
        self.setParent(parent)
        self.figure = Figure(dpi = dpi)
        self.canvas = Canvas(self.figure)
        self.repres = self.figure.add_subplot(111)

    ...

    def zoomManager(self, bouton_pan, bouton_tip):

        if self.zoom_in is not True:
            self.zoom_in = True
            QtWidgets.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.CrossCursor))
            self.x0 = None
            self.y0 = None
            self.x1 = None
            self.y1 = None
            self.cid_zoom_P = self.mpl_connect("button_press_event", self.zoomOnPress)
            self.cid_zoom_R = self.mpl_connect("button_release_event", self.zoomOnRelease)
            self.cid_zoom_M = self.mpl_connect("motion_notify_event", self.zoomOnMotion)
        else:
            self.zoom_in = False
            QtWidgets.QApplication.restoreOverrideCursor()
            self.mpl_disconnect(self.cid_zoom_P)
            self.mpl_disconnect(self.cid_zoom_R)
            self.mpl_disconnect(self.cid_zoom_M)

    ...

    def zoomOnPress(self, event):
        if event.button == 1:
            if not bool(event.inaxes):
                return
            self.zoom_pressed = True
            self.x0 = event.xdata
            self.y0 = event.ydata
            self.rect = Rectangle((0,0), 1, 1, alpha=0.25, ls='--', lw=1, ec='k')
            self.repres.add_patch(self.rect)

    def zoomOnRelease(self, event):
        if event.button == 1:
            self.zoom_pressed = False
            self.x1 = event.xdata
            if not self.x1:
                self.x1 = self.x1_old
            else:
                self.x1_old = self.x1
            self.y1 = event.ydata
            if not self.y1:
                self.y1 = self.y1_old
            else:
                self.y1_old = self.y1
            self.rect.remove()
            self.repres.set_xlim([min(self.x1, self.x0), max(self.x1, self.x0)])
            self.repres.set_ylim([min(self.y1, self.y0), max(self.y1, self.y0)])
            self.draw()


    def zoomOnMotion(self, event):
        if self.zoom_pressed is False:
            return
        self.x1 = event.xdata
        if not self.x1:
            self.x1 = self.x1_old
        else:
            self.x1_old = self.x1
        self.y1 = event.ydata
        if not self.y1:
            self.y1 = self.y1_old
        else:
            self.y1_old = self.y1
        self.rect.set_width(self.x1 - self.x0)
        self.rect.set_height(self.y1 - self.y0)
        self.rect.set_xy((self.x0, self.y0))
        self.draw()

...

【问题讨论】:

您需要提供问题的minimal reproducible example,以便人们能够重现它并找到错误。如果没有minimal reproducible example,这个问题就很难解决。您还需要提供完整的错误回溯,而不仅仅是最后一行。 @ImportanceOfBeingErnest :这是一个关于 matplotlib 版本和向后兼容性的理论问题。真正的代码太长太复杂,无法在此处发布。我已经花了很多时间写这个问题...... 那么理论上的答案是:不,Rectangle 的功能在两个版本之间没有变化,并且 Rectangle 是向后兼容的。如果您仍然对实际问题的解决方案感兴趣,请参见上文。 @ImportanceOfBeingErnest :我对您的回答感到惊讶,因为在我的情况下,这两个版本的 Matplotlib 之间的操作存在真正的差异。此处部分公开的代码在 Matplotlib 2.0.2 中完美运行,但不适用于 2.1.0 版本。在最后一种情况下,错误是对“self.draw”的调用产生的。我已经测试了这个语句的很多变体,但没有成功。我将尝试在“PyPlot”下重现错误,为您提供所需的示例功能和可重现性。感谢您的帮助 不要误会我的意思,我并不是说版本之间没有任何会导致此错误的更改。我要说的是Rectangle 没有变化。为了找出导致错误的原因,需要一个完整且可验证的示例,充其量也应该是最小的。只有这样才能开始调试。 【参考方案1】:

以下代码可以完美地与 2.0.2 和 2.1.0 matplotlib 版本一起使用。 但我认为它不能代表我遇到的问题。无论如何我都会给出它,因为这对于那些对所涉及的机制感兴趣的人来说是一个例子(我在网上找不到类似的例子)。

import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle
import numpy as np

xdata = np.linspace(0,9*np.pi, num=301)
ydata = np.sin(xdata)

fig, ax = plt.subplots()
line, = ax.plot(xdata, ydata)

class Zoom(object):

    zoom_in = False

    def __init__(self, fig, ax):
        self.fig = fig
        self.ax = ax
        self.x0 = None
        self.y0 = None
        self.x1 = None
        self.y1 = None
        self.ax.figure.canvas.mpl_connect('button_press_event', self.on_press)
        self.ax.figure.canvas.mpl_connect('button_release_event', self.on_release)
        self.ax.figure.canvas.mpl_connect("motion_notify_event", self.on_motion)

    def on_press(self, event):
        print('press')
        if event.button == 1:
            if not bool(event.inaxes):
                return
            self.x0 = event.xdata
            self.y0 = event.ydata
            self.rect = Rectangle((0,0), 1, 1, alpha=0.25, ls='--', lw=1, ec='k')
            self.ax.add_patch(self.rect)
            self.zoom_in = True

    def on_release(self, event):
        print('release')
        if event.button == 1:
            self.x1 = event.xdata
            if not self.x1:                 # user is out of axis
                self.x1 = self.x1_old
            else:                           # user is over axis
                self.x1_old = self.x1
            self.y1 = event.ydata
            if not self.y1:
                self.y1 = self.y1_old
            else:
                self.y1_old = self.y1
            self.rect.remove()
            self.ax.set_xlim([min(self.x1, self.x0), max(self.x1, self.x0)])
            self.ax.set_ylim([min(self.y1, self.y0), max(self.y1, self.y0)])
            self.ax.figure.canvas.draw()


    def on_motion(self, event):
        if self.zoom_in is True:
            self.x1 = event.xdata
            if not self.x1:
                self.x1 = self.x1_old
            else:
                self.x1_old = self.x1
            self.y1 = event.ydata
            if not self.y1:
                self.y1 = self.y1_old
            else:
                self.y1_old = self.y1
            self.rect.set_width(self.x1 - self.x0)
            self.rect.set_height(self.y1 - self.y0)
            self.rect.set_xy((self.x0, self.y0))
            #self.draw()    # Statement that is problematic
            #plt.draw()      # Its equivalent under 'Pyplot'
            self.ax.figure.canvas.draw()


a = Zoom(fig, ax)

plt.show()

在我研究的这一点上,我认为问题不在于补丁“矩形”,而在于以下行,其中“self”是 PyQt5 的“matplotlib 小部件”:

    self.cid_zoom_P = self.mpl_connect("button_press_event", self.zoomOnPress)
    self.cid_zoom_R = self.mpl_connect("button_release_event", self.zoomOnRelease)
    self.cid_zoom_M = self.mpl_connect("motion_notify_event", self.zoomOnMotion)

无论如何,我将在这个问题上多花一点时间。如果我找不到我做错了什么,我会将 Matplotlib 降级到 2.0.2。

【讨论】:

【参考方案2】:

我找到了解决这个问题的方法,现在,我可以使用 PyQt5 和 Matplotlib 2.1.2 进行缩放(我在 Matplotlib 2.2.2 中遇到了另一个问题,tkagg 不再有 cursord 属性)。

这是一个名为 backend_qt5agg.py 的文件,它引发了我的错误(矩形不可调用)。 第 89 行和下一个,有:

if self._bbox_queue:
    bbox_queue = self._bbox_queue
else:
    painter.eraseRect(self.rect())  # this is where a Rectangle is 'called'
    bbox_queue = [
            Bbox([[0, 0], [self.renderer.width, self.renderer.height]])]

我刚刚添加了一个像这样的“try/except”块:

if self._bbox_queue:
    bbox_queue = self._bbox_queue
else:
    try:
        painter.eraseRect(self.rect())
    except:
        pass
    bbox_queue = [
            Bbox([[0, 0], [self.renderer.width, self.renderer.height]])]

而且效果很好。

【讨论】:

以上是关于为啥代码片段在 matplotlib 2.0.2 上运行良好,但在 matplotlib 2.1.0 上引发错误的主要内容,如果未能解决你的问题,请参考以下文章

Matplotlib 为啥我点击后绘图功能会在按钮内绘制?

为啥这个混淆矩阵(matplotlib)在 Jupyter Notebook 中看起来被压扁了? [复制]

为啥这个代码片段说包含错误?

为啥这段代码会泄露? (简单的代码片段)

为啥 recyclerview$adapter 在片段中为空

为啥在尝试使用 Line3D 对象时 matplotlib 3d 动画不起作用