如何使用 matplotlib blitting 将 matplot.patches 添加到 wxPython 中的 matplotlib 图?

Posted

技术标签:

【中文标题】如何使用 matplotlib blitting 将 matplot.patches 添加到 wxPython 中的 matplotlib 图?【英文标题】:How to use matplotlib blitting to add matplot.patches to an matplotlib plot in wxPython? 【发布时间】:2018-08-15 10:03:13 【问题描述】:

我正在使用 matplotlib 库进行绘图并在我的 wxPython GUI 中显示它。我正在绘制来自激光雷达仪器的大量数据点。问题是,我想在这个图中绘制矩形来指示有趣的区域。但是当我在与绘图相同的轴上绘制一个矩形时,整个绘图会被重新绘制,这需要很多时间。这是因为 self.canvas.draw(),一个重新绘制所有内容的函数。

代码在 GUI 中显示如下:

Printscreen of GUI

这是该问题的一个最小工作示例。你可以通过按住鼠标右键来绘制矩形。使用左侧的按钮绘制 NetCDF 数据后,矩形的绘制变得非常缓慢。我使用 ImportanceOfBeingErnest 提供的示例尝试了一些有关 blitting 的操作,但经过多次尝试,我仍然无法使其正常工作。

要使最小的工作示例正常工作,您必须在 plot_Data() 函数下指定 NetCDF 文件的路径。我提供了要在此处下载的 NetCDF 文件:

Download NetCDF file

如何在 onselect 函数中将 self.square 粘贴到 self.canvas?

import netCDF4 as nc

import matplotlib
matplotlib.use('WXAgg')

from matplotlib.figure import Figure
from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as FigureCanvas

import matplotlib.pyplot as plt
import matplotlib.colors as colors
import matplotlib.widgets

import time

import wx

class rightPanel(wx.Panel):

    def __init__(self, parent):
        wx.Panel.__init__(self, parent, style=wx.SUNKEN_BORDER)
        self.initiate_Matplotlib_Plot_Canvas()        
        self.add_Matplotlib_Widgets()

    def initiate_Matplotlib_Plot_Canvas(self):
        self.figure = Figure()
        self.axes = self.figure.add_subplot(111)
        self.colorbar = None
        self.canvas = FigureCanvas(self, -1, self.figure)
        self.sizer = wx.BoxSizer(wx.VERTICAL)
        self.sizer.Add(self.canvas, proportion=1, flag=wx.ALL | wx.GROW)
        self.SetSizer(self.sizer)
        self.Fit()
        self.canvas.draw()

    def add_Matplotlib_Widgets(self):

        self.rectangleSelector = matplotlib.widgets.RectangleSelector(self.axes, self.onselect,
                                                                      drawtype="box", useblit=True,
                                                                      button=[3], interactive=False
                                                                      )

    def onselect(self, eclick, erelease):
        tstart = time.time()
        x1, y1 = eclick.xdata, eclick.ydata
        x2, y2 = erelease.xdata, erelease.ydata

        height = y2-y1
        width = x2-x1


        self.square = matplotlib.patches.Rectangle((x1,y1), width, 
                                                   height, angle=0.0, edgecolor='red',
                                                   fill=False
                                                   #blit=True gives Unknown property blit
                                                   )


        self.axes.add_patch(self.square)
        self.canvas.draw()

        # =============================================================================
        #         self.background = self.canvas.copy_from_bbox(self.axes.bbox)
        #         
        #         
        #         self.canvas.restore_region(self.background)
        #        
        #         self.axes.draw_artist(self.square)
        #        
        #         self.canvas.blit(self.axes.bbox)
        # =============================================================================


        tend = time.time()
        print("Took " + str(tend-tstart) + " sec")

    def plot_Data(self):
        """This function gets called by the leftPanel onUpdatePlot. This updates
        the plot to the set variables from the widgets"""

        path = "C:\\Users\\TEST_DATA\\cesar_uvlidar_backscatter_la1_t30s_v1.0_20100501.nc"

        nc_data = self.NetCDF_READ(path)

        print("plotting......")
        vmin_value = 10**2
        vmax_value = 10**-5

        combo_value = nc_data['perp_beta']

        self.axes.clear()
        plot_object = self.axes.pcolormesh(combo_value.T, cmap='rainbow', 
                                           norm=colors.LogNorm(vmin=vmin_value, vmax=vmax_value))

        self.axes.set_title("Insert title here")

        if self.colorbar is None:
            self.colorbar = self.figure.colorbar(plot_object)
        else:
            self.colorbar.update_normal(plot_object)

        self.colorbar.update_normal(plot_object)

        print('canvas draw..............')
        self.canvas.draw()


        print("plotting succesfull")
