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 Bp2DWidget(PlotWidget): def __init__(self): super(Bp2DWidget, self).__init__() # M.B. plot add to params self.setXRange(-60, 60) self.setYRange(-60, 60) self.img = ImageItem() self.addItem(self.img) _translate = QCoreApplication.translate self.setLabels(title=_translate("Bp2DWidget", "Beam pattern"), left=_translate("Bp2DWidget", "Elevation, °"), bottom=_translate("Bp2DWidget", "Azimuth, °")) self.setLogMode() colormap = ColorMap(*zip(*Gradients["bipolar"]["ticks"])) self.img.setLookupTable(colormap.getLookupTable()) #gradient_legend = GradientLegend(10, 10) #self.addItem(gradient_legend) @pyqtSlot() def on_data_changed(self): sender = self.sender() self.img.setImage(np.rot90(sender.data, -1)) self.img.setRect(self.__ensure_rect(np.shape(sender.data))) def __ensure_rect(self, shape): x_offset = 120. / (shape[0]-1) / 2 y_offset = 120. / (shape[1]-1) / 2 return QRectF(-60 - x_offset, 60 + y_offset, 120 + x_offset * 2, -120 - y_offset * 2)
def update_img(self, i: int, j: int, img: pg.ImageItem): """Template function for creating image update callback functions. i is the row axis, j is the col axis corresponding to the image. xy is 0, 1 and zy is 2, 1""" data_slice = [self.cursor.index[i].val for i in range(self.data.ndim)] data_slice[i] = slice(None) data_slice[j] = slice(None) if j > i: img.setImage(self.data_slice2d(i, j)) else: img.setImage(self.data_slice2d(i, j).T)
def _update_preview_image(image_data: Optional[np.ndarray], image: ImageItem, redraw_histogram: Optional[Callable[[Any], None]]): image.clear() image.setImage(image_data) if redraw_histogram: # Update histogram redraw_histogram(image.getHistogram())
class PreviewWidget(GraphicsLayoutWidget): def __init__(self): super(PreviewWidget, self).__init__() self.setMinimumHeight(250) self.setMinimumWidth(250) self.view = self.addViewBox(lockAspect=True, enableMenu=False) self.imageitem = ImageItem() self.textitem = TextItem(anchor=(0.5, 0)) self.textitem.setFont(QFont("Zero Threes")) self.imgdata = None self.imageitem.setOpts(axisOrder="row-major") self.view.addItem(self.imageitem) self.view.addItem(self.textitem) self.textitem.hide() self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) # def textItemBounds(axis, frac=1.0, orthoRange=None): # b = self.textitem.boundingRect() # sx, sy = self.view.viewPixelSize() # x, y = sx*b.width(), sy*b.height() # if axis == 0: return (-x/2, x/2) # if axis == 1: return (0, y) # # self.textitem.dataBounds = textItemBounds def sizeHint(self): return QSize(250, 250) def preview_header(self, header: NonDBHeader): try: data = header.meta_array()[0] self.setImage(data) except IndexError: self.imageitem.clear() self.setText("UNKNOWN DATA FORMAT") def setImage(self, imgdata): self.imageitem.clear() self.textitem.hide() self.imgdata = imgdata self.imageitem.setImage(np.log(self.imgdata * (self.imgdata > 0) + (self.imgdata < 1)), autoLevels=True) self.imageitem.setTransform( QTransform(1, 0, 0, -1, 0, self.imgdata.shape[-2])) self.view.autoRange() def setText(self, text): self.textitem.setText(text) self.imageitem.clear() self.textitem.setVisible(True) self.view.autoRange()
class CamView(PlotWidget): def __init__(self, cam: Cam, parent=None) -> None: super().__init__(parent=parent) self.cam = cam self.main_plot_item = self.plotItem self.last_centers: List[Tuple[float]] = [] self.image_item = ImageItem(cam.read_cam()) #cmap = colormap.get('CET-C2') #self.image_item.setLookupTable(cmap.getLookupTable()) self.target = TargetItem(movable=False) self.main_plot_item.addItem(self.image_item) self.main_plot_item.addItem(self.target) self.iso_curve = IsocurveItem() self.iso_curve.setParentItem(self.image_item) self.iso_curve2 = IsocurveItem() self.iso_curve2.setParentItem(self.image_item) self.main_plot_item.addItem(self.iso_curve) self.center_hist = self.main_plot_item.plot(pen='r') self.update_cam() @Slot() def update_cam(self): #print('update') image = self.cam.last_image self.image_item.setImage(image) if not self.cam.last_fit is None: lf = self.cam.last_fit center = (lf.params['x0'], lf.params['y0']) self.target.setPos(*center) self.last_centers = [center] + self.last_centers[:30] self.iso_curve.setData(lf.best_fit, lf.best_fit.max() * 0.5) self.iso_curve2.setData(lf.best_fit, lf.best_fit.max() * np.exp(-2)) arr = np.array(self.last_centers) self.center_hist.setData(x=arr[:, 0], y=arr[:, 1]) @Slot() def reset_hist(self): self.last_centers = []
class SiriusSpectrogramView(GraphicsLayoutWidget, PyDMWidget, PyDMColorMap, ReadingOrder): """ A SpectrogramView with support for Channels and more from PyDM. If there is no :attr:`channelWidth` it is possible to define the width of the image with the :attr:`width` property. The :attr:`normalizeData` property defines if the colors of the images are relative to the :attr:`colorMapMin` and :attr:`colorMapMax` property or to the minimum and maximum values of the image. Use the :attr:`newImageSignal` to hook up to a signal that is emitted when a new image is rendered in the widget. Parameters ---------- parent : QWidget The parent widget for the Label image_channel : str, optional The channel to be used by the widget for the image data. xaxis_channel : str, optional The channel to be used by the widget to receive the image width (if ReadingOrder == Clike), and to set the xaxis values yaxis_channel : str, optional The channel to be used by the widget to receive the image width (if ReadingOrder == Fortranlike), and to set the yaxis values background : QColor, optional QColor to set the background color of the GraphicsView """ Q_ENUMS(PyDMColorMap) Q_ENUMS(ReadingOrder) color_maps = cmaps def __init__(self, parent=None, image_channel=None, xaxis_channel=None, yaxis_channel=None, roioffsetx_channel=None, roioffsety_channel=None, roiwidth_channel=None, roiheight_channel=None, title='', background='w', image_width=0, image_height=0): """Initialize widget.""" GraphicsLayoutWidget.__init__(self, parent) PyDMWidget.__init__(self) self.thread = None self._imagechannel = None self._xaxischannel = None self._yaxischannel = None self._roioffsetxchannel = None self._roioffsetychannel = None self._roiwidthchannel = None self._roiheightchannel = None self._channels = 7 * [ None, ] self.image_waveform = np.zeros(0) self._image_width = image_width if not xaxis_channel else 0 self._image_height = image_height if not yaxis_channel else 0 self._roi_offsetx = 0 self._roi_offsety = 0 self._roi_width = 0 self._roi_height = 0 self._normalize_data = False self._auto_downsample = True self._last_yaxis_data = None self._last_xaxis_data = None self._auto_colorbar_lims = True self.format_tooltip = '{0:.4g}, {1:.4g}' # ViewBox and imageItem. self._view = ViewBox() self._image_item = ImageItem() self._view.addItem(self._image_item) # ROI self.ROICurve = PlotCurveItem([0, 0, 0, 0, 0], [0, 0, 0, 0, 0]) self.ROIColor = QColor('red') pen = mkPen() pen.setColor(QColor('transparent')) pen.setWidth(1) self.ROICurve.setPen(pen) self._view.addItem(self.ROICurve) # Axis. self.xaxis = AxisItem('bottom') self.xaxis.setPen(QColor(0, 0, 0)) if not xaxis_channel: self.xaxis.setVisible(False) self.yaxis = AxisItem('left') self.yaxis.setPen(QColor(0, 0, 0)) if not yaxis_channel: self.yaxis.setVisible(False) # Colorbar legend. self.colorbar = _GradientLegend() # Title. start_row = 0 if title: self.title = LabelItem(text=title, color='#000000') self.addItem(self.title, 0, 0, 1, 3) start_row = 1 # Set layout. self.addItem(self._view, start_row, 1) self.addItem(self.yaxis, start_row, 0) self.addItem(self.colorbar, start_row, 2) self.addItem(self.xaxis, start_row + 1, 1) self.setBackground(background) self.ci.layout.setColumnSpacing(0, 0) self.ci.layout.setRowSpacing(start_row, 0) # Set color map limits. self.cm_min = 0.0 self.cm_max = 255.0 # Set default reading order of numpy array data to Clike. self._reading_order = ReadingOrder.Clike # Make a right-click menu for changing the color map. self.cm_group = QActionGroup(self) self.cmap_for_action = {} for cm in self.color_maps: action = self.cm_group.addAction(cmap_names[cm]) action.setCheckable(True) self.cmap_for_action[action] = cm # Set the default colormap. self._cm_colors = None self.colorMap = PyDMColorMap.Inferno # Setup the redraw timer. self.needs_redraw = False self.redraw_timer = QTimer(self) self.redraw_timer.timeout.connect(self.redrawImage) self._redraw_rate = 30 self.maxRedrawRate = self._redraw_rate self.newImageSignal = self._image_item.sigImageChanged # Set Channels. self.imageChannel = image_channel self.xAxisChannel = xaxis_channel self.yAxisChannel = yaxis_channel self.ROIOffsetXChannel = roioffsetx_channel self.ROIOffsetYChannel = roioffsety_channel self.ROIWidthChannel = roiwidth_channel self.ROIHeightChannel = roiheight_channel # --- Context menu --- def widget_ctx_menu(self): """ Fetch the Widget specific context menu. It will be populated with additional tools by `assemble_tools_menu`. Returns ------- QMenu or None If the return of this method is None a new QMenu will be created by `assemble_tools_menu`. """ self.menu = ViewBoxMenu(self._view) cm_menu = self.menu.addMenu("Color Map") for act in self.cmap_for_action.keys(): cm_menu.addAction(act) cm_menu.triggered.connect(self._changeColorMap) return self.menu # --- Colormap methods --- def _changeColorMap(self, action): """ Method invoked by the colormap Action Menu. Changes the current colormap used to render the image. Parameters ---------- action : QAction """ self.colorMap = self.cmap_for_action[action] @Property(float) def colorMapMin(self): """ Minimum value for the colormap. Returns ------- float """ return self.cm_min @colorMapMin.setter @Slot(float) def colorMapMin(self, new_min): """ Set the minimum value for the colormap. Parameters ---------- new_min : float """ if self.cm_min != new_min: self.cm_min = new_min if self.cm_min > self.cm_max: self.cm_max = self.cm_min @Property(float) def colorMapMax(self): """ Maximum value for the colormap. Returns ------- float """ return self.cm_max @colorMapMax.setter @Slot(float) def colorMapMax(self, new_max): """ Set the maximum value for the colormap. Parameters ---------- new_max : float """ if self.cm_max != new_max: self.cm_max = new_max if self.cm_max < self.cm_min: self.cm_min = self.cm_max def setColorMapLimits(self, mn, mx): """ Set the limit values for the colormap. Parameters ---------- mn : int The lower limit mx : int The upper limit """ if mn >= mx: return self.cm_max = mx self.cm_min = mn @Property(PyDMColorMap) def colorMap(self): """ Return the color map used by the SpectrogramView. Returns ------- PyDMColorMap """ return self._colormap @colorMap.setter def colorMap(self, new_cmap): """ Set the color map used by the SpectrogramView. Parameters ------- new_cmap : PyDMColorMap """ self._colormap = new_cmap self._cm_colors = self.color_maps[new_cmap] self.setColorMap() for action in self.cm_group.actions(): if self.cmap_for_action[action] == self._colormap: action.setChecked(True) else: action.setChecked(False) def setColorMap(self, cmap=None): """ Update the image colormap. Parameters ---------- cmap : ColorMap """ if not cmap: if not self._cm_colors.any(): return # Take default values pos = np.linspace(0.0, 1.0, num=len(self._cm_colors)) cmap = ColorMap(pos, self._cm_colors) self._view.setBackgroundColor(cmap.map(0)) lut = cmap.getLookupTable(0.0, 1.0, alpha=False) self.colorbar.setIntColorScale(colors=lut) self._image_item.setLookupTable(lut) # --- Connection Slots --- @Slot(bool) def image_connection_state_changed(self, conn): """ Callback invoked when the Image Channel connection state is changed. Parameters ---------- conn : bool The new connection state. """ if conn: self.redraw_timer.start() else: self.redraw_timer.stop() @Slot(bool) def yaxis_connection_state_changed(self, connected): """ Callback invoked when the TimeAxis Channel connection state is changed. Parameters ---------- conn : bool The new connection state. """ self._timeaxis_connected = connected @Slot(bool) def roioffsetx_connection_state_changed(self, conn): """ Run when the ROIOffsetX Channel connection state changes. Parameters ---------- conn : bool The new connection state. """ if not conn: self._roi_offsetx = 0 @Slot(bool) def roioffsety_connection_state_changed(self, conn): """ Run when the ROIOffsetY Channel connection state changes. Parameters ---------- conn : bool The new connection state. """ if not conn: self._roi_offsety = 0 @Slot(bool) def roiwidth_connection_state_changed(self, conn): """ Run when the ROIWidth Channel connection state changes. Parameters ---------- conn : bool The new connection state. """ if not conn: self._roi_width = 0 @Slot(bool) def roiheight_connection_state_changed(self, conn): """ Run when the ROIHeight Channel connection state changes. Parameters ---------- conn : bool The new connection state. """ if not conn: self._roi_height = 0 # --- Value Slots --- @Slot(np.ndarray) def image_value_changed(self, new_image): """ Callback invoked when the Image Channel value is changed. We try to do as little as possible in this method, because it gets called every time the image channel updates, which might be extremely often. Basically just store the data, and set a flag requesting that the image be redrawn. Parameters ---------- new_image : np.ndarray The new image data. This can be a flat 1D array, or a 2D array. """ if new_image is None or new_image.size == 0: return logging.debug("SpectrogramView Received New Image: Needs Redraw->True") self.image_waveform = new_image self.needs_redraw = True if not self._image_height and self._image_width: self._image_height = new_image.size / self._image_width elif not self._image_width and self._image_height: self._image_width = new_image.size / self._image_height @Slot(np.ndarray) @Slot(float) def xaxis_value_changed(self, new_array): """ Callback invoked when the Image Width Channel value is changed. Parameters ---------- new_array : np.ndarray The new x axis array """ if new_array is None: return if isinstance(new_array, float): new_array = np.array([ new_array, ]) self._last_xaxis_data = new_array if self._reading_order == self.Clike: self._image_width = new_array.size else: self._image_height = new_array.size self.needs_redraw = True @Slot(np.ndarray) @Slot(float) def yaxis_value_changed(self, new_array): """ Callback invoked when the TimeAxis Channel value is changed. Parameters ---------- new_array : np.array The new y axis array """ if new_array is None: return if isinstance(new_array, float): new_array = np.array([ new_array, ]) self._last_yaxis_data = new_array if self._reading_order == self.Fortranlike: self._image_width = new_array.size else: self._image_height = new_array.size self.needs_redraw = True @Slot(int) def roioffsetx_value_changed(self, new_offset): """ Run when the ROIOffsetX Channel value changes. Parameters ---------- new_offsetx : int The new image ROI horizontal offset """ if new_offset is None: return self._roi_offsetx = new_offset self.redrawROI() @Slot(int) def roioffsety_value_changed(self, new_offset): """ Run when the ROIOffsetY Channel value changes. Parameters ---------- new_offsety : int The new image ROI vertical offset """ if new_offset is None: return self._roi_offsety = new_offset self.redrawROI() @Slot(int) def roiwidth_value_changed(self, new_width): """ Run when the ROIWidth Channel value changes. Parameters ---------- new_width : int The new image ROI width """ if new_width is None: return self._roi_width = int(new_width) self.redrawROI() @Slot(int) def roiheight_value_changed(self, new_height): """ Run when the ROIHeight Channel value changes. Parameters ---------- new_height : int The new image ROI height """ if new_height is None: return self._roi_height = int(new_height) self.redrawROI() # --- Image update methods --- def process_image(self, image): """ Boilerplate method. To be used by applications in order to add calculations and also modify the image before it is displayed at the widget. .. warning:: This code runs in a separated QThread so it **MUST** not try to write to QWidgets. Parameters ---------- image : np.ndarray The Image Data as a 2D numpy array Returns ------- np.ndarray The Image Data as a 2D numpy array after processing. """ return image def redrawImage(self): """ Set the image data into the ImageItem, if needed. If necessary, reshape the image to 2D first. """ if self.thread is not None and not self.thread.isFinished(): logger.warning( "Image processing has taken longer than the refresh rate.") return self.thread = SpectrogramUpdateThread(self) self.thread.updateSignal.connect(self._updateDisplay) logging.debug("SpectrogramView RedrawImage Thread Launched") self.thread.start() @Slot(list) def _updateDisplay(self, data): logging.debug("SpectrogramView Update Display with new image") # Update axis if self._last_xaxis_data is not None: szx = self._last_xaxis_data.size xMin = self._last_xaxis_data.min() xMax = self._last_xaxis_data.max() else: szx = self.imageWidth if self.readingOrder == self.Clike \ else self.imageHeight xMin = 0 xMax = szx if self._last_yaxis_data is not None: szy = self._last_yaxis_data.size yMin = self._last_yaxis_data.min() yMax = self._last_yaxis_data.max() else: szy = self.imageHeight if self.readingOrder == self.Clike \ else self.imageWidth yMin = 0 yMax = szy self.xaxis.setRange(xMin, xMax) self.yaxis.setRange(yMin, yMax) self._view.setLimits(xMin=0, xMax=szx, yMin=0, yMax=szy, minXRange=szx, maxXRange=szx, minYRange=szy, maxYRange=szy) # Update image if self.autoSetColorbarLims: self.colorbar.setLimits(data) mini, maxi = data[0], data[1] img = data[2] self._image_item.setLevels([mini, maxi]) self._image_item.setImage(img, autoLevels=False, autoDownsample=self.autoDownsample) # ROI update methods def redrawROI(self): startx = self._roi_offsetx endx = self._roi_offsetx + self._roi_width starty = self._roi_offsety endy = self._roi_offsety + self._roi_height self.ROICurve.setData([startx, startx, endx, endx, startx], [starty, endy, endy, starty, starty]) def showROI(self, show): """Set ROI visibility.""" pen = mkPen() if show: pen.setColor(self.ROIColor) else: pen.setColor(QColor('transparent')) self.ROICurve.setPen(pen) # --- Properties --- @Property(bool) def autoDownsample(self): """ Return if we should or not apply the autoDownsample option. Return ------ bool """ return self._auto_downsample @autoDownsample.setter def autoDownsample(self, new_value): """ Whether we should or not apply the autoDownsample option. Parameters ---------- new_value: bool """ if new_value != self._auto_downsample: self._auto_downsample = new_value @Property(bool) def autoSetColorbarLims(self): """ Return if we should or not auto set colorbar limits. Return ------ bool """ return self._auto_colorbar_lims @autoSetColorbarLims.setter def autoSetColorbarLims(self, new_value): """ Whether we should or not auto set colorbar limits. Parameters ---------- new_value: bool """ if new_value != self._auto_colorbar_lims: self._auto_colorbar_lims = new_value @Property(int) def imageWidth(self): """ Return the width of the image. Return ------ int """ return self._image_width @imageWidth.setter def imageWidth(self, new_width): """ Set the width of the image. Can be overridden by :attr:`xAxisChannel` and :attr:`yAxisChannel`. Parameters ---------- new_width: int """ boo = self._image_width != int(new_width) boo &= not self._xaxischannel boo &= not self._yaxischannel if boo: self._image_width = int(new_width) @Property(int) def imageHeight(self): """ Return the height of the image. Return ------ int """ return self._image_height @Property(int) def ROIOffsetX(self): """ Return the ROI offset in X axis in pixels. Return ------ int """ return self._roi_offsetx @ROIOffsetX.setter def ROIOffsetX(self, new_offset): """ Set the ROI offset in X axis in pixels. Can be overridden by :attr:`ROIOffsetXChannel`. Parameters ---------- new_offset: int """ if new_offset is None: return boo = self._roi_offsetx != int(new_offset) boo &= not self._roioffsetxchannel if boo: self._roi_offsetx = int(new_offset) self.redrawROI() @Property(int) def ROIOffsetY(self): """ Return the ROI offset in Y axis in pixels. Return ------ int """ return self._roi_offsety @ROIOffsetY.setter def ROIOffsetY(self, new_offset): """ Set the ROI offset in Y axis in pixels. Can be overridden by :attr:`ROIOffsetYChannel`. Parameters ---------- new_offset: int """ if new_offset is None: return boo = self._roi_offsety != int(new_offset) boo &= not self._roioffsetychannel if boo: self._roi_offsety = int(new_offset) self.redrawROI() @Property(int) def ROIWidth(self): """ Return the ROI width in pixels. Return ------ int """ return self._roi_width @ROIWidth.setter def ROIWidth(self, new_width): """ Set the ROI width in pixels. Can be overridden by :attr:`ROIWidthChannel`. Parameters ---------- new_width: int """ if new_width is None: return boo = self._roi_width != int(new_width) boo &= not self._roiwidthchannel if boo: self._roi_width = int(new_width) self.redrawROI() @Property(int) def ROIHeight(self): """ Return the ROI height in pixels. Return ------ int """ return self._roi_height @ROIHeight.setter def ROIHeight(self, new_height): """ Set the ROI height in pixels. Can be overridden by :attr:`ROIHeightChannel`. Parameters ---------- new_height: int """ if new_height is None: return boo = self._roi_height != int(new_height) boo &= not self._roiheightchannel if boo: self._roi_height = int(new_height) self.redrawROI() @Property(bool) def normalizeData(self): """ Return True if the colors are relative to data maximum and minimum. Returns ------- bool """ return self._normalize_data @normalizeData.setter @Slot(bool) def normalizeData(self, new_norm): """ Define if the colors are relative to minimum and maximum of the data. Parameters ---------- new_norm: bool """ if self._normalize_data != new_norm: self._normalize_data = new_norm @Property(ReadingOrder) def readingOrder(self): """ Return the reading order of the :attr:`imageChannel` array. Returns ------- ReadingOrder """ return self._reading_order @readingOrder.setter def readingOrder(self, order): """ Set reading order of the :attr:`imageChannel` array. Parameters ---------- order: ReadingOrder """ if self._reading_order != order: self._reading_order = order if order == self.Clike: if self._last_xaxis_data is not None: self._image_width = self._last_xaxis_data.size if self._last_yaxis_data is not None: self._image_height = self._last_yaxis_data.size elif order == self.Fortranlike: if self._last_yaxis_data is not None: self._image_width = self._last_yaxis_data.size if self._last_xaxis_data is not None: self._image_height = self._last_xaxis_data.size @Property(int) def maxRedrawRate(self): """ The maximum rate (in Hz) at which the plot will be redrawn. The plot will not be redrawn if there is not new data to draw. Returns ------- int """ return self._redraw_rate @maxRedrawRate.setter def maxRedrawRate(self, redraw_rate): """ The maximum rate (in Hz) at which the plot will be redrawn. The plot will not be redrawn if there is not new data to draw. Parameters ------- redraw_rate : int """ self._redraw_rate = redraw_rate self.redraw_timer.setInterval(int((1.0 / self._redraw_rate) * 1000)) # --- Events rederivations --- def keyPressEvent(self, ev): """Handle keypress events.""" return def mouseMoveEvent(self, ev): if not self._image_item.width() or not self._image_item.height(): super().mouseMoveEvent(ev) return pos = ev.pos() posaux = self._image_item.mapFromDevice(ev.pos()) if posaux.x() < 0 or posaux.x() >= self._image_item.width() or \ posaux.y() < 0 or posaux.y() >= self._image_item.height(): super().mouseMoveEvent(ev) return pos_scene = self._view.mapSceneToView(pos) x = round(pos_scene.x()) y = round(pos_scene.y()) if self.xAxisChannel and self._last_xaxis_data is not None: maxx = len(self._last_xaxis_data) - 1 x = x if x < maxx else maxx valx = self._last_xaxis_data[x] else: valx = x if self.yAxisChannel and self._last_yaxis_data is not None: maxy = len(self._last_yaxis_data) - 1 y = y if y < maxy else maxy valy = self._last_yaxis_data[y] else: valy = y txt = self.format_tooltip.format(valx, valy) QToolTip.showText(self.mapToGlobal(pos), txt, self, self.geometry(), 5000) super().mouseMoveEvent(ev) # --- Channels --- @Property(str) def imageChannel(self): """ The channel address in use for the image data . Returns ------- str Channel address """ if self._imagechannel: return str(self._imagechannel.address) else: return '' @imageChannel.setter def imageChannel(self, value): """ The channel address in use for the image data . Parameters ---------- value : str Channel address """ if self._imagechannel != value: # Disconnect old channel if self._imagechannel: self._imagechannel.disconnect() # Create and connect new channel self._imagechannel = PyDMChannel( address=value, connection_slot=self.image_connection_state_changed, value_slot=self.image_value_changed, severity_slot=self.alarmSeverityChanged) self._channels[0] = self._imagechannel self._imagechannel.connect() @Property(str) def xAxisChannel(self): """ The channel address in use for the x-axis of image. Returns ------- str Channel address """ if self._xaxischannel: return str(self._xaxischannel.address) else: return '' @xAxisChannel.setter def xAxisChannel(self, value): """ The channel address in use for the x-axis of image. Parameters ---------- value : str Channel address """ if self._xaxischannel != value: # Disconnect old channel if self._xaxischannel: self._xaxischannel.disconnect() # Create and connect new channel self._xaxischannel = PyDMChannel( address=value, connection_slot=self.connectionStateChanged, value_slot=self.xaxis_value_changed, severity_slot=self.alarmSeverityChanged) self._channels[1] = self._xaxischannel self._xaxischannel.connect() @Property(str) def yAxisChannel(self): """ The channel address in use for the time axis. Returns ------- str Channel address """ if self._yaxischannel: return str(self._yaxischannel.address) else: return '' @yAxisChannel.setter def yAxisChannel(self, value): """ The channel address in use for the time axis. Parameters ---------- value : str Channel address """ if self._yaxischannel != value: # Disconnect old channel if self._yaxischannel: self._yaxischannel.disconnect() # Create and connect new channel self._yaxischannel = PyDMChannel( address=value, connection_slot=self.yaxis_connection_state_changed, value_slot=self.yaxis_value_changed, severity_slot=self.alarmSeverityChanged) self._channels[2] = self._yaxischannel self._yaxischannel.connect() @Property(str) def ROIOffsetXChannel(self): """ Return the channel address in use for the image ROI horizontal offset. Returns ------- str Channel address """ if self._roioffsetxchannel: return str(self._roioffsetxchannel.address) else: return '' @ROIOffsetXChannel.setter def ROIOffsetXChannel(self, value): """ Return the channel address in use for the image ROI horizontal offset. Parameters ---------- value : str Channel address """ if self._roioffsetxchannel != value: # Disconnect old channel if self._roioffsetxchannel: self._roioffsetxchannel.disconnect() # Create and connect new channel self._roioffsetxchannel = PyDMChannel( address=value, connection_slot=self.roioffsetx_connection_state_changed, value_slot=self.roioffsetx_value_changed, severity_slot=self.alarmSeverityChanged) self._channels[3] = self._roioffsetxchannel self._roioffsetxchannel.connect() @Property(str) def ROIOffsetYChannel(self): """ Return the channel address in use for the image ROI vertical offset. Returns ------- str Channel address """ if self._roioffsetychannel: return str(self._roioffsetychannel.address) else: return '' @ROIOffsetYChannel.setter def ROIOffsetYChannel(self, value): """ Return the channel address in use for the image ROI vertical offset. Parameters ---------- value : str Channel address """ if self._roioffsetychannel != value: # Disconnect old channel if self._roioffsetychannel: self._roioffsetychannel.disconnect() # Create and connect new channel self._roioffsetychannel = PyDMChannel( address=value, connection_slot=self.roioffsety_connection_state_changed, value_slot=self.roioffsety_value_changed, severity_slot=self.alarmSeverityChanged) self._channels[4] = self._roioffsetychannel self._roioffsetychannel.connect() @Property(str) def ROIWidthChannel(self): """ Return the channel address in use for the image ROI width. Returns ------- str Channel address """ if self._roiwidthchannel: return str(self._roiwidthchannel.address) else: return '' @ROIWidthChannel.setter def ROIWidthChannel(self, value): """ Return the channel address in use for the image ROI width. Parameters ---------- value : str Channel address """ if self._roiwidthchannel != value: # Disconnect old channel if self._roiwidthchannel: self._roiwidthchannel.disconnect() # Create and connect new channel self._roiwidthchannel = PyDMChannel( address=value, connection_slot=self.roiwidth_connection_state_changed, value_slot=self.roiwidth_value_changed, severity_slot=self.alarmSeverityChanged) self._channels[5] = self._roiwidthchannel self._roiwidthchannel.connect() @Property(str) def ROIHeightChannel(self): """ Return the channel address in use for the image ROI height. Returns ------- str Channel address """ if self._roiheightchannel: return str(self._roiheightchannel.address) else: return '' @ROIHeightChannel.setter def ROIHeightChannel(self, value): """ Return the channel address in use for the image ROI height. Parameters ---------- value : str Channel address """ if self._roiheightchannel != value: # Disconnect old channel if self._roiheightchannel: self._roiheightchannel.disconnect() # Create and connect new channel self._roiheightchannel = PyDMChannel( address=value, connection_slot=self.roiheight_connection_state_changed, value_slot=self.roiheight_value_changed, severity_slot=self.alarmSeverityChanged) self._channels[6] = self._roiheightchannel self._roiheightchannel.connect() def channels(self): """ Return the channels being used for this Widget. Returns ------- channels : list List of PyDMChannel objects """ return self._channels def channels_for_tools(self): """Return channels for tools.""" return [self._imagechannel]
class SliceableGraphicsView(GraphicsView): sigToggleHorizontalSlice = Signal(bool) sigToggleVerticalSlice = Signal(bool) sigMakePrimary = Signal(object, object) def __init__(self): super(SliceableGraphicsView, self).__init__() self.setContentsMargins(0, 0, 0, 0) # Add axes self.view = SliceableAxes() self.view.axes["left"]["item"].setZValue(10) self.view.axes["top"]["item"].setZValue(10) self.setCentralItem(self.view) self.view.sigToggleVerticalSlice.connect(self.sigToggleVerticalSlice) self.view.sigToggleHorizontalSlice.connect(self.sigToggleHorizontalSlice) self.view.sigMakePrimary.connect(self.sigMakePrimary) # Add imageitem self.image_item = ImageItem() self.view.addItem(self.image_item) # add crosshair self.crosshair = BetterCrosshairROI((0, 0), parent=self.view, resizable=False) self.view.getViewBox().addItem(self.crosshair) def setData(self, data): xvals = data.coords[data.dims[-1]] yvals = data.coords[data.dims[-2]] xmin = float(xvals.min()) xmax = float(xvals.max()) ymin = float(yvals.min()) ymax = float(yvals.max()) # Position the image according to coords shape = data.shape a = [(0, shape[-1]), (shape[-2] - 1, shape[-1]), (shape[-2] - 1, 1), (0, 1)] # b = [(ymin, xmax), (ymax, xmax), (ymax, xmin), (ymin, xmin)] b = [(xmax, ymin), (xmax, ymax), (xmin, ymax), (xmin, ymin)] quad1 = QPolygonF() quad2 = QPolygonF() for p, q in zip(a, b): quad1.append(QPointF(*p)) quad2.append(QPointF(*q)) transform = QTransform() QTransform.quadToQuad(quad1, quad2, transform) # Bind coords from the xarray to the timeline axis # super(SliceableGraphicsView, self).setImage(img, autoRange, autoLevels, levels, axes, np.asarray(img.coords[img.dims[0]]), pos, scale, transform, autoHistogramRange, levelMode) self.image_item.setImage(np.asarray(data), autoLevels=False) self.image_item.setTransform(transform) # Label the image axes self.view.setLabel('left', data.dims[-2]) self.view.setLabel('bottom', data.dims[-1]) def resetCrosshair(self): transform = self.image_item.viewTransform() new_pos = transform.map(self.image_item.boundingRect().center()) self.crosshair.setPos(new_pos) self.crosshair.sigMoved.emit(new_pos) def updateImage(self, autoHistogramRange=True): ## Redraw image on screen if self.image is None: return image = self.getProcessedImage() if autoHistogramRange: self.ui.histogram.setHistogramRange(self.levelMin, self.levelMax) # Transpose image into order expected by ImageItem if self.imageItem.axisOrder == 'col-major': axorder = ['t', 'x', 'y', 'c'] else: axorder = ['t', 'y', 'x', 'c'] axorder = [self.axes[ax] for ax in axorder if self.axes[ax] is not None] ax_swap = [image.dims[ax_index] for ax_index in axorder] image = image.transpose(*ax_swap) # Select time index if self.axes['t'] is not None: self.ui.roiPlot.show() image = image[self.currentIndex] self.imageItem.updateImage(np.asarray(image)) def quickMinMax(self, data): """ Estimate the min/max values of *data* by subsampling. MODIFIED TO USE THE 99TH PERCENTILE instead of max. """ if data is None: return 0, 0 sl = slice(None, None, max(1, int(data.size // 1e6))) data = np.asarray(data[sl]) levels = (np.nanmin(data), np.nanpercentile(np.where(data < np.nanmax(data), data, np.nanmin(data)), 99)) return [levels]
class PsdWaterfallPlotWidget(GraphicsLayoutWidget): """This class manages and displays the power spectrum distribution (PSD) data in a waterfall plot. Attributes ---------- arraySize : int The size of the data array to display. boundingRect : QtCore.QRectF The actual coordinate space base on frequency and time of acquisition. data : numpy.ndarray The 2D array for the PSD data. image : pyqtgraph.ImageItem The instance of the image item for display. timeScale : float The total time for the buffer to accumulate at the ROI FPS. """ def __init__(self, parent=None): """Initialize the class. Parameters ---------- parent : None, optional Top-level widget. """ super().__init__(parent) self.plot = self.addPlot() self.plot.invertY() self.image = ImageItem() self.image.setOpts(axisOrder='row-major') self.plot.addItem(self.image) self.data = None self.arraySize = None self.boundingRect = None self.timeScale = None self.colorMap = 'viridis' self.image.setLookupTable(getLutFromColorMap(self.colorMap)) def clearPlot(self): """Reset all data and clear the plot. """ self.data = None self.boundingRect = None self.image.clear() def getConfiguration(self): """Get the current plot configuration. Returns ------- int, str The set of current configuration parameters. """ return self.arraySize, self.colorMap def setConfiguration(self, config): """Set the new parameters into the widget. Parameters ---------- config : `config.PsdPlotConfig` The new parameters to apply. """ numBins = config.numWaterfallBins if self.arraySize != numBins: self.arraySize = numBins # Invalidate data self.data = None self.boundingRect = None colorMap = config.waterfallColorMap if self.colorMap != colorMap: self.colorMap = colorMap self.image.setLookupTable(getLutFromColorMap(self.colorMap)) def setTimeScale(self, timeScale): """Update the stored timescale and invalidate data and bounding rect. Parameters ---------- timeScale : float The new timescale. """ self.timeScale = timeScale self.data = None self.boundingRect = None def setup(self, arraySize, timeScale, axisLabel): """Setup the widget with the array size. Parameters ---------- arraySize : int The size fo the data array to display in terms of history. timeScale : float The total time for the buffer to accumulate at the ROI FPS. axisLabel : str Label for particular centroid coordinate. """ self.arraySize = arraySize self.timeScale = timeScale self.plot.setLabel('bottom', '{} {}'.format(axisLabel, HTML_NU), units='Hz') self.plot.setLabel('left', 'Time', units='s') def updatePlot(self, psd, freqs): """Update the current plot with the given data. Parameters ---------- psd : numpy.array The PSD data of a given centroid coordinate. freqs : numpy.array The frequency array associated with the PSD data. """ if self.data is None: self.data = np.zeros((self.arraySize, psd.size)) else: self.data[1:, ...] = self.data[:-1, ...] self.data[0, ...] = np.log(psd) self.image.setImage(self.data) if self.boundingRect is None: self.boundingRect = QtCore.QRectF(0, 0, freqs[-1], self.arraySize * self.timeScale) self.image.setRect(self.boundingRect)
def setImage(self, image=None, **kwds): if image is not None and self.__transpose is True: image = np.swapaxes(image, 0, 1) return ImageItem.setImage(self, image, **kwds)
class MIMiniImageView(GraphicsLayout, BadDataOverlay): def __init__(self, name: str = "MIMiniImageView"): super().__init__() self.name = name.title() self.im = ImageItem() self.vb = ViewBox(invertY=True, lockAspect=True, name=name) self.vb.addItem(self.im) self.hist = HistogramLUTItem(self.im) graveyard.append(self.vb) # Sub-layout prevents resizing issues when details text changes image_layout = self.addLayout(colspan=2) image_layout.addItem(self.vb) image_layout.addItem(self.hist) self.hist.setFixedWidth(100) # HistogramLUTItem used pixel sizes self.nextRow() self.details = self.addLabel("", colspan=2) self.im.hoverEvent = lambda ev: self.mouse_over(ev) self.axis_siblings: "WeakSet[MIMiniImageView]" = WeakSet() self.histogram_siblings: "WeakSet[MIMiniImageView]" = WeakSet() @property def image_item(self) -> ImageItem: return self.im @property def viewbox(self) -> ViewBox: return self.vb def clear(self): self.im.clear() def setImage(self, *args, **kwargs): self.im.setImage(*args, **kwargs) self.check_for_bad_data() @staticmethod def set_siblings(sibling_views: List["MIMiniImageView"], axis=False, hist=False): for view1 in sibling_views: for view2 in sibling_views: if view2 is not view1: if axis: view1.add_axis_sibling(view2) if hist: view1.add_hist_sibling(view2) def add_axis_sibling(self, sibling: "MIMiniImageView"): self.axis_siblings.add(sibling) def add_hist_sibling(self, sibling: "MIMiniImageView"): self.histogram_siblings.add(sibling) def get_parts(self) -> Tuple[ImageItem, ViewBox, HistogramLUTItem]: return self.im, self.vb, self.hist def mouse_over(self, ev): # Ignore events triggered by leaving window or right clicking if ev.exit: return pos = CloseEnoughPoint(ev.pos()) self.show_value(pos) for img_view in self.axis_siblings: img_view.show_value(pos) def show_value(self, pos): image = self.im.image if image is not None and pos.y < image.shape[ 0] and pos.x < image.shape[1]: pixel_value = image[pos.y, pos.x] value_string = ("%.6f" % pixel_value)[:8] self.details.setText(f"{self.name}: {value_string}") def link_sibling_axis(self): # Linking multiple viewboxes with locked aspect ratios causes # odd resizing behaviour. Use workaround from # https://github.com/pyqtgraph/pyqtgraph/issues/1348 self.vb.setAspectLocked(True) for view1, view2 in pairwise(chain([self], self.axis_siblings)): view2.vb.linkView(ViewBox.XAxis, view1.vb) view2.vb.linkView(ViewBox.YAxis, view1.vb) view2.vb.setAspectLocked(False) def unlink_sibling_axis(self): for img_view in chain([self], self.axis_siblings): img_view.vb.linkView(ViewBox.XAxis, None) img_view.vb.linkView(ViewBox.YAxis, None) img_view.vb.setAspectLocked(True) def link_sibling_histogram(self): for view1, view2 in pairwise(chain([self], self.histogram_siblings)): view1.hist.vb.linkView(ViewBox.YAxis, view2.hist.vb) for img_view in chain([self], self.histogram_siblings): img_view.hist.sigLevelChangeFinished.connect( img_view.update_sibling_histograms) def unlink_sibling_histogram(self): for img_view in chain([self], self.histogram_siblings): img_view.hist.vb.linkView(ViewBox.YAxis, None) try: img_view.hist.sigLevelChangeFinished.disconnect() except TypeError: # This is expected if there are slots currently connected pass def update_sibling_histograms(self): hist_range = self.hist.getLevels() for img_view in self.histogram_siblings: with BlockQtSignals(img_view.hist): img_view.hist.setLevels(*hist_range)
class SliceableGraphicsView(GraphicsView, SlicingView): sigToggleHorizontalSlice = Signal(bool) sigToggleVerticalSlice = Signal(bool) sigToggleDepthSlice = Signal(bool) sigMakePrimary = Signal(object, object) sigCrosshairMoved = Signal() SUPPORTED_NDIM = 2 def __init__(self, slice_direction, parent=None, xlink=None, ylink=None): super(SliceableGraphicsView, self).__init__(parent=parent) self.slice_direction = slice_direction self.setContentsMargins(0, 0, 0, 0) # Add axes self.view = SliceableAxes(slice_direction) self.view.axes["left"]["item"].setZValue(10) self.view.axes["top"]["item"].setZValue(10) self.setCentralItem(self.view) for sig in [ 'sigToggleVerticalSlice', 'sigToggleHorizontalSlice', 'sigToggleDepthSlice', 'sigMakePrimary' ]: if hasattr(self.view, sig): getattr(self.view, sig).connect(getattr(self, sig)) # Add imageitem self.image_item = ImageItem(axisOrder='row-major') self.image_item.setOpts() self.view.addItem(self.image_item) # add crosshair self.crosshair = BetterCrosshairROI((0, 0), parent=self.view, resizable=False) self.crosshair.sigMoved.connect(self.sigCrosshairMoved) self.view.getViewBox().addItem(self.crosshair) # find top-level parent NDImageView while not isinstance(parent, NDImageView): parent = parent.parent() # Initialize lut, levels self.image_item.setLevels(parent.levels, update=True) self.image_item.setLookupTable(parent.lut, update=True) # Link axes if ylink: self.view.vb.setYLink(ylink) if xlink: self.view.vb.setXLink(xlink) def setData(self, data): # Constrain squareness when units match is_square = data.dims[-2].split('(')[-1] == data.dims[-1].split( '(')[-1] self.view.vb.setAspectLocked(is_square) xvals = data.coords[data.dims[-1]] yvals = data.coords[data.dims[-2]] xmin = float(xvals.min()) xmax = float(xvals.max()) ymin = float(yvals.min()) ymax = float(yvals.max()) # Position the image according to coords shape = data.shape a = [(0, shape[-2]), (shape[-1], shape[-2]), (shape[-1], 0), (0, 0)] # b = [(ymin, xmax), (ymax, xmax), (ymax, xmin), (ymin, xmin)] if self.slice_direction in ['horizontal', 'depth']: b = [(xmin, ymin), (xmax, ymin), (xmax, ymax), (xmin, ymax)] elif self.slice_direction == 'vertical': b = [(xmax, ymax), (xmin, ymax), (xmin, ymin), (xmax, ymin)] quad1 = QPolygonF() quad2 = QPolygonF() for p, q in zip(a, b): quad1.append(QPointF(*p)) quad2.append(QPointF(*q)) transform = QTransform() QTransform.quadToQuad(quad1, quad2, transform) # Bind coords from the xarray to the timeline axis # super(SliceableGraphicsView, self).setImage(img, autoRange, autoLevels, levels, axes, np.asarray(img.coords[img.dims[0]]), pos, scale, transform, autoHistogramRange, levelMode) self.image_item.setImage(np.asarray(data), autoLevels=False) self.image_item.setTransform(transform) # Label the image axes self.view.setLabel('left', data.dims[-2]) self.view.setLabel('bottom', data.dims[-1]) def resetCrosshair(self): transform = self.image_item.viewTransform() new_pos = transform.map(self.image_item.boundingRect().center()) self.crosshair.setPos(new_pos) # self.crosshair.sigMoved.emit(new_pos) def updateImage(self, autoHistogramRange=True): ## Redraw image on screen if self.image is None: return image = self.getProcessedImage() if autoHistogramRange: self.ui.histogram.setHistogramRange(self.levelMin, self.levelMax) # Transpose image into order expected by ImageItem if self.imageItem.axisOrder == 'col-major': axorder = ['t', 'x', 'y', 'c'] else: axorder = ['t', 'y', 'x', 'c'] axorder = [ self.axes[ax] for ax in axorder if self.axes[ax] is not None ] ax_swap = [image.dims[ax_index] for ax_index in axorder] image = image.transpose(*ax_swap) # Select time index if self.axes['t'] is not None: self.ui.roiPlot.show() image = image[self.currentIndex] self.imageItem.updateImage(np.asarray(image)) def updateCrosshair(self, x, y): self.crosshair.setPos(x, y) def quickMinMax(self, data): """ Estimate the min/max values of *data* by subsampling. MODIFIED TO USE THE 99TH PERCENTILE instead of max. """ if data is None: return 0, 0 sl = slice(None, None, max(1, int(data.size // 1e6))) data = np.asarray(data[sl]) levels = (np.nanmin(data), np.nanpercentile( np.where(data < np.nanmax(data), data, np.nanmin(data)), 99)) return [levels]
class BlendView(GraphicsView): def __init__(self, parent: QWidget): GraphicsView.__init__(self, parent) layout = GraphicsLayout() self.setCentralItem(layout) self._image_item = ImageItem() self.viewbox = ViewBox(layout, lockAspect=True, invertY=True) self.viewbox.addItem(self._image_item) layout.addItem(self.viewbox) self.scale = ScaleBar(size=10, suffix='μm') self.scale.setParentItem(self.viewbox) self.scale.anchor((1, 1), (1, 1), offset=(-20, -20)) self.scale.hide() self._show_mask = False self.blend_mode = QPainter.CompositionMode_Screen self.items: List[ChannelImageItem] = None def clear(self): self.viewbox.clear() self.items = None def set_images(self, items: List[ChannelImageItem]): self.items = items if items is None or len(items) == 0: return images = [] for item in items: image = scale_image(item.image, item.channel.settings.max, item.channel.settings.levels) images.append(array2qimage(image, normalize=False)) self._draw_images(images) def _draw_images(self, images: List[QImage]): result = QImage(images[0].size(), QImage.Format_ARGB32_Premultiplied) painter = QPainter(result) painter.setCompositionMode(QPainter.CompositionMode_Source) painter.fillRect(images[0].rect(), Qt.transparent) painter.setCompositionMode(self.blend_mode) for i in images: painter.drawImage(0, 0, i) painter.end() self._image_item.setImage(rgb_view(result)) def refresh_images(self): self.set_images(self.items) def set_blend_mode(self, modename: str): self.blend_mode = getattr(QPainter, 'CompositionMode_' + modename) self.set_images(self.items) def show_scale_bar(self, state: bool): if state: self.scale.show() else: self.scale.hide() def progress_fn(self, n): print("%d%% done" % n) def process_result(self, result): print("PROCESS RESULTS!") if self._show_mask: self._image_item.setImage(result) def thread_complete(self): print("THREAD COMPLETE!") def show_mask(self, state: bool): self._show_mask = state if not state: self.refresh_images() return if Manager.data.selected_mask is None or Manager.data.view_mode is ViewMode.GREYSCALE: return mask = Manager.data.selected_mask.image blend_image = self._image_item.image # Pass the function to execute worker = Worker(apply_mask, image=blend_image, mask=mask) # Any other args, kwargs are passed to the run function worker.signals.result.connect(self.process_result) worker.signals.finished.connect(self.thread_complete) worker.signals.progress.connect(self.progress_fn) # Execute Manager.threadpool.start(worker)
class ESR_plot(QtGui.QWidget): def __init__(self, parent=None): QtGui.QWidget.__init__(self, parent) self.initGUI() def initGUI(self): self.plot = PlotWidget() self.sensorplot = PlotWidget() self.schemeplot = PlotWidget() self.scheme_plot = UTILS_QT.myplot(self.schemeplot,xlabel = ['time', 's'], ylabel =['',''],logmode=False) self.psp = UTILS_QT.pulses_scheme_plot(self.scheme_plot) date_axis = TimeAxisItem(orientation='bottom') #date_axis = pg.graphicsItems.DateAxisItem.DateAxisItem(orientation = 'bottom') self.sensorplot = PlotWidget(axisItems = {'bottom': date_axis}) win = GraphicsLayoutWidget() win2 = PlotWidget() self.view = win.addViewBox(border = 'w', invertY = True) self.view.setAspectLocked(True) self.img = ImageItem() self.plotaxes = win2.getPlotItem() #self.view.addItem(self.img) #self.view.addItem(self.plotaxes) #self.view. data = np.random.normal(size=(1, 600, 600), loc=1024, scale=64).astype(np.uint16) self.img.setImage(data[0]) self.plotaxes.getViewBox().addItem(self.img) # colormap pos = np.array([0., 1., 0.5, 0.25, 0.75]) #pos2 = np.array([1.0,0.75,0.5,0.25,0.]) pos2 = np.array([0.,0.25,0.5,0.75,1.0]) color2 = np.array([[255,242,15,255], [245,124,15,255],[170,69,16,255],[91,50,0,255],[0,0,0,255]],dtype=np.ubyte) color = np.array([[0,255,255,255], [255,255,0,255], [0,0,0,255], (0, 0, 255, 255), (255, 0, 0, 255)], dtype=np.ubyte) cmap = pg.ColorMap(pos2, color2) lut = cmap.getLookupTable(0.0, 1.0, 256) self.img.setLookupTable(lut) #self.img.setLevels([-50,1]) self.tw = QtGui.QTabWidget() self.tw.addTab(win2,'ESR data') self.tw.addTab(self.sensorplot,'B field') self.tw.addTab(self.schemeplot,'Scheme') layout = QtGui.QVBoxLayout() layout.addWidget(self.plot) #layout.addWidget(win2) layout.addWidget(self.tw) self.setLayout(layout) self.p1 = self.plot.getPlotItem() self.p2 = self.plot.getPlotItem() self.ps = self.sensorplot.getPlotItem() #self.p1.addLegend() self.p1data = self.p1.plot([0],pen = 'r') self.p2data = self.p1.plot([0],pen = 'g') self.psdata = self.ps.plot([],pen = 'w') self.ps.setLabel('left','Magnetic field', 'uT') self.vLine5 = pg.InfiniteLine(angle=90, movable=True) self.vLine6 = pg.InfiniteLine(angle=90, movable=True) self.plotaxes.addItem(self.vLine5, ignoreBounds=True) self.plotaxes.addItem(self.vLine6, ignoreBounds=True)
class PreviewWidget(GraphicsLayoutWidget): def __init__(self): super(PreviewWidget, self).__init__() self.setMinimumHeight(250) self.setMinimumWidth(250) self.view = self.addViewBox(lockAspect=True, enableMenu=False) self.imageitem = ImageItem() self.textitem = TextItem(anchor=(0.5, 0)) self.textitem.setFont(QFont("Zero Threes")) self.imgdata = None self.imageitem.setOpts(axisOrder="row-major") self.view.addItem(self.imageitem) self.view.addItem(self.textitem) self.textitem.hide() self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) # def textItemBounds(axis, frac=1.0, orthoRange=None): # b = self.textitem.boundingRect() # sx, sy = self.view.viewPixelSize() # x, y = sx*b.width(), sy*b.height() # if axis == 0: return (-x/2, x/2) # if axis == 1: return (0, y) # # self.textitem.dataBounds = textItemBounds def sizeHint(self): return QSize(250, 250) @threads.method(threadkey="preview", showBusy=False) def preview(self, data): if isinstance(data, NonDBHeader): self.preview_header(data) else: self.preview_catalog(data) @staticmethod def guess_stream_field(catalog: BlueskyRun): # TODO: use some metadata (techniques?) for guidance about how to get a preview streams = bluesky_utils.streams_from_run(catalog) if "primary" in streams: streams.remove("primary") streams.insert(0, "primary") for stream in streams: descriptor = bluesky_utils.descriptors_from_stream( catalog, stream)[0] fields = bluesky_utils.fields_from_descriptor(descriptor) for field in fields: field_ndims = bluesky_utils.ndims_from_descriptor( descriptor, field) if field_ndims > 1: return stream, field def preview_catalog(self, catalog: BlueskyRun): threads.invoke_in_main_thread(self.setText, "LOADING...") try: stream, field = self.guess_stream_field(catalog) data = getattr(catalog, stream).to_dask()[field].squeeze() for i in range(len(data.shape) - 2): data = data[0] threads.invoke_in_main_thread(self.setImage, np.asarray(data.compute())) except Exception as ex: msg.logError(ex) threads.invoke_in_main_thread(self.imageitem.clear) threads.invoke_in_main_thread(self.setText, "UNKNOWN DATA FORMAT") def preview_header(self, header: NonDBHeader): try: data = header.meta_array()[0] threads.invoke_in_main_thread(self.setImage, data) except IndexError: threads.invoke_in_main_thread(self.imageitem.clear) threads.invoke_in_main_thread(self.setText, "UNKNOWN DATA FORMAT") def setImage(self, imgdata): self.imageitem.clear() self.textitem.hide() self.imgdata = imgdata self.imageitem.setImage(np.log(self.imgdata * (self.imgdata > 0) + (self.imgdata < 1)), autoLevels=True) self.imageitem.setTransform( QTransform(1, 0, 0, -1, 0, self.imgdata.shape[-2])) self.view.autoRange() def setText(self, text): self.textitem.setText(text) self.imageitem.clear() self.textitem.setVisible(True) self.view.autoRange()
class Plot2DWidget(PlotWidget): def __init__(self, model_wrapper, statusbar, parent=None): """ The Plot2DWidget is responsible for rendering the 2D chromatogram data. :param model_wrapper: the wrapper of the model. :param parent: the parent of this Widget. """ super().__init__(parent=parent) self.listener = Plot2DListener(self, model_wrapper, statusbar) """ The listener for the 2D plot """ self.img = ImageItem() """ The image of the chromatogram""" self.wrapper_temp = model_wrapper # TEMPORARY TODO What is this for? """A temporary reference to the wrapper?""" # Add the image to the plot. self.addItem(self.img) # Disable right click context menu. self.getPlotItem().setMenuEnabled(False) self.getPlotItem().getAxis('bottom').enableAutoSIPrefix(False) self.getPlotItem().getAxis('left').enableAutoSIPrefix(False) model_wrapper.add_observer(self, self.notify) # call notify to draw the model. NOTE: The if statement isn't nesessary, it checks in notify if there is # a model or not. self.notify('model', model_wrapper.model) def refresh_x_period(self, x_period): if x_period == 0: self.getPlotItem().getAxis('bottom').setScale(1) else: self.getPlotItem().getAxis('bottom').setScale( x_period / self.wrapper_temp.model.get_width()) def refresh_y_period(self, y_period): if y_period == 0: self.getPlotItem().getAxis('left').setScale(1) else: self.getPlotItem().getAxis('left').setScale( y_period / self.wrapper_temp.model.get_height()) def refresh_x_unit(self, x_unit): if x_unit is TimeUnit.NONE: self.getPlotItem().getAxis('bottom').setLabel(units="") else: self.getPlotItem().getAxis('bottom').setLabel( units=x_unit.name.lower()) def refresh_y_unit(self, y_unit): if y_unit is TimeUnit.NONE: self.getPlotItem().getAxis('left').setLabel(units="") else: self.getPlotItem().getAxis('left').setLabel( units=y_unit.name.lower()) def notify(self, name, value): """ Updates the image rendered to match the model. :return: None """ if name == 'newIntegration': self.addItem(value.selector.roi) value.selector.set_viewport(self.img) elif name == 'removeIntegration': self.removeItem(value.selector.roi) elif name in {'model', 'model.viewTransformed'}: if value is None or value.get_2d_chromatogram_data() is None: self.img.clear() else: self.img.setImage(value.get_2d_chromatogram_data().clip( value.lower_bound, value.upper_bound), lut=value.palette) self.refresh_x_period( self.wrapper_temp.get_preference(ScaleEnum.X_PERIOD)) self.refresh_y_period( self.wrapper_temp.get_preference(ScaleEnum.Y_PERIOD)) self.refresh_x_unit( self.wrapper_temp.get_preference(ScaleEnum.X_UNIT)) self.refresh_y_unit( self.wrapper_temp.get_preference(ScaleEnum.Y_UNIT)) elif name == 'model.palette': self.img.setLookupTable(value.palette) elif name == 'model.lower_bound' or name == 'model.upper_bound': self.img.setImage(value.get_2d_chromatogram_data().clip( value.lower_bound, value.upper_bound), lut=value.palette) elif name == ScaleEnum.X_UNIT.name: self.refresh_x_unit(value) elif name == ScaleEnum.Y_UNIT.name: self.refresh_y_unit(value) elif name == ScaleEnum.X_PERIOD.name: self.refresh_x_period(value) elif name == ScaleEnum.Y_PERIOD.name: self.refresh_y_period(value)
class ImageViewer(GraphicsView): """ImageViewer class for Paint4Brains. This class contains the implementation of a series of functions required for the GUI of the Paint4Brains project. Args: brain (class): BrainData class for Paint4Brains parent (class): Base or parent class """ def __init__(self, brain, parent=None): super(ImageViewer, self).__init__(parent=parent) # Inputting data self.brain = brain # Creating viewing box to see data self.view = ModViewBox() self.setCentralItem(self.view) # Making Images out of data self.over_img = ImageItem( self.brain.current_label_data_slice, autoDownSmaple=False, opacity=1, compositionMode=QtGui.QPainter.CompositionMode_Plus) self.mid_img = ImageItem( np.zeros(self.brain.current_other_labels_data_slice.shape), autoDownSmaple=False, opacity=0.7, compositionMode=QtGui.QPainter.CompositionMode_Plus) self.img = ImageItem( self.brain.current_data_slice, autoDownsample=False, compositionMode=QtGui.QPainter.CompositionMode_SourceOver, levels=(0., 1.)) # Colouring the labelled data lut = np.array([[0, 0, 0, 0], [250, 0, 0, 255]]) self.over_img.setLookupTable(lut) self.over_img.setLevels([0, 1]) # Maybe the visualization lecture was not that useless... self.colours = [[166, 206, 27], [31, 120, 180], [178, 223, 138], [51, 160, 44], [251, 154, 153], [227, 26, 28], [253, 191, 111], [255, 127, 0], [202, 178, 214], [106, 61, 154], [255, 255, 153], [177, 89, 40]] self.update_colormap() # Adding the images to the viewing box and setting it to drawing mode (if there is labeled data) self.view.addItem(self.img) self.view.addItem(self.mid_img) self.view.addItem(self.over_img) self.select_mode = False self.see_all_labels = False if self.brain.label_filename is not None: self.enable_drawing() self.update_colormap() self.refresh_image() self.dropbox = SelectLabel(self) self.bonus = BonusBrush() self.bonus.buttn.clicked.connect(self.new_brush) def refresh_image(self): """Image Refresher Sets the images displayed by the Image viewer to the current data slices. It will only show all the labels if the self.see_all_labels parameters is True. """ #Creating slice copy slice = self.brain.current_data_slice new_slice = np.clip( np.log2(1 + slice.astype(float)) * self.brain.intensity, 0, self.brain.scale) self.img.setImage(new_slice, levels=(0., 1.)) self.over_img.setImage(self.brain.current_label_data_slice, autoLevels=False) if self.see_all_labels: self.mid_img.setImage(self.brain.current_other_labels_data_slice, autoLevels=False) else: self.mid_img.setImage(np.zeros( self.brain.current_other_labels_data_slice.shape), autoLevels=False) def recenter(self): """Brain Recenter Recenter the brain into the middle of the image viewer. The implementation may seem weird, but this is a predefined action by PyQt5. """ self.view.menu.actions()[0].trigger() def update_colormap(self): """Label Colormap Update Updates the colormap to account for the current number of distinct labels. There are only 12 distinct colours (not including the "invisible" colour) """ num = len(self.brain.different_labels) + 1 self.mid_img.setLookupTable( np.array([[0, 0, 0]] + int(num / len(self.colours) + 1) * self.colours)[:num]) self.mid_img.setLevels([0, np.max(self.brain.different_labels)]) def enable_drawing(self): """Activates drawing mode The default pen is a voxel in size. """ self.over_img.setDrawKernel(dot, mask=dot, center=(0, 0), mode='add') self.view.drawing = True def disable_drawing(self): """Deactivates drawing mode It does this by deactivating the drawing kernel and setting the value of the drawing parameter in the modified view box to False. """ if self.view.drawing: self.over_img.drawKernel = None self.view.drawing = False self.view.state["mouseMode"] = 3 else: self.enable_drawing() def edit_button1(self): """Sets the drawing mode to DOT This is basically a square of one voxel in size with value one. For all the editing buttons the matrix used to edit is defined at the top of the file """ self.view.drawing = True self.over_img.setDrawKernel(dot, mask=dot, center=(0, 0), mode='add') def edit_button2(self): """Sets the drawing mode to RUBBER This is basically a square of one voxel in size with value one. For all the editing buttons the matrix used to edit is defined at the top of the file Removes the label from voxels. """ self.view.drawing = True self.over_img.setDrawKernel(rubber, mask=rubber, center=(0, 0), mode='add') def edit_button3(self): """Sets the drawing mode to BRUSH (or cross) This sets the paintbrush to a cross of 3x3 voxels in size. For all the editing buttons the matrix used to edit is defined at the top of the file """ self.view.drawing = True self.over_img.setDrawKernel(cross, mask=cross, center=(1, 1), mode='add') def bonus_brush(self): """Allows the user to design their own brush This opens a window in which the user can design a brush by using the pen and rubber. """ self.enable_drawing() self.bonus.setVisible(True) def new_brush(self): """Sets the drawing mode to the user designed BRUSH (or bonus_brush) This sets the paintbrush to a user defined brush. """ self.enable_drawing() cent = len(self.bonus.pen) // 2 self.over_img.setDrawKernel(self.bonus.pen, mask=self.bonus.pen, center=(cent, cent), mode='add') def select_label(self): """Select label of interest Allows the user to select the location of the label to be edited next. Will only have effect if there are multiple labels from which to select The bulk of the implementation for this method is in the modified mouseReleasEevent method """ if self.brain.multiple_labels: self.over_img.drawKernel = None self.select_mode = True def view_back_labels(self): """Toggle all/single label. Switch that determines whether all segmented areas are visible or just one. If see_all_labels was False, it makes all labels visible. If it was True it makes all labels except the one the user is currently editing invisible. """ self.see_all_labels = not self.see_all_labels self.refresh_image() def next_label(self): """Label forward scroll Brings the next label in the list to be edited Lets you iterate through all existing labels """ if self.brain.multiple_labels: new_index = np.where(self.brain.different_labels == self.brain.current_label)[0][0] + 1 if new_index < len(self.brain.different_labels): self.brain.current_label = self.brain.different_labels[ new_index] else: self.brain.current_label = self.brain.different_labels[1] self.refresh_image() self.dropbox.update_box() def previous_label(self): """Label backward scroll Brings the previous label in the list to be edited Lets you iterate through all existing labels """ if self.brain.multiple_labels: old_index = np.where( self.brain.different_labels == self.brain.current_label)[0][0] if old_index != 1: self.brain.current_label = self.brain.different_labels[ old_index - 1] else: self.brain.current_label = self.brain.different_labels[-1] self.refresh_image() self.dropbox.update_box() def undo_previous_edit(self): """Undo function This function reverts the previous user action and refreshes the image. """ current = self.brain.current_edit if current > 1: self.brain.label_data = self.brain.edit_history[current - 2][0] self.brain.other_labels_data = self.brain.edit_history[current - 2][1] self.brain.current_edit = self.brain.current_edit - 1 self.refresh_image() def redo_previous_edit(self): """Redo function This function re-does a previously reverted actions. """ current = self.brain.current_edit if current < len(self.brain.edit_history): self.brain.label_data = self.brain.edit_history[current][0] self.brain.other_labels_data = self.brain.edit_history[current][1] self.brain.current_edit = self.brain.current_edit + 1 self.refresh_image() def mouseReleaseEvent(self, ev): """Mouse event tracker This function keeps track of the actions performed by the mouse, while taking the selcted mode into account. If when select_mode is activated, the left button is released on a previously labeled area, then the pen is set to that label. Otherwise, everything should work as normal (the default) Now when you release the left button it assumes an edit has been made and stores it into the BrainData. Args: ev: signal emitted when user releases a mouse button. """ if self.select_mode: if ev.button() == Qt.LeftButton: pos = ev.pos() mouse_x = int(self.img.mapFromScene(pos).x()) mouse_y = int(self.img.mapFromScene(pos).y()) location = self.brain.position_as_voxel(mouse_x, mouse_y) within = 0 < location[0] < self.brain.shape[0] and 0 < location[1] < self.brain.shape[1] and 0 < \ location[2] < self.brain.shape[2] if within: label = self.brain.other_labels_data[location] if label > 0: self.brain.current_label = self.brain.other_labels_data[ location] self.select_mode = False self.refresh_image() self.enable_drawing() self.dropbox.update_box() super(ImageViewer, self).mouseReleaseEvent(ev) if self.view.drawing and ev.button() == Qt.LeftButton: self.brain.store_edit() def wheelEvent(self, ev): """ Overwriting the wheel functionality. If you scroll it will move along slices. If you scroll while holding the Ctrl button, it will zoom in and out Args: ev: signal emitted when user releases scrolls the wheel. """ if ev.modifiers() == Qt.ControlModifier: super(ImageViewer, self).wheelEvent(ev) else: if ev.angleDelta().y( ) > 0 and self.brain.i < self.brain.shape[self.brain.section] - 1: self.brain.i = self.brain.i + 1 self.refresh_image() elif ev.angleDelta().y() < 0 < self.brain.i: self.brain.i = self.brain.i - 1 self.refresh_image()
class astraPlotWidget(QWidget): twissplotLayout = [ { 'name': 'sigma_x', 'range': [0, 1], 'scale': 1e3 }, { 'name': 'sigma_y', 'range': [0, 1], 'scale': 1e3 }, { 'name': 'kinetic_energy', 'range': [0, 250], 'scale': 1e-6 }, 'next_row', { 'name': 'sigma_p', 'range': [0, 0.015], 'scale': 1e6 }, { 'name': 'sigma_z', 'range': [0, 0.6], 'scale': 1e3 }, { 'name': 'enx', 'range': [0.5, 1.5], 'scale': 1e6 }, 'next_row', { 'name': 'eny', 'range': [0.5, 1.5], 'scale': 1e6 }, { 'name': 'beta_x', 'range': [0, 150], 'scale': 1 }, { 'name': 'beta_y', 'range': [0, 150], 'scale': 1 }, ] def __init__(self, directory='.', **kwargs): super(astraPlotWidget, self).__init__(**kwargs) self.beam = raf.beam() self.twiss = rtf.twiss() self.directory = directory ''' twissPlotWidget ''' self.twissPlotView = GraphicsView(useOpenGL=True) self.twissPlotWidget = GraphicsLayout() self.twissPlotView.setCentralItem(self.twissPlotWidget) self.latticePlotData = imageio.imread('lattice_plot.png') self.latticePlots = {} self.twissPlots = {} i = -1 for entry in self.twissplotLayout: if entry == 'next_row': self.twissPlotWidget.nextRow() else: i += 1 p = self.twissPlotWidget.addPlot(title=entry['name']) p.showGrid(x=True, y=True) vb = p.vb vb.setYRange(*entry['range']) latticePlot = ImageItem(self.latticePlotData) latticePlot.setOpts(axisOrder='row-major') vb.addItem(latticePlot) latticePlot.setZValue(-1) # make sure this image is on top # latticePlot.setOpacity(0.5) self.twissPlots[entry['name']] = p.plot( pen=mkPen('b', width=3)) self.latticePlots[p.vb] = latticePlot p.vb.sigRangeChanged.connect(self.scaleLattice) ''' beamPlotWidget ''' self.beamPlotWidget = QWidget() self.beamPlotLayout = QVBoxLayout() self.item = ImageItem() self.beamPlotWidget.setLayout(self.beamPlotLayout) self.beamPlotView = ImageView(imageItem=self.item) self.rainbow = rainbow() self.item.setLookupTable(self.rainbow) self.item.setLevels([0, 1]) # self.beamPlotWidgetGraphicsLayout = GraphicsLayout() # p = self.beamPlotWidgetGraphicsLayout.addPlot(title='beam') # p.showGrid(x=True, y=True) # self.beamPlot = p.plot(pen=None, symbol='+') # self.beamPlotView.setCentralItem(self.beamPlotWidgetGraphicsLayout) self.beamPlotXAxisCombo = QComboBox() self.beamPlotXAxisCombo.addItems( ['x', 'y', 'zn', 'cpx', 'cpy', 'BetaGamma']) self.beamPlotYAxisCombo = QComboBox() self.beamPlotYAxisCombo.addItems( ['x', 'y', 'zn', 'cpx', 'cpy', 'BetaGamma']) self.beamPlotNumberBins = QSpinBox() self.beamPlotNumberBins.setRange(10, 500) self.beamPlotNumberBins.setSingleStep(10) self.histogramBins = 100 self.beamPlotNumberBins.setValue(self.histogramBins) self.beamPlotAxisWidget = QWidget() self.beamPlotAxisLayout = QHBoxLayout() self.beamPlotAxisWidget.setLayout(self.beamPlotAxisLayout) self.beamPlotAxisLayout.addWidget(self.beamPlotXAxisCombo) self.beamPlotAxisLayout.addWidget(self.beamPlotYAxisCombo) self.beamPlotAxisLayout.addWidget(self.beamPlotNumberBins) self.beamPlotXAxisCombo.currentIndexChanged.connect(self.plotDataBeam) self.beamPlotYAxisCombo.currentIndexChanged.connect(self.plotDataBeam) self.beamPlotNumberBins.valueChanged.connect(self.plotDataBeam) # self.beamPlotXAxisCombo.setCurrentIndex(2) # self.beamPlotYAxisCombo.setCurrentIndex(5) self.beamPlotLayout.addWidget(self.beamPlotAxisWidget) self.beamPlotLayout.addWidget(self.beamPlotView) ''' slicePlotWidget ''' self.sliceParams = [ { 'name': 'slice_normalized_horizontal_emittance', 'units': 'm-rad', 'text': 'enx' }, { 'name': 'slice_normalized_vertical_emittance', 'units': 'm-rad', 'text': 'eny' }, { 'name': 'slice_peak_current', 'units': 'A', 'text': 'PeakI' }, { 'name': 'slice_relative_momentum_spread', 'units': '%', 'text': 'sigma-p' }, ] self.slicePlotWidget = QWidget() self.slicePlotLayout = QVBoxLayout() self.slicePlotWidget.setLayout(self.slicePlotLayout) # self.slicePlotView = GraphicsView(useOpenGL=True) self.slicePlotWidgetGraphicsLayout = GraphicsLayoutWidget() # self.slicePlots = {} self.slicePlotCheckbox = {} self.curve = {} self.sliceaxis = {} self.slicePlotCheckboxWidget = QWidget() self.slicePlotCheckboxLayout = QVBoxLayout() self.slicePlotCheckboxWidget.setLayout(self.slicePlotCheckboxLayout) self.slicePlot = self.slicePlotWidgetGraphicsLayout.addPlot( title='Slice', row=0, col=50) self.slicePlot.showAxis('left', False) self.slicePlot.showGrid(x=True, y=True) i = -1 colors = ['b', 'r', 'g', 'k'] for param in self.sliceParams: i += 1 axis = AxisItem("left") labelStyle = {'color': '#' + colorStr(mkColor(colors[i]))[0:-2]} axis.setLabel(text=param['text'], units=param['units'], **labelStyle) viewbox = ViewBox() axis.linkToView(viewbox) viewbox.setXLink(self.slicePlot.vb) self.sliceaxis[param['name']] = [axis, viewbox] self.curve[param['name']] = PlotDataItem(pen=colors[i], symbol='+') viewbox.addItem(self.curve[param['name']]) col = self.findFirstEmptyColumnInGraphicsLayout() self.slicePlotWidgetGraphicsLayout.ci.addItem(axis, row=0, col=col, rowspan=1, colspan=1) self.slicePlotWidgetGraphicsLayout.ci.addItem(viewbox, row=0, col=50) p.showGrid(x=True, y=True) # self.slicePlots[param] = self.slicePlot.plot(pen=colors[i], symbol='+') self.slicePlotCheckbox[param['name']] = QCheckBox(param['text']) self.slicePlotCheckboxLayout.addWidget( self.slicePlotCheckbox[param['name']]) self.slicePlotCheckbox[param['name']].stateChanged.connect( self.plotDataSlice) # self.slicePlotView.setCentralItem(self.slicePlotWidgetGraphicsLayout) self.slicePlotSliceWidthWidget = QSpinBox() self.slicePlotSliceWidthWidget.setMaximum(1000) self.slicePlotSliceWidthWidget.setValue(100) self.slicePlotSliceWidthWidget.setSingleStep(10) self.slicePlotSliceWidthWidget.setSuffix("fs") self.slicePlotSliceWidthWidget.setSpecialValueText('Automatic') self.slicePlotAxisWidget = QWidget() self.slicePlotAxisLayout = QHBoxLayout() self.slicePlotAxisWidget.setLayout(self.slicePlotAxisLayout) self.slicePlotAxisLayout.addWidget(self.slicePlotCheckboxWidget) self.slicePlotAxisLayout.addWidget(self.slicePlotSliceWidthWidget) # self.slicePlotXAxisCombo.currentIndexChanged.connect(self.plotDataSlice) self.slicePlotSliceWidthWidget.valueChanged.connect( self.changeSliceLength) # self.beamPlotXAxisCombo.setCurrentIndex(2) # self.beamPlotYAxisCombo.setCurrentIndex(5) self.slicePlotLayout.addWidget(self.slicePlotAxisWidget) self.slicePlotLayout.addWidget(self.slicePlotWidgetGraphicsLayout) self.layout = QVBoxLayout() self.setLayout(self.layout) self.tabWidget = QTabWidget() self.folderButton = QPushButton('Select Directory') self.folderLineEdit = QLineEdit() self.folderLineEdit.setReadOnly(True) self.folderLineEdit.setText(self.directory) self.reloadButton = QPushButton() self.reloadButton.setIcon(qApp.style().standardIcon( QStyle.SP_BrowserReload)) self.folderWidget = QGroupBox() self.folderLayout = QHBoxLayout() self.folderLayout.addWidget(self.folderButton) self.folderLayout.addWidget(self.folderLineEdit) self.folderLayout.addWidget(self.reloadButton) self.folderWidget.setLayout(self.folderLayout) self.folderWidget.setMaximumWidth(800) self.reloadButton.clicked.connect( lambda: self.changeDirectory(self.directory)) self.folderButton.clicked.connect(self.changeDirectory) self.fileSelector = QComboBox() self.fileSelector.currentIndexChanged.connect(self.updateScreenCombo) self.screenSelector = QComboBox() self.screenSelector.currentIndexChanged.connect(self.changeScreen) self.beamWidget = QGroupBox() self.beamLayout = QHBoxLayout() self.beamLayout.addWidget(self.fileSelector) self.beamLayout.addWidget(self.screenSelector) self.beamWidget.setLayout(self.beamLayout) self.beamWidget.setMaximumWidth(800) self.beamWidget.setVisible(False) self.folderBeamWidget = QWidget() self.folderBeamLayout = QHBoxLayout() self.folderBeamLayout.setAlignment(Qt.AlignLeft) self.folderBeamWidget.setLayout(self.folderBeamLayout) self.folderBeamLayout.addWidget(self.folderWidget) self.folderBeamLayout.addWidget(self.beamWidget) self.tabWidget.addTab(self.twissPlotView, 'Twiss Plots') self.tabWidget.addTab(self.beamPlotWidget, 'Beam Plots') self.tabWidget.addTab(self.slicePlotWidget, 'Slice Beam Plots') self.tabWidget.currentChanged.connect(self.changeTab) self.layout.addWidget(self.folderBeamWidget) self.layout.addWidget(self.tabWidget) self.plotType = 'Twiss' self.changeDirectory(self.directory) def findFirstEmptyColumnInGraphicsLayout(self): rowsfilled = self.slicePlotWidgetGraphicsLayout.ci.rows.get(0, {}).keys() for i in range(49): if not i in rowsfilled: return i def changeTab(self, i): if self.tabWidget.tabText(i) == 'Beam Plots': self.plotType = 'Beam' self.beamWidget.setVisible(True) elif self.tabWidget.tabText(i) == 'Slice Beam Plots': self.plotType = 'Slice' self.beamWidget.setVisible(True) else: self.plotType = 'Twiss' self.beamWidget.setVisible(False) self.loadDataFile() def changeDirectory(self, directory=None): if directory == None or directory == False: self.directory = str( QFileDialog.getExistingDirectory(self, "Select Directory", self.directory, QFileDialog.ShowDirsOnly)) else: self.directory = directory self.folderLineEdit.setText(self.directory) self.currentFileText = self.fileSelector.currentText() self.currentScreenText = self.screenSelector.currentText() self.getScreenFiles() self.updateFileCombo() self.updateScreenCombo() self.loadDataFile() def getScreenFiles(self): self.screenpositions = {} files = glob.glob(self.directory + '/*.????.???') filenames = [ '.'.join(os.path.basename(f).split('.')[:-2]) for f in files ] print 'filenames = ', filenames runnumber = [os.path.basename(f).split('.')[-1] for f in files] for f, r in list(set(zip(filenames, runnumber))): files = glob.glob(self.directory + '/' + f + '.????.???') screenpositions = [ re.search(f + '\.(\d\d\d\d)\.\d\d\d', s).group(1) for s in files ] print 'screenpositions = ', screenpositions self.screenpositions[f] = { 'screenpositions': sorted(screenpositions), 'run': r } def updateFileCombo(self): self.fileSelector.clear() i = -1 screenfirstpos = [] for f in self.screenpositions: screenfirstpos.append( [f, min(self.screenpositions[f]['screenpositions'])]) screenfirstpos = np.array(screenfirstpos) sortedscreennames = screenfirstpos[np.argsort( np.array(screenfirstpos)[:, 1])] print 'sortedscreennames = ', sortedscreennames for f in sortedscreennames: self.fileSelector.addItem(f[0]) i += 1 if f[0] == self.currentFileText: self.fileSelector.setCurrentIndex(i) def changeScreen(self, i): run = self.screenpositions[str(self.fileSelector.currentText())]['run'] self.beamFileName = str(self.fileSelector.currentText()) + '.' + str( self.screenSelector.currentText()) + '.' + str(run) # print 'beamFileName = ', self.beamFileName self.loadDataFile() def updateScreenCombo(self): self.screenSelector.clear() i = -1 for s in self.screenpositions[str( self.fileSelector.currentText())]['screenpositions']: self.screenSelector.addItem(s) i += 1 if s == self.currentScreenText: self.screenSelector.setCurrentIndex(i) def loadDataFile(self): if self.plotType == 'Twiss': files = sorted(glob.glob(self.directory + "/*Xemit*")) self.twiss.read_astra_emit_files(files) self.plotDataTwiss() elif self.plotType == 'Beam' or self.plotType == 'Slice': if hasattr( self, 'beamFileName') and os.path.isfile(self.directory + '/' + self.beamFileName): # starttime = time.time() self.beam.read_astra_beam_file(self.directory + '/' + self.beamFileName) # print 'reading file took ', time.time()-starttime, 's' # print 'Read file: ', self.beamFileName if self.plotType == 'Beam': self.plotDataBeam() else: self.beam.bin_time() self.plotDataSlice() def plotDataTwiss(self): for entry in self.twissplotLayout: if entry == 'next_row': pass else: x = self.twiss['z'] y = self.twiss[entry['name']] * entry['scale'] xy = np.transpose(np.array([x, y])) x, y = np.transpose(xy[np.argsort(xy[:, 0])]) self.twissPlots[entry['name']].setData(x=x, y=y, pen=mkPen('b', width=3)) def plotDataBeam(self): self.histogramBins = self.beamPlotNumberBins.value() x = getattr(self.beam, str(self.beamPlotXAxisCombo.currentText())) y = getattr(self.beam, str(self.beamPlotYAxisCombo.currentText())) h, xedges, yedges = np.histogram2d(x, y, self.histogramBins, normed=True) x0 = xedges[0] y0 = yedges[0] xscale = (xedges[-1] - xedges[0]) / len(xedges) yscale = (yedges[-1] - yedges[0]) / len(yedges) self.item.setImage(h) self.item.setLookupTable(self.rainbow) # self.item.setLevels([0,1]) def changeSliceLength(self): self.beam.slice_length = self.slicePlotSliceWidthWidget.value() * 1e-15 self.beam.bin_time() self.plotDataSlice() def plotDataSlice(self): for param in self.sliceParams: if self.slicePlotCheckbox[param['name']].isChecked(): x = self.beam.slice_bins self.slicePlot.setRange(xRange=[min(x), max(x)]) # self.plot.setRange(xRange=[-0.5,1.5]) y = getattr(self.beam, param['name']) self.curve[param['name']].setData(x=x, y=y) self.sliceaxis[param['name']][0].setVisible(True) # currentrange = self.sliceaxis[param['name']][0].range # print 'currentrange = ', currentrange # self.sliceaxis[param['name']][0].setRange(0, currentrange[1]) else: # pass self.curve[param['name']].setData(x=[], y=[]) self.sliceaxis[param['name']][0].setVisible(False) self.sliceaxis[param['name']][1].autoRange() currentrange = self.sliceaxis[param['name']][1].viewRange() self.sliceaxis[param['name']][1].setYRange(0, currentrange[1][1]) def scaleLattice(self, vb, range): yrange = range[1] scaleY = 0.05 * abs(yrange[1] - yrange[0]) rect = QRectF(0, yrange[0] + 2 * scaleY, 49.2778, 4 * scaleY) self.latticePlots[vb].setRect(rect)
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 FilterPreviews(GraphicsLayoutWidget): image_before: ImageItem image_after: ImageItem image_diff: ImageItem histogram: Optional[PlotItem] def __init__(self, parent=None, **kwargs): super().__init__(parent, **kwargs) widget_location = self.mapToGlobal(QPoint(self.width() // 2, 0)) # allow the widget to take up to 80% of the desktop's height if QGuiApplication.screenAt(widget_location) is not None: screen_height = QGuiApplication.screenAt( widget_location).availableGeometry().height() else: screen_height = max( QGuiApplication.primaryScreen().availableGeometry().height(), 600) LOG.info( "Unable to detect current screen. Setting screen height to %s" % screen_height) self.ALLOWED_HEIGHT: QRect = screen_height * 0.8 self.histogram = None self.addLabel("Image before") self.addLabel("Image after") self.addLabel("Image difference") self.nextRow() self.imageview_before = MIMiniImageView(name="before") self.imageview_after = MIMiniImageView(name="after") self.imageview_difference = MIMiniImageView(name="difference") self.all_imageviews = [ self.imageview_before, self.imageview_after, self.imageview_difference ] MIMiniImageView.set_siblings(self.all_imageviews, axis=True) MIMiniImageView.set_siblings( [self.imageview_before, self.imageview_after], hist=True) self.image_before, self.image_before_vb, self.image_before_hist = self.imageview_before.get_parts( ) self.image_after, self.image_after_vb, self.image_after_hist = self.imageview_after.get_parts( ) self.image_difference, self.image_difference_vb, self.image_difference_hist = \ self.imageview_difference.get_parts() self.all_histograms = [ self.image_before_hist, self.image_after_hist, self.image_difference_hist ] self.image_diff_overlay = ImageItem() self.image_diff_overlay.setZValue(10) self.image_after_vb.addItem(self.image_diff_overlay) # Ensure images resize equally self.image_layout: GraphicsLayout = self.addLayout(colspan=3) self.image_layout.addItem(self.imageview_before) self.image_layout.addItem(self.imageview_after) self.image_layout.addItem(self.imageview_difference) self.nextRow() self.init_histogram() # Work around for https://github.com/mantidproject/mantidimaging/issues/565 self.scene().contextMenu = [ item for item in self.scene().contextMenu if "export" not in item.text().lower() ] self.auto_colour_actions = [] self._add_auto_colour_action(self.image_before_hist, self.image_before) self._add_auto_colour_action(self.image_after_hist, self.image_after) self._add_auto_colour_action(self.image_difference_hist, self.image_difference) self.imageview_before.link_sibling_axis() self.imageview_before.enable_nan_check() self.imageview_after.enable_nan_check() def resizeEvent(self, ev: QResizeEvent): if ev is not None and isinstance(self.histogram, PlotItem): size = ev.size() self.histogram.setFixedHeight( min(size.height() * 0.7, self.ALLOWED_HEIGHT) * 0.25) super().resizeEvent(ev) def clear_items(self, clear_before: bool = True): if clear_before: self.imageview_before.clear() self.imageview_after.clear() self.imageview_difference.clear() self.image_diff_overlay.clear() def init_histogram(self): self.histogram = self.addPlot(row=histogram_coords.row, col=histogram_coords.col, labels=histogram_axes_labels, lockAspect=True, colspan=3) self.addLabel("Pixel values", row=label_coords.row, col=label_coords.col) self.legend = self.histogram.addLegend() self.legend.setOffset((0, 1)) def update_histogram_data(self): # Plot any histogram that has data, and add a legend if both exist before_data = self.imageview_before.image_item.getHistogram() after_data = self.imageview_after.image_item.getHistogram() if _data_valid_for_histogram(before_data): before_plot = self.histogram.plot(*before_data, pen=before_pen, clear=True) self.legend.addItem(before_plot, "Before") if _data_valid_for_histogram(after_data): after_plot = self.histogram.plot(*after_data, pen=after_pen) self.legend.addItem(after_plot, "After") @property def histogram_legend(self) -> Optional[LegendItem]: if self.histogram and self.histogram.legend: return self.histogram.legend return None def link_all_views(self): self.imageview_before.link_sibling_axis() def unlink_all_views(self): self.imageview_before.unlink_sibling_axis() def add_difference_overlay(self, diff, nan_change): diff = np.absolute(diff) diff[diff > OVERLAY_THRESHOLD] = 1.0 diff[nan_change] = 1.0 pos = np.array([0, 1]) color = np.array([[0, 0, 0, 0], OVERLAY_COLOUR_DIFFERENCE], dtype=np.ubyte) map = ColorMap(pos, color) self.image_diff_overlay.setVisible(True) self.image_diff_overlay.setImage(diff) lut = map.getLookupTable(0, 1, 2) self.image_diff_overlay.setLookupTable(lut) def add_negative_overlay(self): self.imageview_after.enable_nonpositive_check() def hide_difference_overlay(self): self.image_diff_overlay.setVisible(False) def hide_negative_overlay(self): self.imageview_after.enable_nonpositive_check(False) def auto_range(self): # This will cause the previews to all show by just causing autorange on self.image_before_vb self.image_before_vb.autoRange() def record_histogram_regions(self): self.before_region = self.image_before_hist.region.getRegion() self.diff_region = self.image_difference_hist.region.getRegion() self.after_region = self.image_after_hist.region.getRegion() def restore_histogram_regions(self): self.image_before_hist.region.setRegion(self.before_region) self.image_difference_hist.region.setRegion(self.diff_region) self.image_after_hist.region.setRegion(self.after_region) def link_before_after_histogram_scales(self, create_link: bool): """ Connects or disconnects the scales of the before/after histograms. :param create_link: Whether the link should be created or removed. """ if create_link: self.imageview_after.link_sibling_histogram() else: self.imageview_after.unlink_sibling_histogram() def set_histogram_log_scale(self): """ Sets the y-values of the before and after histogram plots to a log scale. """ set_histogram_log_scale(self.image_before_hist) set_histogram_log_scale(self.image_after_hist) def _add_auto_colour_action(self, histogram: HistogramLUTItem, image: ImageItem): """ Adds an "Auto" action to the histogram right-click menu. :param histogram: The HistogramLUTItem :param image: The ImageItem to have the Jenks/Otsu algorithm performed on it. """ self.auto_colour_actions.append(QAction("Auto")) self.auto_colour_actions[-1].triggered.connect( lambda: self._on_change_colour_palette(histogram, image)) action = histogram.gradient.menu.actions()[12] histogram.gradient.menu.insertAction(action, self.auto_colour_actions[-1]) histogram.gradient.menu.insertSeparator(self.auto_colour_actions[-1]) def _on_change_colour_palette(self, main_histogram: HistogramLUTItem, image: ImageItem): """ Creates a Palette Changer window when the "Auto" option has been selected. :param main_histogram: The HistogramLUTItem. :param image: The ImageItem. """ other_histograms = self.all_histograms[:] other_histograms.remove(main_histogram) change_colour_palette = PaletteChangerView(self, main_histogram, image.image, other_histograms) change_colour_palette.show()
class FilterPreviews(GraphicsLayoutWidget): image_before: ImageItem image_after: ImageItem image_diff: ImageItem histogram_before: Optional[PlotItem] histogram_after: Optional[PlotItem] histogram: Optional[PlotItem] def __init__(self, parent=None, **kwargs): super(FilterPreviews, self).__init__(parent, **kwargs) widget_location = self.mapToGlobal(QPoint(self.width() / 2, 0)) # allow the widget to take up to 80% of the desktop's height self.ALLOWED_HEIGHT: QRect = QGuiApplication.screenAt(widget_location).availableGeometry().height() * 0.8 self.before_histogram_data = None self.after_histogram_data = None self.histogram = None self.before_histogram = None self.after_histogram = None self.combined_histograms = True self.histogram_legend_visible = True self.addLabel("Image before") self.addLabel("Image after") self.addLabel("Image difference") self.nextRow() self.image_before, self.image_before_vb, self.image_before_hist = self.image_in_vb(name="before") self.image_after, self.image_after_vb, self.image_after_hist = self.image_in_vb(name="after") self.image_difference, self.image_difference_vb, self.image_difference_hist = self.image_in_vb( name="difference") self.image_after_overlay = ImageItem() self.image_after_overlay.setZValue(10) self.image_after_vb.addItem(self.image_after_overlay) # Ensure images resize equally self.image_layout: GraphicsLayout = self.addLayout(colspan=6) self.image_layout.addItem(self.image_before_vb, 0, 0) self.image_layout.addItem(self.image_before_hist, 0, 1) self.image_layout.addItem(self.image_after_vb, 0, 2) self.image_layout.addItem(self.image_after_hist, 0, 3) self.image_layout.addItem(self.image_difference_vb, 0, 4) self.image_layout.addItem(self.image_difference_hist, 0, 5) self.nextRow() before_details = self.addLabel("") after_details = self.addLabel("") difference_details = self.addLabel("") self.display_formatted_detail = { self.image_before: lambda val: before_details.setText(f"Before: {val:.6f}"), self.image_after: lambda val: after_details.setText(f"After: {val:.6f}"), self.image_difference: lambda val: difference_details.setText(f"Difference: {val:.6f}"), } for img in self.image_before, self.image_after, self.image_difference: img.hoverEvent = lambda ev: self.mouse_over(ev) self.init_histogram() def resizeEvent(self, ev: QResizeEvent): if ev is not None: size = ev.size() self.image_layout.setFixedHeight(min(size.height() * 0.7, self.ALLOWED_HEIGHT)) super().resizeEvent(ev) def image_in_vb(self, name=None): im = ImageItem() vb = ViewBox(invertY=True, lockAspect=True, name=name) vb.addItem(im) hist = HistogramLUTItem(im) return im, vb, hist def clear_items(self): self.image_before.clear() self.image_after.clear() self.image_difference.clear() self.image_after_overlay.clear() def init_histogram(self): self.histogram = self.addPlot(row=histogram_coords["combined"].row, col=histogram_coords["combined"].col, labels=histogram_axes_labels, lockAspect=True, colspan=3) self.addLabel("Pixel values", row=label_coords["combined"].row, col=label_coords["combined"].col) self.legend = self.histogram.addLegend() def update_histogram_data(self): # Plot any histogram that has data, and add a legend if both exist before_data = self.image_before.getHistogram() after_data = self.image_after.getHistogram() if _data_valid_for_histogram(before_data): if self.combined_histograms: before_plot = self.histogram.plot(*before_data, pen=before_pen, clear=True) self.legend.addItem(before_plot, "Before") else: self.before_histogram.plot(*before_data, pen=before_pen, clear=True) if _data_valid_for_histogram(after_data): if self.combined_histograms: after_plot = self.histogram.plot(*after_data, pen=after_pen) self.legend.addItem(after_plot, "After") else: self.after_histogram.plot(*after_data, pen=after_pen, clear=True) def init_separate_histograms(self): hc = histogram_coords self.before_histogram = self.addPlot(row=hc["before"].row, col=hc["before"].col, labels=histogram_axes_labels, lockAspect=True) self.after_histogram = self.addPlot(row=hc["after"].row, col=hc["after"].col, labels=histogram_axes_labels, lockAspect=True) lc = label_coords self.addLabel("Pixel values before", row=lc["before"].row, col=lc["before"].col) self.addLabel("Pixel values after", row=lc["after"].row, col=lc["after"].col) if _data_valid_for_histogram(self.before_histogram_data): self.before_histogram.plot(*self.before_histogram_data, pen=before_pen) if _data_valid_for_histogram(self.after_histogram_data): self.after_histogram.plot(*self.after_histogram_data, pen=after_pen) def delete_histograms(self): coords = set(c for c in histogram_coords.values()) histograms = (self.getItem(*coord) for coord in coords) for histogram in filter(lambda h: h is not None, histograms): self.removeItem(histogram) self.histogram = None self.before_histogram = None self.after_histogram = None def delete_histogram_labels(self): coords = set(c for c in label_coords.values()) labels = (self.getItem(*coord) for coord in coords) for label in filter(lambda h: h is not None, labels): self.removeItem(label) @property def histogram_legend(self) -> Optional[LegendItem]: if self.histogram and self.histogram.legend: return self.histogram.legend return None def mouse_over(self, ev): # Ignore events triggered by leaving window or right clicking if ev.exit: return pos = CloseEnoughPoint(ev.pos()) for img in self.image_before, self.image_after, self.image_difference: if img.image is not None and pos.x < img.image.shape[0] and pos.y < img.image.shape[1]: pixel_value = img.image[pos.y, pos.x] self.display_formatted_detail[img](pixel_value) def link_all_views(self): for view1, view2 in [[self.image_before_vb, self.image_after_vb], [self.image_after_vb, self.image_difference_vb], [self.image_after_hist.vb, self.image_before_hist.vb]]: view1.linkView(ViewBox.XAxis, view2) view1.linkView(ViewBox.YAxis, view2) def unlink_all_views(self): for view in self.image_before_vb, self.image_after_vb, self.image_after_hist.vb: view.linkView(ViewBox.XAxis, None) view.linkView(ViewBox.YAxis, None) def add_difference_overlay(self, diff): diff = -diff diff[diff > 0.0] = 1.0 pos = np.array([0, 1]) color = np.array([[0, 0, 0, 0], [255, 0, 0, 255]], dtype=np.ubyte) map = ColorMap(pos, color) self.image_after_overlay.setOpacity(1) self.image_after_overlay.setImage(diff) lut = map.getLookupTable(0, 1, 2) self.image_after_overlay.setLookupTable(lut) def hide_difference_overlay(self): self.image_after_overlay.setOpacity(0) def auto_range(self): # This will cause the previews to all show by just causing autorange on self.image_before_vb self.image_before_vb.autoRange()
class PreviewWidget(GraphicsLayoutWidget): def __init__(self): super(PreviewWidget, self).__init__() self.setMinimumHeight(250) self.setMinimumWidth(250) self.view = self.addViewBox(lockAspect=True, enableMenu=False) self.imageitem = ImageItem() self.textitem = TextItem(anchor=(0.5, 0)) self.textitem.setFont(QFont("Zero Threes")) self.imgdata = None self.imageitem.setOpts(axisOrder="row-major") self.view.addItem(self.imageitem) self.view.addItem(self.textitem) self.textitem.hide() self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) # def textItemBounds(axis, frac=1.0, orthoRange=None): # b = self.textitem.boundingRect() # sx, sy = self.view.viewPixelSize() # x, y = sx*b.width(), sy*b.height() # if axis == 0: return (-x/2, x/2) # if axis == 1: return (0, y) # # self.textitem.dataBounds = textItemBounds def sizeHint(self): return QSize(250, 250) def preview(self, data): if isinstance(data, NonDBHeader): self.preview_header(data) else: self.preview_catalog(data) def preview_catalog(self, catalog: BlueskyRun): try: dask_array = catalog.primary.to_dask() fields = dask_array.keys() # Filter out seq num and uid field = next(field for field in fields if not field in ["seq_num", "uid"]) data = dask_array[field] for i in range(len(data.shape) - 2): data = data[0] self.setImage(np.asarray(data.compute())) except IndexError: self.imageitem.clear() self.setText("UNKNOWN DATA FORMAT") def preview_header(self, header: NonDBHeader): try: data = header.meta_array()[0] self.setImage(data) except IndexError: self.imageitem.clear() self.setText("UNKNOWN DATA FORMAT") def setImage(self, imgdata): self.imageitem.clear() self.textitem.hide() self.imgdata = imgdata self.imageitem.setImage(np.log(self.imgdata * (self.imgdata > 0) + (self.imgdata < 1)), autoLevels=True) self.imageitem.setTransform(QTransform(1, 0, 0, -1, 0, self.imgdata.shape[-2])) self.view.autoRange() def setText(self, text): self.textitem.setText(text) self.imageitem.clear() self.textitem.setVisible(True) self.view.autoRange()
class ClusteringWidget(QSplitter): def __init__(self, headermodel, selectionmodel): super(ClusteringWidget, self).__init__() self.headermodel = headermodel self.selectionmodel = selectionmodel # init some values self.selectMapidx = 0 self.embedding = None self.labels = None self.mean_spectra = None # split between cluster image and scatter plot self.image_and_scatter = QSplitter() # split between image&scatter and spec plot, vertical split self.leftsplitter = QSplitter() self.leftsplitter.setOrientation(Qt.Vertical) # split between params, buttons and map list, vertical split self.rightsplitter = QSplitter() self.rightsplitter.setOrientation(Qt.Vertical) self.clusterImage = MapViewWidget() self.clusterScatterPlot = ScatterPlotWidget() self.rawSpecPlot = SpectraPlotWidget() self.clusterMeanPlot = ClusterSpectraWidget() # ParameterTree self.parametertree = ClusteringParameters() self.parameter = self.parametertree.parameter # buttons layout self.buttons = QWidget() self.buttonlayout = QGridLayout() self.buttons.setLayout(self.buttonlayout) # set up buttons self.fontSize = 12 font = QFont("Helvetica [Cronyx]", self.fontSize) self.computeBtn = QPushButton() self.computeBtn.setText('Compute clusters') self.computeBtn.setFont(font) self.saveBtn = QPushButton() self.saveBtn.setText('Save clusters') self.saveBtn.setFont(font) # add all buttons self.buttonlayout.addWidget(self.computeBtn) self.buttonlayout.addWidget(self.saveBtn) # Headers listview self.headerlistview = QListView() self.headerlistview.setModel(headermodel) self.headerlistview.setSelectionModel( selectionmodel) # This might do weird things in the map view? self.headerlistview.setSelectionMode(QListView.SingleSelection) # add title to list view self.mapListWidget = QWidget() self.listLayout = QVBoxLayout() self.mapListWidget.setLayout(self.listLayout) mapListTitle = QLabel('Maps list') mapListTitle.setFont(font) self.listLayout.addWidget(mapListTitle) self.listLayout.addWidget(self.headerlistview) # assemble widgets self.image_and_scatter.addWidget(self.clusterImage) self.image_and_scatter.addWidget(self.clusterScatterPlot) self.leftsplitter.addWidget(self.image_and_scatter) self.leftsplitter.addWidget(self.rawSpecPlot) self.leftsplitter.addWidget(self.clusterMeanPlot) self.leftsplitter.setSizes([200, 50, 50]) self.rightsplitter.addWidget(self.parametertree) self.rightsplitter.addWidget(self.buttons) self.rightsplitter.addWidget(self.mapListWidget) self.rightsplitter.setSizes([300, 50, 50]) self.addWidget(self.leftsplitter) self.addWidget(self.rightsplitter) self.setSizes([500, 100]) # setup ROI item sideLen = 10 self.roi = PolyLineROI(positions=[[0, 0], [sideLen, 0], [sideLen, sideLen], [0, sideLen]], closed=True) self.roi.hide() self.roiInitState = self.roi.getState() # set up mask item self.maskItem = ImageItem(np.ones((1, 1)), axisOrder="row-major", autoLevels=True, opacity=0.3) self.maskItem.hide() # set up select mask item self.selectMaskItem = ImageItem(np.ones((1, 1)), axisOrder="row-major", autoLevels=True, opacity=0.3, lut=np.array([[0, 0, 0], [255, 0, 0]])) self.selectMaskItem.hide() self.clusterImage.view.addItem(self.roi) self.clusterImage.view.addItem(self.maskItem) self.clusterImage.view.addItem(self.selectMaskItem) # Connect signals self.computeBtn.clicked.connect(self.computeEmbedding) self.saveBtn.clicked.connect(self.saveCluster) self.clusterImage.sigShowSpectra.connect(self.rawSpecPlot.showSpectra) self.clusterImage.sigShowSpectra.connect( self.clusterScatterPlot.clickFromImage) self.clusterScatterPlot.sigScatterRawInd.connect( self.rawSpecPlot.showSpectra) self.clusterScatterPlot.sigScatterClicked.connect(self.showClusterMean) self.clusterScatterPlot.sigScatterRawInd.connect(self.setImageCross) self.parametertree.sigParamChanged.connect(self.updateClusterParams) self.selectionmodel.selectionChanged.connect(self.updateMap) self.selectionmodel.selectionChanged.connect(self.updateRoiMask) def computeEmbedding(self): # get current map idx if not self.isMapOpen(): return msg.showMessage('Compute embedding.') # Select wavenumber region wavROIList = [] for entry in self.parameter['Wavenumber Range'].split(','): try: wavROIList.append(val2ind(int(entry), self.wavenumbers)) except: continue if len(wavROIList) % 2 == 0: wavROIList = sorted(wavROIList) wavROIidx = [] for i in range(len(wavROIList) // 2): wavROIidx += list( range(wavROIList[2 * i], wavROIList[2 * i + 1] + 1)) else: msg.logMessage('"Wavenumber Range" values must be in pairs', msg.ERROR) MsgBox('Clustering computation aborted.', 'error') return self.wavenumbers_select = self.wavenumbers[wavROIidx] self.N_w = len(self.wavenumbers_select) # get current dataset if self.selectedPixels is None: n_spectra = len(self.data) self.dataset = np.zeros((n_spectra, self.N_w)) for i in range(n_spectra): self.dataset[i, :] = self.data[i][wavROIidx] else: n_spectra = len(self.selectedPixels) self.dataset = np.zeros((n_spectra, self.N_w)) for i in range(n_spectra): # i: ith selected pixel row_col = tuple(self.selectedPixels[i]) self.dataset[i, :] = self.data[self.rc2ind[row_col]][wavROIidx] # get parameters and compute embedding n_components = self.parameter['Components'] if self.parameter['Embedding'] == 'UMAP': n_neighbors = self.parameter['Neighbors'] metric = self.parameter['Metric'] min_dist = np.clip(self.parameter['Min Dist'], 0, 1) self.umap = UMAP(n_neighbors=n_neighbors, min_dist=min_dist, n_components=n_components, metric=metric, random_state=0) self.embedding = self.umap.fit_transform(self.dataset) elif self.parameter['Embedding'] == 'PCA': # normalize and mean center if self.parameter['Normalization'] == 'L1': # normalize data_norm = Normalizer(norm='l1').fit_transform(self.dataset) elif self.parameter['Normalization'] == 'L2': data_norm = Normalizer(norm='l2').fit_transform(self.dataset) else: data_norm = self.dataset # subtract mean data_centered = StandardScaler( with_std=False).fit_transform(data_norm) # Do PCA self.PCA = PCA(n_components=n_components) self.PCA.fit(data_centered) self.embedding = self.PCA.transform(data_centered) # save embedding to standardModelItem self.item.embedding = self.embedding # update cluster map self.computeCluster() def computeCluster(self): # check if embeddings exist if self.embedding is None: return msg.showMessage('Compute clusters.') # get num of clusters n_clusters = self.parameter['Clusters'] # set colorLUT self.colorLUT = cm.get_cmap('viridis', n_clusters + 1).colors[:, :3] * 255 # compute cluster cluster_object = KMeans(n_clusters=n_clusters, random_state=0).fit(self.embedding) self.labels = cluster_object.labels_ + 1 # update cluster image if self.selectedPixels is None: # full map self.cluster_map = self.labels.reshape(self.imgShape[0], self.imgShape[1]) elif self.selectedPixels.size == 0: self.cluster_map = np.zeros((self.imgShape[0], self.imgShape[1]), dtype=int) else: self.cluster_map = np.zeros((self.imgShape[0], self.imgShape[1]), dtype=int) self.cluster_map[self.selectedPixels[:, 0], self.selectedPixels[:, 1]] = self.labels self.cluster_map = np.flipud(self.cluster_map) self.clusterImage.setImage(self.cluster_map, levels=[0, n_clusters]) # self.clusterImage.setImage(self.cluster_map) self.clusterImage._image = self.cluster_map self.clusterImage.rc2ind = self.rc2ind self.clusterImage.row, self.clusterImage.col = self.imgShape[ 0], self.imgShape[1] self.clusterImage.txt.setPos(self.clusterImage.col, 0) self.clusterImage.cross.show() # update cluster mean mean_spectra = [] self.dfGroups = [] if self.selectedPixels is None: n_spectra = len(self.data) self.dataList = np.zeros((n_spectra, len(self.wavenumbers))) dataIdx = np.arange(n_spectra) for i in range(n_spectra): self.dataList[i] = self.data[i] else: n_spectra = len(self.selectedPixels) self.dataList = np.zeros((n_spectra, len(self.wavenumbers))) dataIdx = np.zeros(n_spectra, dtype=int) for i in range(n_spectra): # i: ith selected pixel row_col = tuple(self.selectedPixels[i]) dataIdx[i] = self.rc2ind[row_col] self.dataList[i] = self.data[dataIdx[i]] for ii in range(1, n_clusters + 1): sel = (self.labels == ii) # save each group spectra to a dataFrame self.dfGroups.append( pd.DataFrame(self.dataList[sel], columns=self.wavenumbers.tolist(), index=dataIdx[sel])) this_mean = np.mean(self.dataset[sel, :], axis=0) mean_spectra.append(this_mean) self.mean_spectra = np.vstack(mean_spectra) self.clusterMeanPlot.setColors(self.colorLUT) self.clusterMeanPlot._data = self.mean_spectra self.clusterMeanPlot.wavenumbers = self.wavenumbers_select self.clusterMeanPlot.plotClusterSpectra() # update scatter plot self.updateScatterPlot() def saveCluster(self): if hasattr(self, 'cluster_map') and hasattr(self, 'mean_spectra'): filePath = self.pathList[self.selectMapidx] # get dirname and old filename dirName = os.path.dirname(filePath) oldFileName = os.path.basename(filePath) n_clusters = self.parameter['Clusters'] for i in range(n_clusters): # save dataFrames to csv file csvName = oldFileName[:-3] + f'_cluster{i+1}.csv' newFilePath = os.path.join(dirName, csvName) self.dfGroups[i].to_csv(newFilePath) MsgBox( f'Cluster spectra groups were successfully saved at: {newFilePath}!' ) def updateScatterPlot(self): if (self.embedding is None) or (self.labels is None): return # get scatter x, y values self.clusterScatterPlot.scatterData = self.embedding[:, [ self.parameter['X Component'] - 1, self.parameter['Y Component'] - 1 ]] # get colormapings brushes = [mkBrush(self.colorLUT[x, :]) for x in self.labels] # make plots if hasattr(self, 'scatterPlot'): self.clusterScatterPlot.plotItem.clearPlots() self.scatterPlot = self.clusterScatterPlot.plotItem.plot( self.clusterScatterPlot.scatterData, pen=None, symbol='o', symbolBrush=brushes) self.clusterScatterPlot.getViewBox().autoRange(padding=0.1) self.clusterScatterPlot.getNN() def updateClusterParams(self, name): if name == 'Components': self.computeEmbedding() elif name == 'Clusters': self.computeCluster() elif name in ['X Component', 'Y Component']: self.updateScatterPlot() def updateMap(self): # get current map idx if not self.selectionmodel.selectedIndexes(): # no map is open return else: self.selectMapidx = self.selectionmodel.selectedIndexes()[0].row() # get current item self.item = self.headermodel.item(self.selectMapidx) if hasattr(self.item, 'embedding'): # compute embedding self.computeEmbedding() else: # reset custer image and plots self.cleanUp() def showClusterMean(self, i): if self.mean_spectra is None: return self.clusterMeanPlot.curveHighLight(self.labels[i] - 1) def setImageCross(self, ind): row, col = self.ind2rc[ind] # update cross self.clusterImage.cross.setData([col + 0.5], [self.imgShape[0] - row - 0.5]) # update text self.clusterImage.txt.setHtml( toHtml(f'Point: #{ind}', size=8) + toHtml(f'X: {col}', size=8) + toHtml(f'Y: {row}', size=8) + toHtml( f'Val: {self.clusterImage._image[self.imgShape[0] - row - 1, col] :d}', size=8)) def cleanUp(self): if self.selectionmodel.hasSelection(): self.selectMapIdx = self.selectionmodel.selectedIndexes()[0].row() elif self.headermodel.rowCount() > 0: self.selectMapIdx = 0 else: return if hasattr(self, 'imgShapes') and (self.selectMapIdx < len(self.imgShapes)): # self.clusterImage.clear() img = np.zeros((self.imgShapes[self.selectMapIdx][0], self.imgShapes[self.selectMapIdx][1])) self.clusterImage.setImage(img=img) if hasattr(self, 'scatterPlot'): self.clusterScatterPlot.plotItem.clearPlots() self.clusterScatterPlot.scatterData = None self.rawSpecPlot.clearAll() self.rawSpecPlot._data = None self.clusterMeanPlot.clearAll() self.clusterMeanPlot._data = None def updateRoiMask(self): if self.selectionmodel.hasSelection(): self.selectMapIdx = self.selectionmodel.selectedIndexes()[0].row() elif self.headermodel.rowCount() > 0: self.selectMapIdx = 0 else: return # update roi try: roiState = self.headermodel.item(self.selectMapIdx).roiState if roiState[0]: # roi on self.roi.show() else: self.roi.hide() # update roi state self.roi.blockSignals(True) self.roi.setState(roiState[1]) self.roi.blockSignals(False) except Exception: self.roi.hide() # update automask try: maskState = self.headermodel.item(self.selectMapIdx).maskState self.maskItem.setImage(maskState[1]) if maskState[0]: # automask on self.maskItem.show() else: self.maskItem.hide() except Exception: pass # update selectMask try: selectMaskState = self.headermodel.item( self.selectMapIdx).selectState self.selectMaskItem.setImage(selectMaskState[1]) if selectMaskState[0]: # selectmask on self.selectMaskItem.show() else: self.selectMaskItem.hide() except Exception: pass def setHeader(self, field: str): self.headers = [ self.headermodel.item(i).header for i in range(self.headermodel.rowCount()) ] self.field = field self.wavenumberList = [] self.imgShapes = [] self.rc2indList = [] self.ind2rcList = [] self.pathList = [] self.dataSets = [] # get wavenumbers, imgShapes, rc2ind for header in self.headers: dataEvent = next(header.events(fields=[field])) self.wavenumberList.append(dataEvent['wavenumbers']) self.imgShapes.append(dataEvent['imgShape']) self.rc2indList.append(dataEvent['rc_index']) self.ind2rcList.append(dataEvent['index_rc']) self.pathList.append(dataEvent['path']) # get raw spectra data = None try: # spectra datasets data = header.meta_array('spectra') except IndexError: msg.logMessage( 'Header object contained no frames with field ' '{field}' '.', msg.ERROR) if data is not None: self.dataSets.append(data) self.cleanUp() def isMapOpen(self): if not self.selectionmodel.selectedIndexes(): # no map is open return False else: self.selectMapidx = self.selectionmodel.selectedIndexes()[0].row() # get current data self.item = self.headermodel.item(self.selectMapidx) self.selectedPixels = self.item.selectedPixels self.clusterScatterPlot.selectedPixels = self.selectedPixels self.currentHeader = self.headers[self.selectMapidx] self.wavenumbers = self.wavenumberList[self.selectMapidx] self.rc2ind = self.rc2indList[self.selectMapidx] self.ind2rc = self.ind2rcList[self.selectMapidx] self.clusterScatterPlot.ind2rc = self.ind2rc self.clusterScatterPlot.rc2ind = self.rc2ind self.imgShape = self.imgShapes[self.selectMapidx] self.data = self.dataSets[self.selectMapidx] self.rawSpecPlot.setHeader(self.currentHeader, 'spectra') if self.selectedPixels is not None: self.clusterScatterPlot.selPx_rc2ind = { tuple(self.selectedPixels[i]): i for i in range(len(self.selectedPixels)) } self.clusterScatterPlot.selPx_ind2rc = { i: tuple(self.selectedPixels[i]) for i in range(len(self.selectedPixels)) } return True
class PreviewWidget(GraphicsLayoutWidget): def __init__(self): super(PreviewWidget, self).__init__() self.setMinimumHeight(250) self.setMinimumWidth(250) self.view = self.addViewBox(lockAspect=True, enableMenu=False) self.imageitem = ImageItem() self.textitem = TextItem(anchor=(0.5, 0)) self.textitem.setFont(QFont("Zero Threes")) self.imageitem.setOpts(axisOrder="row-major") self.view.addItem(self.imageitem) self.view.addItem(self.textitem) self.textitem.hide() self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) # def textItemBounds(axis, frac=1.0, orthoRange=None): # b = self.textitem.boundingRect() # sx, sy = self.view.viewPixelSize() # x, y = sx*b.width(), sy*b.height() # if axis == 0: return (-x/2, x/2) # if axis == 1: return (0, y) # # self.textitem.dataBounds = textItemBounds def sizeHint(self): return QSize(250, 250) @threads.method(threadkey="preview", showBusy=False) def preview(self, data): if isinstance(data, NonDBHeader): self.preview_header(data) else: self.preview_catalog(data) def preview_catalog(self, catalog: BlueskyRun): threads.invoke_in_main_thread(self.setText, "LOADING...") try: stream, field = bluesky_utils.guess_stream_field(catalog) data = bluesky_utils.preview(catalog, stream, field) threads.invoke_in_main_thread(self.setImage, data) except Exception as ex: msg.logError(ex) threads.invoke_in_main_thread(self.imageitem.clear) threads.invoke_in_main_thread(self.setText, "UNKNOWN DATA FORMAT") def preview_header(self, header: NonDBHeader): try: data = header.meta_array()[0] threads.invoke_in_main_thread(self.setImage, data) except IndexError: threads.invoke_in_main_thread(self.imageitem.clear) threads.invoke_in_main_thread(self.setText, "UNKNOWN DATA FORMAT") def setImage(self, imgdata): self.imageitem.clear() self.textitem.hide() self.imageitem.setImage(np.log(imgdata * (imgdata > 0) + (imgdata < 1)), autoLevels=True) self.imageitem.setTransform( QTransform(1, 0, 0, -1, 0, imgdata.shape[-2])) self.view.autoRange() def setText(self, text): self.textitem.setText(text) self.imageitem.clear() self.textitem.setVisible(True) self.view.autoRange()
def _update_preview_image(image_data: Optional[np.ndarray], image: ImageItem): image.clear() image.setImage(image_data)
class FilterPreviews(GraphicsLayoutWidget): image_before: ImageItem image_after: ImageItem image_diff: ImageItem histogram_before: Optional[PlotItem] histogram_after: Optional[PlotItem] histogram: Optional[PlotItem] def __init__(self, parent=None, **kwargs): super(FilterPreviews, self).__init__(parent, **kwargs) self.before_histogram_data = None self.after_histogram_data = None self.histogram = None self.before_histogram = None self.after_histogram = None self.combined_histograms = True self.histogram_legend_visible = True self.addLabel("Image before") self.addLabel("Image after") self.addLabel("Image difference") self.nextRow() self.image_before, self.image_before_vb = self.image_in_vb( name="before") self.image_after, self.image_after_vb = self.image_in_vb(name="after") self.image_difference, self.image_difference_vb = self.image_in_vb( name="difference") self.image_after_overlay = ImageItem() self.image_after_overlay.setZValue(10) self.image_after_vb.addItem(self.image_after_overlay) # Ensure images resize equally image_layout = self.addLayout(colspan=3) image_layout.addItem(self.image_before_vb, 0, 0) image_layout.addItem(self.image_after_vb, 0, 1) image_layout.addItem(self.image_difference_vb, 0, 2) self.nextRow() before_details = self.addLabel("") after_details = self.addLabel("") difference_details = self.addLabel("") self.display_formatted_detail = { self.image_before: lambda val: before_details.setText(f"Before: {val:.6f}"), self.image_after: lambda val: after_details.setText(f"After: {val:.6f}"), self.image_difference: lambda val: difference_details.setText(f"Difference: {val:.6f}"), } for img in self.image_before, self.image_after, self.image_difference: img.hoverEvent = lambda ev: self.mouse_over(ev) def image_in_vb(self, name=None): im = ImageItem() im.setAutoDownsample(False) vb = ViewBox(invertY=True, lockAspect=True, name=name) vb.addItem(im) return im, vb def clear_items(self): self.image_before.clear() self.image_after.clear() self.image_difference.clear() self.image_after_overlay.clear() self.delete_histograms() # There seems to be a bug with pyqtgraph.PlotDataItem.setData not forcing a redraw. # We work around this by redrawing everything completely every time, which is unreasonably fast anyway. def redraw_histograms(self): self.delete_histograms() self.delete_histogram_labels() if self.combined_histograms: self.draw_combined_histogram() else: self.draw_separate_histograms() def delete_histograms(self): coords = set(c for c in histogram_coords.values()) histograms = (self.getItem(*coord) for coord in coords) for histogram in filter(lambda h: h is not None, histograms): self.removeItem(histogram) self.histogram = None self.before_histogram = None self.after_histogram = None self.diff_histogram = None def delete_histogram_labels(self): coords = set(c for c in label_coords.values()) labels = (self.getItem(*coord) for coord in coords) for label in filter(lambda h: h is not None, labels): self.removeItem(label) def draw_combined_histogram(self): self.histogram = self.addPlot(row=histogram_coords["combined"].row, col=histogram_coords["combined"].col, labels=histogram_axes_labels, lockAspect=True, colspan=3) self.addLabel("Pixel values", row=label_coords["combined"].row, col=label_coords["combined"].col) legend = self.histogram.addLegend() # Plot any histogram that has data, and add a legend if both exist if _data_valid_for_histogram(self.before_histogram_data): before_plot = self.histogram.plot(*self.before_histogram_data, pen=before_pen) legend.addItem(before_plot, "Before") if _data_valid_for_histogram(self.after_histogram_data): after_plot = self.histogram.plot(*self.after_histogram_data, pen=after_pen) legend.addItem(after_plot, "After") def draw_separate_histograms(self): hc = histogram_coords self.before_histogram = self.addPlot(row=hc["before"].row, col=hc["before"].col, labels=histogram_axes_labels, lockAspect=True) self.after_histogram = self.addPlot(row=hc["after"].row, col=hc["after"].col, labels=histogram_axes_labels, lockAspect=True) lc = label_coords self.addLabel("Pixel values before", row=lc["before"].row, col=lc["before"].col) self.addLabel("Pixel values after", row=lc["after"].row, col=lc["after"].col) if _data_valid_for_histogram(self.before_histogram_data): self.before_histogram.plot(*self.before_histogram_data, pen=before_pen) if _data_valid_for_histogram(self.after_histogram_data): self.after_histogram.plot(*self.after_histogram_data, pen=after_pen) def set_before_histogram(self, data: Tuple[ndarray]): self.before_histogram_data = data self.redraw_histograms() def set_after_histogram(self, data: Tuple[ndarray]): self.after_histogram_data = data self.redraw_histograms() @property def histogram_legend(self) -> Optional[LegendItem]: if self.histogram and self.histogram.legend: return self.histogram.legend return None def mouse_over(self, ev): # Ignore events triggered by leaving window or right clicking if ev.exit: return pos = CloseEnoughPoint(ev.pos()) for img in self.image_before, self.image_after, self.image_difference: if img.image is not None and pos.x < img.image.shape[ 0] and pos.y < img.image.shape[1]: pixel_value = img.image[pos.y, pos.x] self.display_formatted_detail[img](pixel_value) def link_all_views(self): for view1, view2 in zip( [self.image_before_vb, self.image_after_vb], [self.image_after_vb, self.image_difference_vb]): view1.linkView(ViewBox.XAxis, view2) view1.linkView(ViewBox.YAxis, view2) def unlink_all_views(self): for view in self.image_before_vb, self.image_after_vb, self.image_difference_vb: view.linkView(ViewBox.XAxis, None) view.linkView(ViewBox.YAxis, None) def add_difference_overlay(self, diff): diff = -diff diff[diff > 0.0] = 1.0 pos = np.array([0, 1]) color = np.array([[0, 0, 0, 0], [255, 0, 0, 255]], dtype=np.ubyte) map = ColorMap(pos, color) self.image_after_overlay.setOpacity(1) self.image_after_overlay.setImage(diff) lut = map.getLookupTable(0, 1, 2) self.image_after_overlay.setLookupTable(lut) def hide_difference_overlay(self): self.image_after_overlay.setOpacity(0)
class SideView(GraphicsView): """SideView class This class contains a series of functions allowing the user to simultaneously view the 3D volume from all orientations. Args: diff (int): Flag indicating the different brain view orientations. parent (class): Base or parent class """ def __init__(self, diff, parent=None): super(SideView, self).__init__(parent=parent) self.parent = parent self.brain = parent.brain self.diff = diff self.view1 = ViewBox() self.setCentralItem(self.view1) # Making Images out of data self.brain.section = (self.brain.section + self.diff) % 3 self.i = int(self.brain.shape[self.brain.section] / 2) data_slice = self.brain.get_data_slice(self.i) self.brain_img1 = ImageItem( data_slice, autoDownsample=False, compositionMode=QtGui.QPainter.CompositionMode_SourceOver) self.brain.section = (self.brain.section - self.diff) % 3 self.view1.addItem(self.brain_img1) self.view1.setAspectLocked(True) self.view1.setFixedHeight(250) self.view1.setFixedWidth(250) self.setMinimumHeight(250) self.vLine = InfiniteLine(angle=90, movable=False) self.hLine = InfiniteLine(angle=0, movable=False) self.vLine.setVisible(False) self.hLine.setVisible(False) self.view1.addItem(self.vLine, ignoreBounds=True) self.view1.addItem(self.hLine, ignoreBounds=True) def refresh_image(self): """Refresh Image This function refreshes the displayed volume orientation image. """ self.brain.section = (self.brain.section + self.diff) % 3 data_slice = self.brain.get_data_slice(self.i) self.brain_img1.setImage(data_slice) self.brain.section = (self.brain.section - self.diff) % 3 def refresh_all_images(self): """Refresh all images This function refreshes both the side view images when triggered. """ self.parent.main_widget._update_section_helper() self.parent.win1.refresh_image() self.parent.win1.view1.menu.actions()[0].trigger() self.parent.win2.refresh_image() self.parent.win2.view1.menu.actions()[0].trigger() def mouseDoubleClickEvent(self, event): """Click trigger Tracks a mouse double click event which triggers the refreshing of all the views Args event (event): Mouse double click events for the widget. """ super(SideView, self).mouseDoubleClickEvent(event) self.brain.section = (self.brain.section + self.diff) % 3 self.refresh_all_images() def set_i(self, position, out_of_box=False): """Set position This function sets the cursor position for each window. Args: position (tuple): Tuple containing the required coordinates. out_of_box (bool): Flag indicating if event possition is outisde of the considered volume. """ section = (self.brain.section + self.diff) % 3 if out_of_box: self.i = int(self.brain.shape[section] / 2) self.vLine.setVisible(False) self.hLine.setVisible(False) else: i = position[section] self.i = np.clip(i, 0, self.brain.shape[section] - 1) self.vLine.setVisible(True) self.hLine.setVisible(True) self.brain.section = (self.brain.section + self.diff) % 3 x, y = self.brain.voxel_as_position(position[0], position[1], position[2]) self.brain.section = (self.brain.section - self.diff) % 3 self.vLine.setPos(x) self.hLine.setPos(y)
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])