def populate_spectrogram_widget(self, widget: GraphicsLayoutWidget, game_name: str, player_name: PlayerName, electrode_name: str): # https://stackoverflow.com/questions/51312923/plotting-the-spectrum-of-a-wavfile-in-pyqtgraph-using-scipy-signal-spectrogram f, t, Sxx = self._acquire_spectrogram_signal(game_name, player_name, electrode_name) plot = widget.addPlot() plot.setTitle('Frequency over time for electrode %s' % ELECTRODES[electrode_name]) img = ImageItem() plot.addItem(img) hist = HistogramLUTItem() hist.setImageItem(img) widget.addItem(hist) hist.setLevels(np.min(Sxx), np.max(Sxx)) hist.gradient.restoreState({ 'mode': 'rgb', 'ticks': [(0.5, (0, 182, 188, 255)), (1.0, (246, 111, 0, 255)), (0.0, (75, 0, 113, 255))] }) img.setImage(Sxx) img.scale(t[-1] / np.size(Sxx, axis=1), f[-1] / np.size(Sxx, axis=0)) plot.setLimits(xMin=0, xMax=t[-1], yMin=0, yMax=f[-1]) plot.setLabel('bottom', "Time", units='s') plot.setLabel('left', "Frequency", units='Hz')
class ImageItemWithHistogram(ExtendedImageItem): def __init__(self, setpoint_x, setpoint_y, *args, colormap=None, **kwargs): # Create the attached histogram self._LUTitem = HistogramLUTItem() # Initialize self super().__init__(setpoint_x, setpoint_y, *args, colormap=colormap, **kwargs) # Update _LUTitem self._LUTitem.setImageItem(self) self._LUTitem.autoHistogramRange() # enable autoscaling # Attach a signal handler on parent changed self._parent = None def setLevels(self, levels, update=True): """ Hook setLevels to update histogram when the levels are changed in the image """ super().setLevels(levels, update) self._LUTitem.setLevels(*self.levels) def changeColorScale(self, name=None): if name is None: raise ValueError("Name of color map must be given") self.cmap = name self._LUTitem.gradient.setColorMap(COLORMAPS[name]) def getHistogramLUTItem(self): return self._LUTitem def parentChanged(self): super().parentChanged() # Add the histogram to the parent view_box = self.getViewBox() if isinstance(view_box, ExtendedPlotWindow): logger.debug("Adding _LUTitem to parent %r.", view_box) view_box.addItem(self._LUTitem) self._parent = view_box elif view_box is None: if getattr(self, "_parent", None) is not None: self._parent.removeItem(self._LUTitem) self._parent = None elif isinstance(view_box, CustomViewBox): # This second call always seems to occur... Ignore it, since we've added # ourselves to the plot window. pass else: raise NotImplementedError( "parentChanged is not implemented for anything " "other than ExtendedPlotWindows at this time. " f"Got {type(view_box)}.")
class CustomImageViewer(GraphicsLayoutWidget): @property def view_box(self): return self.image_plot.vb def __init__(self, parent=None, **kwargs): setConfigOptions(imageAxisOrder='row-major') super(CustomImageViewer, self).__init__(parent) self._scale = (1., 1.) self._center = (0, 0) self.__init_ui__() def __init_ui__(self): self.setWindowTitle('Image Viewer') self.image_plot = self.addPlot() self.image_plot.vb.setAspectLocked() self.image_plot.vb.invertY() self.image_item = ImageItem() self.image_plot.addItem(self.image_item) self.hist = HistogramLUTItem() self.hist.setImageItem(self.image_item) self.addItem(self.hist) def set_data(self, data, change_limits: bool = True, reset_axes: bool = False): if data is None: return self.image_item.setImage(data, change_limits) if change_limits: self.hist.setLevels(data.min(), data.max()) if reset_axes: self.image_item.resetTransform() self.set_default_range() def set_default_range(self): axes = self.get_axes() self.image_plot.setRange(xRange=axes[0], yRange=axes[1]) def set_auto_range(self): self.image_plot.autoRange() def set_levels(self, levels=None): if levels: self.hist.setLevels(levels[0], levels[1]) else: self.hist.setLevels(self.image_item.image.min(), self.image_item.image.max()) def get_levels(self): return self.hist.getLevels() def set_center(self, center: tuple, pixel_units: bool = True): if not pixel_units: scale = self.get_scale() center = (center[0] / scale[0], center[1] / scale[1]) if self._center != (0, 0) or self._scale != (1., 1.): self.image_item.resetTransform() self.image_item.scale(*self._scale) self.image_item.translate(- center[0], - center[1]) self._center = center self.set_default_range() def set_scale(self, scale: float or tuple): if isinstance(scale, float) or isinstance(scale, int): scale = (scale, scale) if self._center != (0, 0) or self._scale != (1., 1.): self.image_item.resetTransform() self.image_item.scale(*scale) if self._center != (0, 0): self.image_item.translate(- self._center[0], - self._center[1]) self._scale = scale self.set_default_range() def get_scale(self): # scale property is occupied by Qt superclass. return self._scale def get_center(self): return self._center def set_x_axis(self, x_min, x_max): self._set_axis(x_min, x_max, 0) self.set_default_range() def set_y_axis(self, y_min, y_max): self._set_axis(y_min, y_max, 1) self.set_default_range() def _set_axis(self, min_: float, max_: float, axis_ind: int): shape = self.image_item.image.shape scale = np.array(self._scale) scale[axis_ind] = (max_ - min_) / shape[axis_ind] center = np.array(self._center) center[axis_ind] = - min_ / scale[axis_ind] if self._center != (0, 0) or self._scale != (1., 1.): self.image_item.resetTransform() self.image_item.scale(scale[0], scale[1]) self.image_item.translate(- center[0], - center[1]) self._scale = tuple(scale) self._center = tuple(center) def get_axes(self): shape = np.array(self.image_item.image.shape) scale = np.array(self._scale) min_ = - np.array(self._center) * scale max_ = min_ + shape * scale return (min_[0], max_[0]), (min_[1], max_[1])
class MeshPlot(GraphicsObject): def __init__(self, *args, positions: np.array=None, data: np.array=None, colormap: str=None, **kwargs): super().__init__(*args, **kwargs) # Initialize data structures self.positions = positions self.data = data self.rgb_data: Union[None, np.array] = None self.polygons: List[QtCore.QPolygonF] = [] self.xmin, self.xmax = 0, 0 self.ymin, self.ymax = 0, 0 if positions is not None and data is not None: self.calc_lims() elif not (positions is None and data is None): raise ValueError("Either positions and data must both be given, or neither.") # Initialize menus self.menu = None self.gradientSelectorMenu = None # Create LUT item self._LUTitem = HistogramLUTItem() self._LUTitem.sigLookupTableChanged.connect(self.changedColorScale) self._LUTitem.sigLevelChangeFinished.connect(self.updateRGBData) if colormap is not None: self.changeColorScale(name=colormap) else: self.changeColorScale(name=DEFAULT_CMAP) # And update color and polygon data if self.data is not None: self.updateRGBData() self.calculate_polygons() # Attach a signal handler on parent changed self._parent = None ### # Function related to plot data def setData(self, positions, data): self.positions = positions self.data = data # Calculate data size self.calc_lims() # Update plot self.updateRGBData() self.calculate_polygons() # Update histogram and autorange hist, bins = np.histogram(self.data, "auto") newBins = np.ndarray(bins.size+1) newHist = np.ndarray(hist.size+2) newBins[0] = bins[0] newBins[-1] = bins[-1] newBins[1:-1] = (bins[:-1] + bins[1:])/2 newHist[[0,-1]] = 0 newHist[1:-1] = hist self._LUTitem.plot.setData(newBins, newHist) self._LUTitem.setLevels(newBins[0], newBins[-1]) self._LUTitem.plot.getViewBox().itemBoundsChanged(self._LUTitem.plot) # Force viewport update self.getViewBox().itemBoundsChanged(self) self.update() ### # Functions relating to the size of the image def calc_lims(self): if not self.positions: self.xmin, self.xmax = 0, 0 self.ymin, self.ymax = 0, 0 return self.xmin, self.ymin = self.positions[0] self.xmax, self.ymax = self.positions[0] for x, y in islice(self.positions, 1, None): self.xmin, self.xmax = min(self.xmin, x), max(self.xmax, x) self.ymin, self.ymax = min(self.ymin, y), max(self.ymax, y) logger.debug("Calculated limits (%f, %f) - (%f, %f)", self.xmin, self.ymin, self.xmax, self.ymax) def width(self): return self.xmax - self.xmin def height(self): return self.ymax - self.ymin def boundingRect(self): tl = QtCore.QPointF(self.xmin, self.ymin) br = QtCore.QPointF(self.xmax, self.ymax) return QtCore.QRectF(tl, br) ### # Functions relating to the colorscale def setLevels(self, levels, update=True): """ Hook setLevels to update histogram when the levels are changed in the image """ super().setLevels(levels, update) self._LUTitem.setLevels(*self.levels) def changeColorScale(self, name=None): if name is None: raise ValueError("Name of color map must be given") logger.debug("Changed color scale to %s.", name) self._LUTitem.gradient.setColorMap(COLORMAPS[name]) def getHistogramLUTItem(self): return self._LUTitem @property def histogram(self): return self.getHistogramLUTItem() def changedColorScale(self): logger.debug("Changed color scale") self.updateRGBData() def updateRGBData(self): minr, maxr = self._LUTitem.getLevels() logger.debug("Recoloring to changed levels: (%f, %f)", minr, maxr) if self.data is not None: scaled = (self.data - minr)/(maxr - minr) logger.debug("Calculating new colors") self.rgb_data = self._LUTitem.gradient.colorMap().map(scaled, mode="qcolor") logger.debug("Done") self.update() ### # Functions relating to drawing def calculate_polygons(self): """ Calculate the polygons to be drawn by the mesh plot """ raise NotImplementedError() def paint(self, p, _options, _widget): logger.debug("Starting paint") visible = self.parentItem().boundingRect() if self.polygons is not None and self.polygons: p.setPen(mkPen(None)) for poly in self.polygons: #pylint: disable=not-an-iterable if not poly[1].boundingRect().intersects(visible): continue p.setBrush(self.rgb_data[poly[0]]) p.drawPolygon(poly[1]) logger.debug("Done painting") else: logger.debug("No polygons to draw") def parentChanged(self): super().parentChanged() # Add the histogram to the parent view_box = self.getViewBox() if isinstance(view_box, ExtendedPlotWindow): logger.debug("Adding _LUTitem to parent %r.", view_box) view_box.addItem(self._LUTitem) self._parent = view_box elif view_box is None: if getattr(self, "_parent", None) is not None: self._parent.removeItem(self._LUTitem) self._parent = None elif isinstance(view_box, CustomViewBox): # This second call always seems to occur... Ignore it, since we've added # ourselves to the plot window. pass else: raise NotImplementedError("parentChanged is not implemented for anything " "other than ExtendedPlotWindows at this time. " f"Got {type(view_box)}.")
class CustomImageViewer(GraphicsLayoutWidget): @property def view_box(self): return self.image_plot.vb def __init__(self, parent=None, *, hist_range: tuple = None, sigma_factor: float = 3, **kwargs): setConfigOptions(imageAxisOrder='row-major') super(CustomImageViewer, self).__init__(parent) self._raw_data = None self._use_clahe: bool = True self._scale = (1., 1.) self._center = (0, 0) self._hist_range = hist_range self._sigma_factor = sigma_factor self._init_ui(**kwargs) def _init_ui(self, **kwargs): self.setWindowTitle('Image Viewer') self.image_plot = self.addPlot(**kwargs) self.image_plot.vb.setAspectLocked() self.image_plot.vb.invertY() self.image_item = ImageItem() self.image_plot.addItem(self.image_item) self.image_plot.setMenuEnabled(False) self.hist = HistogramLUTItem() self.hist.setImageItem(self.image_item) self.addItem(self.hist) self.hist.vb.menu = CustomViewBoxMenu(self.hist.vb) self.hist.vb.menu.sigSigmaChanged.connect(self.set_sigma_factor) self.hist.vb.menu.sigRangeAsDefault.connect(self.set_limit_as_default) self.hist.vb.menu.sigUseClahe.connect(self.enable_clahe) def set_data(self, data, *, reset_axes: bool = False): self._raw_data = data if data is None: return if self._use_clahe: data = standard_contrast_correction(data) self.image_item.setImage(data) self.set_levels() if reset_axes: self.image_item.resetTransform() self.set_default_range() def hist_params(self) -> dict: return dict(sigma_factor=self._sigma_factor, hist_range=self._hist_range) def clear_image(self): self.set_data(np.zeros((1, 1))) def set_default_range(self): if self.image_item.image is None: return # self.set_auto_range() axes = self.get_axes() self.image_plot.setRange(xRange=axes[1], yRange=axes[0]) @pyqtSlot(bool) def enable_clahe(self, enable: bool): self._use_clahe = enable self.set_data(self._raw_data) def set_auto_range(self): self.image_plot.autoRange() def set_levels(self): img = self.image_item.image if img is None: return if self._sigma_factor and self._sigma_factor > 0: m, s = img.flatten().mean( ), img.flatten().std() * self._sigma_factor self.hist.setLevels(max(m - s, img.min()), min(m + s, img.max())) elif self._hist_range: self.hist.setLevels(*self._hist_range) else: self.hist.setLevels(self.image_item.image.min(), self.image_item.image.max()) @pyqtSlot(float) def set_sigma_factor(self, sigma_factor: float): self._sigma_factor = sigma_factor self.set_levels() @pyqtSlot() def set_limit_as_default(self): self._hist_range = self.hist.getLevels() self._sigma_factor = None self.set_levels() def get_levels(self): return self.hist.getLevels() def set_center(self, center: tuple, pixel_units: bool = True): if not pixel_units: scale = self.get_scale() center = (center[0] / scale[0], center[1] / scale[1]) if self._center != (0, 0) or self._scale != (1., 1.): self.image_item.resetTransform() self.image_item.scale(*self._scale) self.image_item.translate(-center[0], -center[1]) self._center = center self.set_default_range() def set_scale(self, scale: float or tuple): if isinstance(scale, float) or isinstance(scale, int): scale = (scale, scale) if self._center != (0, 0) or self._scale != (1., 1.): self.image_item.resetTransform() self.image_item.scale(*scale) if self._center != (0, 0): self.image_item.translate(-self._center[0], -self._center[1]) self._scale = scale self.set_default_range() def get_scale(self) -> tuple: # scale property is occupied by Qt superclass. return self._scale def get_center(self) -> tuple: return self._center def set_x_axis(self, x_min, x_max): self._set_axis(x_min, x_max, 0) self.set_default_range() def set_y_axis(self, y_min, y_max): self._set_axis(y_min, y_max, 1) self.set_default_range() def _set_axis(self, min_: float, max_: float, axis_ind: int): shape = self.image_item.image.shape scale = np.array(self._scale) scale[axis_ind] = (max_ - min_) / shape[axis_ind] center = np.array(self._center) center[axis_ind] = -min_ / scale[axis_ind] if self._center != (0, 0) or self._scale != (1., 1.): self.image_item.resetTransform() self.image_item.scale(scale[0], scale[1]) self.image_item.translate(-center[0], -center[1]) self._scale = tuple(scale) self._center = tuple(center) def get_axes(self): shape = np.array(self.image_item.image.shape) scale = np.array(self._scale) min_ = -np.array((self._center[1], self._center[0])) * scale max_ = min_ + shape * scale return (min_[0], max_[0]), (min_[1], max_[1])