###############################################################################
###############################################################################
        """BELOW HERE IS JUST DATA MANAGEMENT AND FRAME/PANEL INIT"""
###############################################################################
###############################################################################        
    def NetCDF_READ(self, path):
        in_nc = nc.Dataset(path)

        list_of_keys = in_nc.variables.keys()

        nc_data =     #Create an empty dictionary to store NetCDF variables

        for item in list_of_keys:
            variable_shape = in_nc.variables[item].shape
            variable_dimensions = len(variable_shape)
            if variable_dimensions > 1:
                nc_data[item] = in_nc.variables[item][...]      #Adding netCDF variables to dictonary

        return nc_data

class leftPanel(wx.Panel):

    def __init__(self, parent, mainPanel):
        wx.Panel.__init__(self, parent)

        button = wx.Button(self, -1, label="PRESS TO PLOT")
        button.Bind(wx.EVT_BUTTON, self.onButton)
        self.mainPanel = mainPanel

    def onButton(self, event):
        self.mainPanel.rightPanel.plot_Data()

class MainPanel(wx.Panel):

    def __init__(self, parent):
        """Initializing the mainPanel. This class is called by the frame."""

        wx.Panel.__init__(self, parent)
        self.SetBackgroundColour('red')

        """Acquire the width and height of the monitor"""
        width, height = wx.GetDisplaySize()
        """Split mainpanel into two sections"""
        self.vSplitter = wx.SplitterWindow(self, size=(width,(height-100)))

        self.leftPanel = leftPanel(self.vSplitter, self) 
        self.rightPanel = rightPanel(self.vSplitter)

        self.vSplitter.SplitVertically(self.leftPanel, self.rightPanel,102)

class UV_Lidar(wx.Frame):
    """Uppermost class. This class contains everything and calls everything.
    It is the container around the mainClass, which on its turn is the container around
    the leftPanel class and the rightPanel class. This class generates the menubar, menu items,
    toolbar and toolbar items"""

    def __init__(self, parent, id):
        print("UV-lidar> Initializing GUI...")
        wx.Frame.__init__(self, parent, id, 'UV-lidar application')

        self.Bind(wx.EVT_CLOSE, self.OnCloseWindow)
        self.mainPanel = MainPanel(self)

    def OnCloseWindow(self, event):
        self.Destroy()

if __name__ == '__main__':
    app = wx.App()
    frame = UV_Lidar(parent=None, id=-1)
    frame.Show()
    print("UV-lidar> ")
    print("UV-lidar> Initializing GUI OK")
    app.MainLoop()

【问题讨论】:

如果在绘图之前绘制 Recangle,之后不需要重新绘制。另外,看看 blitting 技术。 SO上有很多问题。 我必须在绘图时在绘图上绘制矩形。否则我无法标记有趣的区域。我在绘图上添加了另一个带有一些矩形的 GUI 打印屏幕。 然后你可以blit矩形。请参阅this question 或this one。 对于 rectangleSelector,我使用 blit。但是,我希望在释放鼠标按钮后矩形保持绘制状态。我添加了将矩形绘制到问题中的代码。 是的,你需要恢复画布而不是 self.canvas.draw() 并 blit self.square 【参考方案1】:

我自己找到了解决方案:

