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

How to use matplotlib blitting to add matplot.patches to an matplotlib plot in wxPython?

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

代码在 GUI 中显示如下:

Printscreen of GUI

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

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

Download NetCDF file

如何在 onselect 函数中将 self.square blit 到 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()

我自己找到了解决方案:

为了 blit matplotlib 补丁,您必须首先将补丁添加到坐标区。然后在坐标轴上绘制补丁,然后你可以将补丁 blit 到 canvas。

    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 widget 和 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