class Spectrogram(StandardMonitorPage): """Main class for a page that generates real-time spectrogram plots of EEG. """ def __init__(self, *args, **kwargs): """Construct a new Spectrogram page. Args: *args, **kwargs: Arguments to pass to the Page base class. """ self.initConfig() # initialize Page base class StandardMonitorPage.__init__(self, name='Spectrogram', configPanelClass=ConfigPanel, *args, **kwargs) self.initCanvas() self.initLayout() def initConfig(self): self.filter = True # use raw or filtered signal self.chanIndex = 0 # index of channel to show self.width = 5.0 # width of window to use for computing PSD self.decimationFactor = 1 # decimation factor, e.g., 2 will decimate to half sampRate self.interpolation = 'none' self.normScale = 'log' self.scale = -2 self.method = 'Wavelet' self.setRefreshDelay(200) self.waveletConfig = util.Holder(nFreq=100, span=10) self.fourierConfig = util.Holder() def initCanvas(self): """Initialize a new matplotlib canvas, figure and axis. """ self.plotPanel = wx.Panel(self) self.plotPanel.SetBackgroundColour('white') plotSizer = wx.BoxSizer(orient=wx.VERTICAL) self.plotPanel.SetSizer(plotSizer) self.fig = plt.Figure(facecolor='white') #self.canvas = FigureCanvas(parent=self, id=wx.ID_ANY, figure=self.fig) self.canvas = FigureCanvas(parent=self.plotPanel, id=wx.ID_ANY, figure=self.fig) self.ax = self.fig.add_subplot(1, 1, 1) self.ax.set_xlabel('Time (s)') self.ax.set_ylabel('Frequency (Hz)') self.cbAx = self.fig.add_axes([0.91, 0.05, 0.03, 0.93]) #self.fig.subplots_adjust(hspace=0.0, wspace=0.0, # left=0.035, right=0.92, top=0.98, bottom=0.05) self.adjustMargins() self.firstPlot() self.lastSize = (0, 0) self.needsResizePlot = True self.canvas.Bind(wx.EVT_SIZE, self.resizePlot) self.canvas.Bind(wx.EVT_IDLE, self.idleResizePlot) ##self.plotToolbar = widgets.PyPlotNavbar(self.canvas) ##plotSizer.Add(self.plotToolbar, proportion=0, flag=wx.EXPAND) plotSizer.Add(self.canvas, proportion=1, flag=wx.EXPAND) #self.plotToolbar.Hide() def initLayout(self): self.initStandardLayout() plotPaneAuiInfo = aui.AuiPaneInfo().Name('canvas').Caption( 'Spectrogram').CenterPane() #self.auiManager.AddPane(self.canvas, plotPaneAuiInfo) self.auiManager.AddPane(self.plotPanel, plotPaneAuiInfo) self.auiManager.Update() self.canvas.Hide() def afterUpdateSource(self): self.configPanel.updateChannels() def afterStart(self): # make sure canvas is visible self.canvas.Show() self.plotPanel.Layout() # trigger initial plot update self.needsFirstPlot = True def getCap(self): cap = self.src.getEEGSecs(self.width, filter=self.filter, copy=False) if self.decimationFactor > 1: cap.decimate(self.decimationFactor) return cap def getSpectrum(self, cap): # configurable XXX - idfah data = cap.data[:, self.chanIndex] * sig.windows.tukey( cap.data.shape[0]) # tukey or hann? XXX - idfah freqs, powers, phases = self.cwt.apply(data) # configurable XXX - idfah powers = np.clip(powers, 1.0e-10, np.inf) return freqs, powers def firstPlot(self, event=None): cap = self.getCap() self.cwt = sig.CWT(sampRate=cap.getSampRate(), freqs=self.waveletConfig.nFreq, span=self.waveletConfig.span) if self.isRunning(): freqs, powers = self.getSpectrum(cap) else: freqs = np.arange(1, self.src.getSampRate() // 2 + 1) powers = np.zeros((128, 10, 1)) powers[0, 0, 0] = 1.0 self.ax.cla() self.cbAx.cla() self.ax.set_xlabel('Time (s)') self.ax.set_ylabel('Frequency (Hz)') self.wimg = self.ax.imshow(powers[:, :, 0].T, interpolation=self.interpolation, origin='lower', aspect='auto', norm=self.getNorm(), extent=self.getExtent(cap, freqs), cmap=plt.cm.get_cmap('jet'), animated=True) self.cbar = self.fig.colorbar(self.wimg, cax=self.cbAx) self.cbar.set_label(r'Power Density ($V^2 / Hz$)') #self.updateNorm(powers) self.canvas.draw() #self.background = self.canvas.copy_from_bbox(self.fig.bbox) self.background = self.canvas.copy_from_bbox(self.ax.bbox) self.needsFirstPlot = False def adjustMargins(self): self.fig.subplots_adjust(hspace=0.0, wspace=0.0, left=0.045, right=0.90, top=0.98, bottom=0.07) def resizePlot(self, event): # prevents handling extra resize events, hack XXX - idfah size = self.canvas.GetSize() if self.lastSize == size: return else: self.lastSize = size # this is all a hack to do resizing on idle when page is not running # should this be a custom FigureCanvas derived widget? XXX - idfah if self.isRunning(): # when running, just do event.Skip() this will # call canvas._onSize since it is second handler self.needsResizePlot = False event.Skip() else: # flag to resize on next idle event self.needsResizePlot = True def idleResizePlot(self, event): # if not running and flagged for resize if not self.isRunning() and self.needsResizePlot: ##self.adjustMargins() self.needsResizePlot = False # call canvas resize method manually # hack alert, we just pass None as event # since it's not used anyway self.canvas._onSize(None) def getExtent(self, cap, freqs): return (0.0, cap.getNObs() / float(cap.getSampRate()), np.min(freqs), np.max(freqs)) def getNorm(self): mx = 10**self.scale if self.normScale == 'linear': mn = 0.0 norm = pltLinNorm(mn, mx) elif self.normScale == 'log': mn = 1e-10 norm = pltLogNorm(mn, mx) else: raise RuntimeError('Invalid norm %s.' % norm) return norm def updatePlot(self, event=None): """Draw the spectrogram plot. """ if self.needsFirstPlot: self.firstPlot() else: cap = self.getCap() freqs, powers = self.getSpectrum(cap) #self.updateNorm(powers) self.canvas.restore_region(self.background) self.wimg.set_array(powers[:, :, 0].T) self.wimg.set_extent(self.getExtent(cap, freqs)) self.ax.draw_artist(self.wimg) ##self.cbAx.draw_artist(self.cbar.patch) ##self.cbAx.draw_artist(self.cbar.solids) #self.cbar.draw_all() #self.canvas.blit(self.cbAx.bbox) #self.canvas.blit(self.fig.bbox) self.canvas.blit(self.ax.bbox) # for debugging, redraws everything ##self.canvas.draw() def captureImage(self, event=None): ## Parts borrowed from backends_wx.py from matplotlib # Fetch the required filename and file type. filetypes, exts, filter_index = self.canvas._get_imagesave_wildcards() default_file = self.canvas.get_default_filename() dlg = wx.FileDialog(self, "Save to file", "", default_file, filetypes, wx.SAVE | wx.OVERWRITE_PROMPT) dlg.SetFilterIndex(filter_index) if dlg.ShowModal() == wx.ID_OK: dirname = dlg.GetDirectory() filename = dlg.GetFilename() format = exts[dlg.GetFilterIndex()] basename, ext = os.path.splitext(filename) if ext.startswith('.'): ext = ext[1:] if ext in ('svg', 'pdf', 'ps', 'eps', 'png') and format != ext: #looks like they forgot to set the image type drop #down, going with the extension. format = ext self.canvas.print_figure(os.path.join(dirname, filename), format=format)
class BasePlotWindow(VTKPlotWindowGUI.VTKPlotWindowGUI): def CreateLowerPanel(self): return PlotInfoGUIC.PlotInfoGUIC(self) def EnableToolbar(self): self.toolbar.EnableTool(self.toolbar.AUTO_THRESHOLD, False) self.toolbar.EnableTool(self.toolbar.VIEW_SYMBOLS, True) self.toolbar.EnableTool(self.toolbar.COPY_HIGHLIGHT, False) self.toolbar.EnableTool(self.toolbar.SHOW_HIGHLIGHT, False) def __del__(self): gsm = component.getGlobalSiteManager() gsm.unregisterHandler(self.OnImageChangeEvent) def __init__(self, parent, *args, **kw): VTKPlotWindowGUI.VTKPlotWindowGUI.__init__(self, parent) # ------------------------------------------------------------------------------------ # Set up some zope event handlers # ------------------------------------------------------------------------------------ component.provideHandler(self.OnImageChangeEvent) self._voxel_volume_set = False self._voxel_volume = 1.0 if ('bar' in kw): self._usebar = True else: self._usebar = False self._scale = 1.0 self._unit = kw.get('units', 'pixels') self._dragging = 0 # is this needed? self._x0 = None self._y0 = None self._x1 = None self._y1 = None self._select_x0 = None self._select_x1 = None self._select_i0 = None self._select_i1 = None self.xdata = [] self.ydata = [] self._line = [None, None] self._liney = None self._title = "This is the title" self.filename = '' self._xlabel = "" self._ylabel = "" self._usesymbols = False # indicates whether nearest data point should be highlighted or not self._use_highlight_data = True self._isROI = False self._linelength = None self._inputname = "" self._highlight_visible = False self.__shortname__ = 'VTKPlot' self._otsu_threshold = None self._otsu_marker = None self._unit_scalings = {'pixels': 1.0, 'mm': 1.0, 'wavelength': 1.0} self.plot_data = None self.legend = None if ('scale' in kw): self._scale = float(kw['scale']) if ('title' in kw): self._title = kw['title'] if ('xlabel' in kw): self._xlabel = kw['xlabel'] if ('ylabel' in kw): self._ylabel = kw['ylabel'] # get icon factory self._stockicons = StockItems.StockIconFactory() # create info panel - it'll depend on whether this is a line plot or # histogram window self.lower_panel = self.CreateLowerPanel() self.GetSizer().Add(self.lower_panel, 0, wx.EXPAND, 5) # create a matplotlib panel self.dpi = 100 self.fig = Figure((3.0, 3.0), dpi=self.dpi) # self.fig.subplots_adjust(left=0.07, right=0.97, bottom=0.08, top=0.95) self.fig.subplots_adjust(left=0.0, right=1, bottom=0.0, top=1) self.axes = self.fig.add_subplot(111) # default labels self.axes.set_title(self._title, size=10) self.axes.set_ylabel(self._ylabel, size=8) self.line_tool = None # A red, horizontal line tool self.end_markers = None # Markers that go at the end of each line self.data_markers = None # Marks that indicate nearest data point pylab.setp(self.axes.get_xticklabels(), fontsize=8) pylab.setp(self.axes.get_yticklabels(), fontsize=8) sizer = wx.BoxSizer(wx.VERTICAL) # create canvas for plot widget self.canvas = FigCanvas(self.m_panelMatplotPanel, -1, self.fig) self.m_panelMatplotPanel.SetSizer(sizer) self.m_panelMatplotPanel.Fit() # activate interactive navigation self.toolbar = MicroViewNavigationToolbar(self.canvas) self.toolbar.Realize() tw, th = self.toolbar.GetSizeTuple() fw, fh = self.canvas.GetSizeTuple() self.toolbar.SetSize(wx.Size(fw, th)) sizer.Add(self.toolbar, 0, wx.LEFT | wx.EXPAND) sizer.Add(self.canvas, 1, wx.EXPAND) self.toolbar.update() # adjust toolbar self.EnableToolbar() # wire up events self.canvas.mpl_connect('motion_notify_event', self.MouseMoveEvent) self.canvas.mpl_connect('key_press_event', self.KeyPressEvent) self.canvas.mpl_connect('figure_leave_event', self.LeaveCanvasEvent) # wire up events here self.toolbar.Bind( wx.EVT_TOOL, self.SaveData, id=self.toolbar.SAVE_DATA) self.toolbar.Bind( wx.EVT_TOOL, self.SaveSnapShot, id=self.toolbar.SAVE_SNAPSHOT) self.toolbar.Bind( wx.EVT_TOOL, self.symbolsOnOff, id=self.toolbar.VIEW_SYMBOLS) # self.toolbar.Bind(wx.EVT_TOOL, self.Reset, id=self.toolbar.RESET_VIEW) self.toolbar.Bind( wx.EVT_TOOL, self.AutoThreshold, id=self.toolbar.AUTO_THRESHOLD) self.toolbar.Bind( wx.EVT_TOOL, self.onCopyHighlightToolbarButton, id=self.toolbar.COPY_HIGHLIGHT) self.toolbar.Bind( wx.EVT_TOOL, self.onShowHighlightToolbarToogle, id=self.toolbar.SHOW_HIGHLIGHT) self.toolbar.Bind( wx.EVT_TOOL, self.NearestDataSymbolsOnOff, id=self.toolbar.VIEW_NEARESTDATA) self.toolbar.Bind( wx.EVT_TOOL, self.select_roi, id=self.toolbar.SELECT_ROI) # listen to size events self.Bind(wx.EVT_PAINT, self.OnPaint) self._plotData = None # This table is used to make the input invisible self._wlTableInvisible = vtk.vtkWindowLevelLookupTable() self._wlTableInvisible.SetSaturationRange(0, 0) self._wlTableInvisible.SetHueRange(0, 0) self._wlTableInvisible.SetValueRange(0, 1) self._wlTableInvisible.SetNumberOfColors(2) self._wlTableInvisible.SetTableValue(0, 0.0, 0.0, 0.0, 0.0) self._wlTableInvisible.SetTableValue(1, 0.0, 0.0, 0.0, 0.0) self._wlTableInvisible.Build() # Invoke an event to react to currently loaded image mv = component.getUtility(IMicroViewMainFrame) current_image = mv.GetCurrentImageIndex() number_images_displayed = mv.GetNumberOfImagesCurrentlyLoaded() title = mv.GetCurrentImageTitle() self.OnImageChangeEvent(CurrentImageChangeEvent( current_image, number_images_displayed, title)) def OnPaint(self, evt): s = self.GetClientSize() try: self.fig.tight_layout(pad=0.25) except ValueError: # turn off legend if self.legend: self.legend.set_visible(False) return if (s[0] < 350) or (s[1] < 350): # turn off legend if self.legend: self.legend.set_visible(False) else: if self.legend: self.legend.set_visible(True) self.canvas.draw() def ClearPlot(self): self.xdata = np.array([]) self.ydata = np.array([]) if self.plot_data: for p in self.plot_data: p.remove() if self.end_markers: self.end_markers.remove() if self.data_markers: self.data_markers.remove() self.HideOtsuThreshold() self.plot_data = self.end_markers = self.data_markers = None # redraw canvas self.canvas.draw() def SetFileName(self, filename): self.filename = filename def update_axes_bounds(self): xmax = round(self.xdata.max(), 0) + 1 xmin = 0 ymin = round(self.ydata.min(), 0) - 1 ymax = round(self.ydata.max(), 0) + 1 self.axes.set_xbound(lower=xmin, upper=xmax) self.axes.set_ybound(lower=ymin, upper=ymax) def update_colour_names(self): numC = 1 if len(self.ydata.shape) > 1: numC = self.ydata.shape[-1] self.colours = ['red', 'green', 'blue', 'black'] else: self.ydata.shape = (self.ydata.shape[0], 1) self.colours = ['black'] def update_channel_names(self): numC = 1 if len(self.ydata.shape) > 1: numC = self.ydata.shape[-1] self.channel_names = ['red', 'green', 'blue', 'opacity'] else: self.ydata.shape = (self.ydata.shape[0], 1) self.channel_names = ['chan. 1'] def draw_plot(self): """ Redraws the plot""" # set axis range self.update_axes_bounds() self.update_colour_names() self.update_channel_names() self.axes.grid(True, color='gray') pylab.setp(self.axes.get_xticklabels(), visible=True) numC = 1 if len(self.ydata.shape) > 1: numC = self.ydata.shape[-1] else: self.ydata.shape = (self.ydata.shape[0], 1) self.file_label = '# Pos. (%s)\t' % self._unit + \ (numC - 1) * '%s\t' + '%s' + '\n#' self.file_label = self.file_label % tuple(self.channel_names[:numC]) args = [] marker_x = [] marker_y = [] marker_c = [] for i in range(numC): args.append(self.xdata) args.append(self.ydata[:, i]) args.append(self.colours[i]) marker_x.append(self.xdata[0]) marker_y.append(self.ydata[0, i]) marker_c.append('b') marker_x.append(self.xdata[-1]) marker_y.append(self.ydata[-1, i]) marker_c.append('g') if self.plot_data: if len(self.plot_data) != numC: for p in self.plot_data: p.remove() self.plot_data = None if self.plot_data is None: self.plot_data = self.axes.plot(*args, linewidth=1) self.legend = self.axes.legend( tuple(self.channel_names[0:numC])) pylab.setp(self.legend.get_texts(), fontsize='x-small') else: for i in range(numC): self.plot_data[i].set_xdata(args[i * 3 + 0]) self.plot_data[i].set_ydata(args[i * 3 + 1]) # draw end markers if self.end_markers: self.end_markers.remove() self.end_markers = self.axes.scatter( marker_x, marker_y, c=tuple(marker_c), linewidths=0, zorder=3) @component.adapter(CurrentImageChangeEvent) def OnImageChangeEvent(self, evt): # I only care about the input image number - I need to make a context switch here to account # for multiple ROIs self._current_index = evt.GetCurrentImageIndex() def showThresholdMarker(self, threshold): dlg = wx.MessageDialog(self, 'threshold:' + ' %0.1f' % ( threshold), 'Auto-Threshold', wx.OK | wx.ICON_INFORMATION) dlg.ShowModal() dlg.Destroy() if self._usebar is True: self._otsu_threshold = threshold # draw Otsu threshold if self._otsu_marker: self._otsu_marker.remove() self._otsu_marker = self.axes.scatter([self._otsu_threshold], [-1000], c='r', marker='^', linewidths=0, zorder=3) def GetMouseMoveLabelFormat(self): return 'x=%11.5f, y=%s' def MouseMoveEvent(self, evt): refresh_required = False # erase current data markers if self.data_markers: self.data_markers.remove() self.data_markers = None refresh_required = True # determine nearest data values and update wx gui if evt.xdata and len(self.xdata) > 0: index = abs(self.xdata - evt.xdata).argmin() val = self.ydata[index, :] marker_x = [self.xdata[index]] * len(val) marker_y = val marker_c = ['red'] * len(val) if len(val) == 1: val = val[0] else: val = tuple(val) # update line/histogram if evt.button == 2 or (evt.button == 1 and self.toolbar._active == 'SELECT_ROI'): self.setLinePoint(1, evt) fmt = self.GetMouseMoveLabelFormat() self.lower_panel.m_staticTextNearestDataValue.SetLabel( fmt % (self.xdata[index], str(val))) # optionally draw highlighted data if self._use_highlight_data and self.toolbar.ShouldIShowCursor(): # draw data markers self.data_markers = self.axes.scatter( marker_x, marker_y, c=marker_c, linewidths=0, zorder=4) refresh_required = True else: self.lower_panel.m_staticTextNearestDataValue.SetLabel('') if refresh_required: self.canvas.draw() # update wx user interface self.reportPosition([evt.xdata, evt.ydata]) self.reportLineLength() def SetSelectionRange(self, x0, x1): self._select_x0 = x0 self._select_x1 = x1 self.NotifyHistoHighlightChange() def GetSelectionRange(self): return self._select_x0, self._select_x1 def KeyPressEvent(self, evt): k = evt.key if k == '1': self.setFirstLinePoint(evt) elif k == '2': self.setSecondLinePoint(evt) elif k == 'r': self.Reset() elif k == 'y': self.removeMeasurementLine() def LeaveCanvasEvent(self, evt): # remove marker if self.data_markers: self.data_markers.remove() self.data_markers = None self.canvas.draw() def SetBinSize(self, evt=None, binsize=None): if binsize is None: binsize = float(self.binVar.get()) else: self.binVar.set(str(binsize)) # generate an event event.notify(BinSizeModifiedEvent(binsize)) def Delete(self): self.Hide() def Hide(self, evt=None): self.removeMeasurementLine() if self._usebar is True: if self._isROI is False: self.HideHighlight() self.HideOtsuThreshold() def HideHighlight(self, image_index=None): if self._isROI is True: self._isROI = False event.notify(HistogramClosedEvent()) if self._usebar is True and self.GetHighlightVisible() is True: self.SetHighlightVisible(False) self.NotifyHistoHighlightChange() self.OrthoPlanesRemoveInput('histogram') def HideOtsuThreshold(self): if self._otsu_marker: self._otsu_marker.remove() self._otsu_marker = None def OrthoPlanesRemoveInput(self, name): event.notify(OrthoPlanesRemoveInputEvent(name)) def Reset(self, e=None): self.removeMeasurementLine() # this might need to be reworked self.lower_panel.m_staticTextLineLength.SetLabel('') def GetCurrentDirectory(self): # determine working directory curr_dir = cwd = os.getcwd() config = MicroViewSettings.MicroViewSettings.getObject() # over-ride with system-wide directory try: curr_dir = config.GlobalCurrentDirectory or curr_dir except: config.GlobalCurrentDirectory = curr_dir try: curr_dir = config.CurrentSnapshotDirectory or curr_dir except: config.CurrentSnapshotDirectory = curr_dir return curr_dir def SaveCurrentDirectory(self, curr_dir): config = MicroViewSettings.MicroViewSettings.getObject() config.CurrentSnapshotDirectory = curr_dir def GetwlTableInvisible(self): return self._wlTableInvisible def CreateHighlightLookupTable(self): # Create colour table for histogram overlay table = vtk.vtkWindowLevelLookupTable() table.SetSaturationRange(0, 0) table.SetHueRange(0, 0) table.SetValueRange(0, 1) table.SetLevel(0.5) table.SetWindow(1.0) table.SetNumberOfColors(2) table.SetTableValue(0, 0.0, 0.0, 0.0, 0.0) table.SetTableValue(1, 1.0, 0.0, 0.0, 1.0) table.Build() return table def GetInputName(self): return self._inputname def SetInputName(self, name): self._inputname = name def NotifyHistoHighlightChange(self): event.notify(HistogramSelectionModifiedEvent( self._select_x0, self._select_x1, self.GetHighlightVisible())) def SelectionToROI(self): """Assert ourselves as the owner of the ROI object for this image""" # This next line is increasingly not accurate - need something better self._isROI = True # send out a notification to allow any ROI visuals to modify themselves event.notify(HistogramIsActiveROIEvent()) def GetROIType(self, index): return 'custom' def AutoThreshold(self, evt): """Force an autothreshold calculation This routine posts an AutoThresholdCommandEvent command in order to request a new calculation of an optimal Otsu threshold. """ event.notify(AutoThresholdCommandEvent()) def SetInput(self, inp): if self.plot_data: for p in self.plot_data: p.remove() self.plot_data = None if self.data_markers: self.data_markers.remove() self.data_markers = None self._plotData = inp # bit of a bad name -- this next line actually resets all inputs, then # adds self._plotData self.SetHighlightVisible(False) def SaveSnapShot(self, evt): # Fetch the required filename and file type. filetypes, exts, filter_index = self.canvas._get_imagesave_wildcards() default_file = "image." + self.canvas.get_default_filetype() dlg = wx.FileDialog(self, "Save snapshot to file", "", default_file, filetypes, wx.SAVE | wx.OVERWRITE_PROMPT) dlg.SetFilterIndex(filter_index) if dlg.ShowModal() == wx.ID_OK: dirname = dlg.GetDirectory() filename = dlg.GetFilename() format = exts[dlg.GetFilterIndex()] basename, ext = os.path.splitext(filename) if ext.startswith('.'): ext = ext[1:] if ext in ('svg', 'pdf', 'ps', 'eps', 'png') and format != ext: # looks like they forgot to set the image type drop # down, going with the extension. dlg = wx.MessageDialog(self, 'extension %s did not match the selected image type %s; going with %s' % (ext, format, ext), 'Warning', wx.OK | wx.ICON_WARNING) dlg.ShowModal() dlg.Destroy() format = ext try: self.canvas.print_figure( os.path.join(dirname, filename), format=format) except Exception, e: logging.error(e.message)