2

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

代码在 GUI 中显示如下:

GUI的打印屏幕

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

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

下载 NetCDF 文件

如何在 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()
4

1 回答 1

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)。这比绘制所有内容要快得多!

当使用带有 useblit=True 的 matplotlib 的 RectangleSelector 小部件时,它将创建另一个背景实例变量,这会干扰您自己的背景实例变量。要解决此问题,您必须将 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        
于 2018-03-09T14:13:50.807 回答