为了对 matplotlib 补丁进行 blit,您必须首先将补丁添加到轴上。然后在轴上绘制补丁,然后您可以将补丁粘贴到画布上。

    square = matplotlib.patches.Rectangle((x1,y1), width, 
                                               height, angle=0.0, edgecolor='red',
                                               fill=False)

    self.axes.add_patch(square)
    self.axes.draw_artist(square)
    self.canvas.blit(self.axes.bbox)

如果您不想使用self.canvas.draw,但仍使用具有 useblit=True 的 matplotlib 小部件,则可以将绘图保存为背景图像:self.background = self.canvas.copy_from_bbox(self.axes.bbox),稍后使用:self.canvas.restore_region(self.background) 将其恢复。这比绘制所有内容要快得多!

当使用 matplotlib 的 RectangleSelector 小部件并设置 useblit=True 时,它​​将创建另一个背景实例变量,这会干扰您自己的背景实例变量。要解决此问题,您必须将 RectangleSelector 小部件的背景实例变量设置为等于您自己的背景实例变量。但是,这应该只在 RectangleSelector 小部件不再活动后完成。否则它会将一些绘图动画保存到背景中。 因此,一旦 RectangleSelector 变为非活动状态,您可以使用以下命令更新其背景:self.rectangleSelector.background = self.background

下面给出了必须编辑的代码。使用wx.CallLater(0, lambda: self.tbd(square)) 以便 RectangleSelector 小部件的背景实例变量仅在变为非活动状态时更新。

def add_Matplotlib_Widgets(self):
    """Calling these instances creates another self.background in memory. Because the widget classes
    restores their self-made background after the widget closes it interferes with the restoring of 
    our leftPanel self.background. In order to compesate for this problem, all background instances 
    should be equal to eachother. They are made equal in the update_All_Background_Instances(self) 
    function"""


    """Creating a widget that serves as the selector to draw a square on the plot"""
    self.rectangleSelector = matplotlib.widgets.RectangleSelector(self.axes, self.onselect,
                                                                  drawtype="box", useblit=True,
                                                                  button=[3], interactive=False
                                                              )

def onselect(self, eclick, erelease):
    self.tstart = time.time()
    x1, y1 = eclick.xdata, eclick.ydata
    x2, y2 = erelease.xdata, erelease.ydata

    height = y2-y1
    width = x2-x1



    square = matplotlib.patches.Rectangle((x1,y1), width, 
                                               height, angle=0.0, edgecolor='red',
                                               fill=False
                                               #blit=True gives Unknown property blit
                                               )

    """In order to keep the right background and not save any rectangle drawing animations 
    on the background, the RectangleSelector widget has to be closed first before saving 
    or restoring the background"""
    wx.CallLater(0, lambda: self.tbd(square))


def tbd(self, square):

    """leftPanel background is restored"""
    self.canvas.restore_region(self.background)

    self.axes.add_patch(square)
    self.axes.draw_artist(square)


    self.canvas.blit(self.axes.bbox)

    """leftPanel background is updated"""
    self.background = self.canvas.copy_from_bbox(self.axes.bbox)


    """Setting all backgrounds equal to the leftPanel self.background"""
    self.update_All_Background_Instances()

    print('Took '+ str(time.time()-self.tstart) + ' s')

def update_All_Background_Instances(self):
    """This function sets all of the background instance variables equal 
    to the lefPanel self.background instance variable"""
    self.rectangleSelector.background = self.background        

【讨论】:

以上是关于如何使用 matplotlib blitting 将 matplot.patches 添加到 wxPython 中的 matplotlib 图?的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 matplotlib blitting 将 matplot.patches 添加到 wxPython 中的 matplotlib 图?

Tkinter GUI 中实时更新的 Blitting - 性能和图像重叠问题

OPENGL:如何使用 blit 将 alpha 移动到红色通道

如何使用 QOpenGLFramebufferObject 对两个透明帧缓冲区对象进行 blit?

matplotlib中FuncAnimation的手动时间循环控制

在Pygame中使用screen.blit时,我可以使用.format功能吗?