class TestActiveImageAlphaSlider(TestCaseQt): def setUp(self): super(TestActiveImageAlphaSlider, self).setUp() self.plot = PlotWidget() self.aslider = AlphaSlider.ActiveImageAlphaSlider(plot=self.plot) self.aslider.setOrientation(qt.Qt.Horizontal) toolbar = qt.QToolBar("plot", self.plot) toolbar.addWidget(self.aslider) self.plot.addToolBar(toolbar) self.plot.show() self.qWaitForWindowExposed(self.plot) self.mouseMove(self.plot) # Move to center self.qapp.processEvents() def tearDown(self): self.qapp.processEvents() self.plot.setAttribute(qt.Qt.WA_DeleteOnClose) self.plot.close() del self.plot del self.aslider super(TestActiveImageAlphaSlider, self).tearDown() def testWidgetEnabled(self): # no active image initially, slider must be deactivate self.assertFalse(self.aslider.isEnabled()) self.plot.addImage(numpy.array([[0, 1, 2], [3, 4, 5]])) # now we have an active image self.assertTrue(self.aslider.isEnabled()) self.plot.setActiveImage(None) self.assertFalse(self.aslider.isEnabled()) def testGetImage(self): self.plot.addImage(numpy.array([[0, 1, 2], [3, 4, 5]])) self.assertEqual(self.plot.getActiveImage(), self.aslider.getItem()) self.plot.addImage(numpy.array([[0, 1, 3], [2, 4, 6]]), legend="2") self.plot.setActiveImage("2") self.assertEqual(self.plot.getImage("2"), self.aslider.getItem()) def testGetAlpha(self): self.plot.addImage(numpy.array([[0, 1, 2], [3, 4, 5]]), legend="1") self.aslider.setValue(137) self.assertAlmostEqual(self.aslider.getAlpha(), 137. / 255)
class SilxMaskImageWidget(qt.QMainWindow): """Main window with a plot widget, a toolbar and a slider. A list of images can be set with :meth:`setImages`. The mask can be accessed through getter and setter methods: :meth:`setSelectionMask` and :meth:`getSelectionMask`. The plot widget can be accessed as :attr:`plot`. It is a silx plot widget. The toolbar offers some basic interaction tools: zoom control, colormap, aspect ratio, y axis orientation, "save image" menu and a mask widget. """ sigMaskImageWidget = qt.pyqtSignal(object) def __init__(self, parent=None): qt.QMainWindow.__init__(self, parent=parent) if parent is not None: # behave as a widget self.setWindowFlags(qt.Qt.Widget) else: self.setWindowTitle("PyMca - Image Selection Tool") centralWidget = qt.QWidget(self) layout = qt.QVBoxLayout(centralWidget) centralWidget.setLayout(layout) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) # Plot self.plot = PlotWidget(parent=centralWidget) self.plot.setWindowFlags(qt.Qt.Widget) self.plot.setDefaultColormap({ 'name': 'temperature', 'normalization': 'linear', 'autoscale': True, 'vmin': 0., 'vmax': 1. }) layout.addWidget(self.plot) # Mask Widget self._maskToolsDockWidget = None # Image selection slider self.slider = qt.QSlider(self.centralWidget()) self.slider.setOrientation(qt.Qt.Horizontal) self.slider.setMinimum(0) self.slider.setMaximum(0) layout.addWidget(self.slider) self.slider.valueChanged[int].connect(self.showImage) # ADD/REMOVE/REPLACE IMAGE buttons buttonBox = qt.QWidget(self) buttonBoxLayout = qt.QHBoxLayout(buttonBox) buttonBoxLayout.setContentsMargins(0, 0, 0, 0) buttonBoxLayout.setSpacing(0) self.addImageButton = qt.QPushButton(buttonBox) icon = qt.QIcon(qt.QPixmap(IconDict["rgb16"])) self.addImageButton.setIcon(icon) self.addImageButton.setText("ADD IMAGE") self.addImageButton.setToolTip("Add image to RGB correlator") buttonBoxLayout.addWidget(self.addImageButton) self.removeImageButton = qt.QPushButton(buttonBox) self.removeImageButton.setIcon(icon) self.removeImageButton.setText("REMOVE IMAGE") self.removeImageButton.setToolTip("Remove image from RGB correlator") buttonBoxLayout.addWidget(self.removeImageButton) self.replaceImageButton = qt.QPushButton(buttonBox) self.replaceImageButton.setIcon(icon) self.replaceImageButton.setText("REPLACE IMAGE") self.replaceImageButton.setToolTip( "Replace all images in RGB correlator with this one") buttonBoxLayout.addWidget(self.replaceImageButton) self.addImageButton.clicked.connect(self._addImageClicked) self.removeImageButton.clicked.connect(self._removeImageClicked) self.replaceImageButton.clicked.connect(self._replaceImageClicked) layout.addWidget(buttonBox) # median filter widget self._medianParameters = {'row_width': 1, 'column_width': 1} self._medianParametersWidget = MedianParameters(self) self._medianParametersWidget.widthSpin.setValue(1) self._medianParametersWidget.widthSpin.valueChanged[int].connect( self._setMedianKernelWidth) layout.addWidget(self._medianParametersWidget) self.setCentralWidget(centralWidget) # Init actions self.group = qt.QActionGroup(self) self.group.setExclusive(False) self.resetZoomAction = self.group.addAction( PlotActions.ResetZoomAction(plot=self.plot, parent=self)) self.addAction(self.resetZoomAction) self.zoomInAction = PlotActions.ZoomInAction(plot=self.plot, parent=self) self.addAction(self.zoomInAction) self.zoomOutAction = PlotActions.ZoomOutAction(plot=self.plot, parent=self) self.addAction(self.zoomOutAction) self.xAxisAutoScaleAction = self.group.addAction( PlotActions.XAxisAutoScaleAction(plot=self.plot, parent=self)) self.addAction(self.xAxisAutoScaleAction) self.yAxisAutoScaleAction = self.group.addAction( PlotActions.YAxisAutoScaleAction(plot=self.plot, parent=self)) self.addAction(self.yAxisAutoScaleAction) self.colormapAction = self.group.addAction( PlotActions.ColormapAction(plot=self.plot, parent=self)) self.addAction(self.colormapAction) self.copyAction = self.group.addAction( PlotActions.CopyAction(plot=self.plot, parent=self)) self.addAction(self.copyAction) self.group.addAction(self.getMaskAction()) # Init toolbuttons self.saveToolbutton = SaveToolButton(parent=self, maskImageWidget=self) self.yAxisInvertedButton = PlotToolButtons.YAxisOriginToolButton( parent=self, plot=self.plot) self.keepDataAspectRatioButton = PlotToolButtons.AspectToolButton( parent=self, plot=self.plot) self.backgroundButton = qt.QToolButton(self) self.backgroundButton.setCheckable(True) self.backgroundButton.setIcon( qt.QIcon(qt.QPixmap(IconDict["subtract"]))) self.backgroundButton.setToolTip( 'Toggle background image subtraction from current image\n' + 'No action if no background image available.') self.backgroundButton.clicked.connect(self._subtractBackground) # Creating the toolbar also create actions for toolbuttons self._toolbar = self._createToolBar(title='Plot', parent=None) self.addToolBar(self._toolbar) self._profile = ProfileToolBar(plot=self.plot) self.addToolBar(self._profile) self.setProfileToolbarVisible(False) # add a transparency slider for the stack data self._alphaSliderToolbar = qt.QToolBar("Alpha slider", parent=self) self._alphaSlider = NamedImageAlphaSlider( parent=self._alphaSliderToolbar, plot=self.plot, legend="current") self._alphaSlider.setOrientation(qt.Qt.Vertical) self._alphaSlider.setToolTip("Adjust opacity of stack image overlay") self._alphaSliderToolbar.addWidget(self._alphaSlider) self.addToolBar(qt.Qt.RightToolBarArea, self._alphaSliderToolbar) # hide optional tools and actions self.setAlphaSliderVisible(False) self.setBackgroundActionVisible(False) self.setMedianFilterWidgetVisible(False) self.setProfileToolbarVisible(False) self._images = [] """List of images, as 2D numpy arrays or 3D numpy arrays (RGB(A)). """ self._labels = [] """List of image labels. """ self._bg_images = [] """List of background images, as 2D numpy arrays or 3D numpy arrays (RGB(A)). These images are not active, their colormap cannot be changed and they cannot be the base image used for drawing a mask. """ self._bg_labels = [] self._deltaXY = ( 1.0, 1.0) # TODO: allow different scale and origin for each image """Current image scale (Xscale, Yscale) (in axis units per image pixel). The scale is adjusted to keep constant width and height for the image when a crop operation is applied.""" self._origin = (0., 0.) """Current image origin: coordinate (x, y) of sample located at (row, column) = (0, 0)""" # scales and origins for background images self._bg_deltaXY = [] self._bg_origins = [] def sizeHint(self): return qt.QSize(500, 400) def _createToolBar(self, title, parent): """Create a QToolBar with crop, rotate and flip operations :param str title: The title of the QMenu :param qt.QWidget parent: See :class:`QToolBar` """ toolbar = qt.QToolBar(title, parent) # Order widgets with actions objects = self.group.actions() # Add standard push buttons to list index = objects.index(self.colormapAction) objects.insert(index + 1, self.keepDataAspectRatioButton) objects.insert(index + 2, self.yAxisInvertedButton) objects.insert(index + 3, self.saveToolbutton) objects.insert(index + 4, self.backgroundButton) for obj in objects: if isinstance(obj, qt.QAction): toolbar.addAction(obj) else: # keep reference to toolbutton's action for changing visibility if obj is self.keepDataAspectRatioButton: self.keepDataAspectRatioAction = toolbar.addWidget(obj) elif obj is self.yAxisInvertedButton: self.yAxisInvertedAction = toolbar.addWidget(obj) elif obj is self.saveToolbutton: self.saveAction = toolbar.addWidget(obj) elif obj is self.backgroundButton: self.bgAction = toolbar.addWidget(obj) else: raise RuntimeError() return toolbar def _getMaskToolsDockWidget(self): """DockWidget with image mask panel (lazy-loaded).""" if self._maskToolsDockWidget is None: self._maskToolsDockWidget = MyMaskToolsDockWidget(plot=self.plot, name='Mask') self._maskToolsDockWidget.hide() self.addDockWidget(qt.Qt.RightDockWidgetArea, self._maskToolsDockWidget) # self._maskToolsDockWidget.setFloating(True) self._maskToolsDockWidget.sigMaskChanged.connect( self._emitMaskImageWidgetSignal) return self._maskToolsDockWidget def _setMedianKernelWidth(self, value): kernelSize = numpy.asarray(value) if len(kernelSize.shape) == 0: kernelSize = [kernelSize.item()] * 2 self._medianParameters['row_width'] = kernelSize[0] self._medianParameters['column_width'] = kernelSize[1] self._medianParametersWidget.widthSpin.setValue(int(kernelSize[0])) current = self.slider.value() self.showImage(current) def _subtractBackground(self): """When background button is clicked, this causes showImage to display the data after subtracting the stack background image. This background image is unrelated to the background images set with :meth:`setBackgroundImages`, it is simply the first data image whose label ends with 'background'.""" current = self.getCurrentIndex() self.showImage(current) def setBackgroundActionVisible(self, visible): """Set visibility of the background toolbar button. :param visible: True to show tool button, False to hide it. """ self.bgAction.setVisible(visible) def setProfileToolbarVisible(self, visible): """Set visibility of the profile toolbar :param visible: True to show toolbar, False to hide it. """ self._profile.setVisible(visible) def setMedianFilterWidgetVisible(self, visible): """Set visibility of the median filter parametrs widget. :param visible: True to show widget, False to hide it. """ self._medianParametersWidget.setVisible(visible) def setAlphaSliderVisible(self, visible): """Set visibility of the transparency slider widget in the right toolbar area. :param visible: True to show widget, False to hide it. """ self._alphaSliderToolbar.setVisible(visible) def setImagesAlpha(self, alpha): """Set the opacity of the images layer. Full opacity means that the background images layer will not be visible. :param float alpha: Opacity of images layer, in [0., 1.] """ self._alphaSlider.setValue(round(alpha * 255)) def getMaskAction(self): """QAction toggling image mask dock widget :rtype: QAction """ return self._getMaskToolsDockWidget().toggleViewAction() def _emitMaskImageWidgetSignal(self): mask = self.getSelectionMask() if not mask.size: # workaround to ignore the empty mask emitted when the mask widget # is initialized return self.sigMaskImageWidget.emit({ "event": "selectionMaskChanged", "current": self.getSelectionMask(), "id": id(self) }) def setSelectionMask(self, mask, copy=True): """Set the mask to a new array. :param numpy.ndarray mask: The array to use for the mask. Mask type: array of uint8 of dimension 2, Array of other types are converted. :param bool copy: True (the default) to copy the array, False to use it as is if possible. :return: None if failed, shape of mask as 2-tuple if successful. The mask can be cropped or padded to fit active image, the returned shape is that of the active image. """ if mask is None: mask = numpy.zeros_like( self._getMaskToolsDockWidget().getSelectionMask()) if not len(mask): return # disconnect temporarily to avoid infinite loop self._getMaskToolsDockWidget().sigMaskChanged.disconnect( self._emitMaskImageWidgetSignal) ret = self._getMaskToolsDockWidget().setSelectionMask(mask, copy=copy) self._getMaskToolsDockWidget().sigMaskChanged.connect( self._emitMaskImageWidgetSignal) return ret def getSelectionMask(self, copy=True): """Get the current mask as a 2D array. :param bool copy: True (default) to get a copy of the mask. If False, the returned array MUST not be modified. :return: The array of the mask with dimension of the 'active' image. If there is no active image, an empty array is returned. :rtype: 2D numpy.ndarray of uint8 """ return self._getMaskToolsDockWidget().getSelectionMask(copy=copy) @staticmethod def _RgbaToGrayscale(image): """Convert RGBA image to 2D array of grayscale values (Luma coding) :param image: RGBA image, as a numpy array of shapes (nrows, ncols, 3/4) :return: Image as a 2D array """ if len(image.shape) == 2: return image assert len(image.shape) == 3 imageData = image[:, :, 0] * 0.299 +\ image[:, :, 1] * 0.587 +\ image[:, :, 2] * 0.114 return imageData def getImageData(self): """Return current image data to be sent to RGB correlator :return: Image as a 2D array """ index = self.slider.value() image = self._images[index] return self._RgbaToGrayscale(image) def getFirstBgImageData(self): """Return first bg image data to be sent to RGB correlator :return: Image as a 2D array """ image = self._bg_images[0] return self._RgbaToGrayscale(image) def _addImageClicked(self): imageData = self.getImageData() ddict = { 'event': "addImageClicked", 'image': imageData, 'title': self.plot.getGraphTitle(), 'id': id(self) } self.sigMaskImageWidget.emit(ddict) def _replaceImageClicked(self): imageData = self.getImageData() ddict = { 'event': "replaceImageClicked", 'image': imageData, 'title': self.plot.getGraphTitle(), 'id': id(self) } self.sigMaskImageWidget.emit(ddict) def _removeImageClicked(self): imageData = self.getImageData() ddict = { 'event': "removeImageClicked", 'image': imageData, 'title': self.plot.getGraphTitle(), 'id': id(self) } self.sigMaskImageWidget.emit(ddict) def showImage(self, index=0): """Show data image corresponding to index. Update slider to index. """ if not self._images: return assert index < len(self._images) bg_index = None if self.backgroundButton.isChecked(): for i, imageName in enumerate(self._labels): if imageName.lower().endswith('background'): bg_index = i break mf_text = "" a = self._medianParameters['row_width'] b = self._medianParameters['column_width'] if max(a, b) > 1: mf_text = "MF(%d,%d) " % (a, b) imdata = self._getMedianData(self._images[index]) if bg_index is None: self.plot.setGraphTitle(mf_text + self._labels[index]) else: self.plot.setGraphTitle(mf_text + self._labels[index] + " Net") imdata -= self._images[bg_index] self.plot.addImage(imdata, legend="current", origin=self._origin, scale=self._deltaXY, replace=False, z=0) self.plot.setActiveImage("current") self.slider.setValue(index) def _getMedianData(self, data): data = copy.copy(data) if max(self._medianParameters['row_width'], self._medianParameters['column_width']) > 1: data = medfilt2d(data, [ self._medianParameters['row_width'], self._medianParameters['column_width'] ]) return data def setImages(self, images, labels=None, origin=None, height=None, width=None): """Set the list of data images. All images share the same origin, width and height. :param images: List of 2D or 3D (for RGBA data) numpy arrays of image data. All images must have the same shape. :type images: List of ndarrays :param labels: list of image names :param origin: Image origin: coordinate (x, y) of sample located at (row, column) = (0, 0). If None, use (0., 0.) :param height: Image height in Y axis units. If None, use the image height in number of pixels. :param width: Image width in X axis units. If None, use the image width in number of pixels. """ self._images = images if labels is None: labels = ["Image %d" % (i + 1) for i in range(len(images))] self._labels = labels height_pixels, width_pixels = images[0].shape[0:2] height = height or height_pixels width = width or width_pixels self._deltaXY = (float(width) / width_pixels, float(height) / height_pixels) self._origin = origin or (0., 0.) current = self.slider.value() self.slider.setMaximum(len(self._images) - 1) if current < len(self._images): self.showImage(current) else: self.showImage(0) # _maskParamsCache = width, height, self._origin, self._deltaXY # if _maskParamsCache != self._maskParamsCache: # self._maskParamsCache = _maskParamsCache # self.resetMask(width, height, self._origin, self._deltaXY) def _updateBgScales(self, heights, widths): """Recalculate BG scales (e.g after a crop operation on :attr:`_bg_images`)""" self._bg_deltaXY = [] for w, h, img in zip(widths, heights, self._bg_images): self._bg_deltaXY.append( (float(w) / img.shape[1], float(h) / img.shape[0])) def setBackgroundImages(self, images, labels=None, origins=None, heights=None, widths=None): """Set the list of background images. Each image should be a tile and have an origin (x, y) tuple, a height and a width defined, so that all images can be plotted on the same background layer. :param images: List of 2D or 3D (for RGBA data) numpy arrays of image data. All images must have the same shape. :type images: List of ndarrays :param labels: list of image names :param origins: Images origins: list of coordinate tuples (x, y) of sample located at (row, column) = (0, 0). If None, use (0., 0.) for all images. :param height: Image height in Y axis units. If None, use the image height in number of pixels. :param width: Image width in X axis units. If None, use the image width in number of pixels. """ self._bg_images = images if labels is None: labels = [ "Background image %d" % (i + 1) for i in range(len(images)) ] # delete existing images for label in self._bg_labels: self.plot.removeImage(label) self._bg_labels = labels if heights is None: heights = [image.shape[0] for image in images] else: assert len(heights) == len(images) if widths is None: widths = [image.shape[1] for image in images] else: assert len(widths) == len(images) if origins is None: origins = [(0, 0) for _img in images] else: assert len(origins) == len(images) self._bg_origins = origins self._updateBgScales(heights, widths) for bg_deltaXY, bg_orig, label, img in zip(self._bg_deltaXY, self._bg_origins, labels, images): # FIXME: we use z=-1 because the mask is always on z=1, # so the data must be on z=0. To be fixed after the silx mask # is improved self.plot.addImage(img, origin=bg_orig, scale=bg_deltaXY, legend=label, replace=False, z=-1) # TODO: z=0 def getCurrentIndex(self): """ :return: Index of slider widget used for image selection. """ return self.slider.value() def getCurrentColormap(self): """Return colormap dict associated with the current image. If the current image is a RGBA Image, return None. See doc of silx.gui.plot.Plot for an explanation about the colormap dictionary. """ image = self.plot.getImage(legend="current") if not hasattr(image, "getColormap" ): # isinstance(image, silx.gui.plot.items.ImageRgba): return None return self.plot.getImage(legend="current").getColormap() def showAndRaise(self): self.show() self.raise_()
def setActiveImage(self, *var, **kw): if "replot" in kw: del kw["replot"] return PlotWidget.setActiveImage(self, *var, **kw)
class SilxMaskImageWidget(qt.QMainWindow): """Main window with a plot widget, a toolbar and a slider. A list of images can be set with :meth:`setImages`. The mask can be accessed through getter and setter methods: :meth:`setSelectionMask` and :meth:`getSelectionMask`. The plot widget can be accessed as :attr:`plot`. It is a silx plot widget. The toolbar offers some basic interaction tools: zoom control, colormap, aspect ratio, y axis orientation, "save image" menu and a mask widget. """ sigMaskImageWidget = qt.pyqtSignal(object) def __init__(self, parent=None): qt.QMainWindow.__init__(self, parent=parent) if parent is not None: # behave as a widget self.setWindowFlags(qt.Qt.Widget) else: self.setWindowTitle("PyMca - Image Selection Tool") centralWidget = qt.QWidget(self) layout = qt.QVBoxLayout(centralWidget) centralWidget.setLayout(layout) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) # Plot self.plot = PlotWidget(parent=centralWidget) self.plot.setWindowFlags(qt.Qt.Widget) self.plot.setDefaultColormap({'name': 'temperature', 'normalization': 'linear', 'autoscale': True, 'vmin': 0., 'vmax': 1.}) layout.addWidget(self.plot) # Mask Widget self._maskToolsDockWidget = None # Image selection slider self.slider = qt.QSlider(self.centralWidget()) self.slider.setOrientation(qt.Qt.Horizontal) self.slider.setMinimum(0) self.slider.setMaximum(0) layout.addWidget(self.slider) self.slider.valueChanged[int].connect(self.showImage) # ADD/REMOVE/REPLACE IMAGE buttons buttonBox = qt.QWidget(self) buttonBoxLayout = qt.QHBoxLayout(buttonBox) buttonBoxLayout.setContentsMargins(0, 0, 0, 0) buttonBoxLayout.setSpacing(0) self.addImageButton = qt.QPushButton(buttonBox) icon = qt.QIcon(qt.QPixmap(IconDict["rgb16"])) self.addImageButton.setIcon(icon) self.addImageButton.setText("ADD IMAGE") self.addImageButton.setToolTip("Add image to RGB correlator") buttonBoxLayout.addWidget(self.addImageButton) self.removeImageButton = qt.QPushButton(buttonBox) self.removeImageButton.setIcon(icon) self.removeImageButton.setText("REMOVE IMAGE") self.removeImageButton.setToolTip("Remove image from RGB correlator") buttonBoxLayout.addWidget(self.removeImageButton) self.replaceImageButton = qt.QPushButton(buttonBox) self.replaceImageButton.setIcon(icon) self.replaceImageButton.setText("REPLACE IMAGE") self.replaceImageButton.setToolTip( "Replace all images in RGB correlator with this one") buttonBoxLayout.addWidget(self.replaceImageButton) self.addImageButton.clicked.connect(self._addImageClicked) self.removeImageButton.clicked.connect(self._removeImageClicked) self.replaceImageButton.clicked.connect(self._replaceImageClicked) layout.addWidget(buttonBox) # median filter widget self._medianParameters = {'row_width': 1, 'column_width': 1, 'conditional': 0} self._medianParametersWidget = MedianParameters(self, use_conditional=True) self._medianParametersWidget.widthSpin.setValue(1) self._medianParametersWidget.widthSpin.valueChanged[int].connect( self._setMedianKernelWidth) self._medianParametersWidget.conditionalSpin.valueChanged[int].connect( self._setMedianConditionalFlag) layout.addWidget(self._medianParametersWidget) # motor positions (hidden by default) self.motorPositionsWidget = MotorInfoWindow.MotorInfoDialog(self, [""], [{}]) self.motorPositionsWidget.setMaximumHeight(100) self.plot.sigPlotSignal.connect(self._updateMotors) self.motorPositionsWidget.hide() self._motors_first_update = True layout.addWidget(self.motorPositionsWidget) self.setCentralWidget(centralWidget) # Init actions self.group = qt.QActionGroup(self) self.group.setExclusive(False) self.resetZoomAction = self.group.addAction( PlotActions.ResetZoomAction(plot=self.plot, parent=self)) self.addAction(self.resetZoomAction) self.zoomInAction = PlotActions.ZoomInAction(plot=self.plot, parent=self) self.addAction(self.zoomInAction) self.zoomOutAction = PlotActions.ZoomOutAction(plot=self.plot, parent=self) self.addAction(self.zoomOutAction) self.xAxisAutoScaleAction = self.group.addAction( PlotActions.XAxisAutoScaleAction(plot=self.plot, parent=self)) self.addAction(self.xAxisAutoScaleAction) self.yAxisAutoScaleAction = self.group.addAction( PlotActions.YAxisAutoScaleAction(plot=self.plot, parent=self)) self.addAction(self.yAxisAutoScaleAction) self.colormapAction = self.group.addAction( PlotActions.ColormapAction(plot=self.plot, parent=self)) self.addAction(self.colormapAction) self.copyAction = self.group.addAction( PlotActions.CopyAction(plot=self.plot, parent=self)) self.addAction(self.copyAction) self.group.addAction(self.getMaskAction()) # Init toolbuttons self.saveToolbutton = SaveToolButton(parent=self, maskImageWidget=self) self.yAxisInvertedButton = PlotToolButtons.YAxisOriginToolButton( parent=self, plot=self.plot) self.keepDataAspectRatioButton = PlotToolButtons.AspectToolButton( parent=self, plot=self.plot) self.backgroundButton = qt.QToolButton(self) self.backgroundButton.setCheckable(True) self.backgroundButton.setIcon(qt.QIcon(qt.QPixmap(IconDict["subtract"]))) self.backgroundButton.setToolTip( 'Toggle background image subtraction from current image\n' + 'No action if no background image available.') self.backgroundButton.clicked.connect(self._subtractBackground) # Creating the toolbar also create actions for toolbuttons self._toolbar = self._createToolBar(title='Plot', parent=None) self.addToolBar(self._toolbar) self._profile = ProfileToolBar(plot=self.plot) self.addToolBar(self._profile) self.setProfileToolbarVisible(False) # add a transparency slider for the stack data self._alphaSliderToolbar = qt.QToolBar("Alpha slider", parent=self) self._alphaSlider = NamedImageAlphaSlider(parent=self._alphaSliderToolbar, plot=self.plot, legend="current") self._alphaSlider.setOrientation(qt.Qt.Vertical) self._alphaSlider.setToolTip("Adjust opacity of stack image overlay") self._alphaSliderToolbar.addWidget(self._alphaSlider) self.addToolBar(qt.Qt.RightToolBarArea, self._alphaSliderToolbar) # hide optional tools and actions self.setAlphaSliderVisible(False) self.setBackgroundActionVisible(False) self.setMedianFilterWidgetVisible(False) self.setProfileToolbarVisible(False) self._images = [] """List of images, as 2D numpy arrays or 3D numpy arrays (RGB(A)). """ self._labels = [] """List of image labels. """ self._bg_images = [] """List of background images, as 2D numpy arrays or 3D numpy arrays (RGB(A)). These images are not active, their colormap cannot be changed and they cannot be the base image used for drawing a mask. """ self._bg_labels = [] self._deltaXY = (1.0, 1.0) # TODO: allow different scale and origin for each image """Current image scale (Xscale, Yscale) (in axis units per image pixel). The scale is adjusted to keep constant width and height for the image when a crop operation is applied.""" self._origin = (0., 0.) """Current image origin: coordinate (x, y) of sample located at (row, column) = (0, 0)""" # scales and origins for background images self._bg_deltaXY = [] self._bg_origins = [] def sizeHint(self): return qt.QSize(500, 400) def _createToolBar(self, title, parent): """Create a QToolBar with crop, rotate and flip operations :param str title: The title of the QMenu :param qt.QWidget parent: See :class:`QToolBar` """ toolbar = qt.QToolBar(title, parent) # Order widgets with actions objects = self.group.actions() # Add standard push buttons to list index = objects.index(self.colormapAction) objects.insert(index + 1, self.keepDataAspectRatioButton) objects.insert(index + 2, self.yAxisInvertedButton) objects.insert(index + 3, self.saveToolbutton) objects.insert(index + 4, self.backgroundButton) for obj in objects: if isinstance(obj, qt.QAction): toolbar.addAction(obj) else: # keep reference to toolbutton's action for changing visibility if obj is self.keepDataAspectRatioButton: self.keepDataAspectRatioAction = toolbar.addWidget(obj) elif obj is self.yAxisInvertedButton: self.yAxisInvertedAction = toolbar.addWidget(obj) elif obj is self.saveToolbutton: self.saveAction = toolbar.addWidget(obj) elif obj is self.backgroundButton: self.bgAction = toolbar.addWidget(obj) else: raise RuntimeError() return toolbar def _getMaskToolsDockWidget(self): """DockWidget with image mask panel (lazy-loaded).""" if self._maskToolsDockWidget is None: self._maskToolsDockWidget = MyMaskToolsDockWidget( plot=self.plot, name='Mask') self._maskToolsDockWidget.hide() self.addDockWidget(qt.Qt.RightDockWidgetArea, self._maskToolsDockWidget) # self._maskToolsDockWidget.setFloating(True) self._maskToolsDockWidget.sigMaskChanged.connect( self._emitMaskImageWidgetSignal) return self._maskToolsDockWidget def _setMedianKernelWidth(self, value): kernelSize = numpy.asarray(value) if len(kernelSize.shape) == 0: kernelSize = [kernelSize.item()] * 2 self._medianParameters['row_width'] = kernelSize[0] self._medianParameters['column_width'] = kernelSize[1] self._medianParametersWidget.widthSpin.setValue(int(kernelSize[0])) self.showImage(self.slider.value()) def _setMedianConditionalFlag(self, value): self._medianParameters['conditional'] = int(value) self._medianParametersWidget.conditionalSpin.setValue(int(value)) self.showImage(self.slider.value()) def _subtractBackground(self): """When background button is clicked, this causes showImage to display the data after subtracting the stack background image. This background image is unrelated to the background images set with :meth:`setBackgroundImages`, it is simply the first data image whose label ends with 'background'.""" current = self.getCurrentIndex() self.showImage(current) def _updateMotors(self, ddict): if not ddict["event"] == "mouseMoved": return if not self.motorPositionsWidget.isVisible(): return motorsValuesAtCursor = self._getPositionersFromXY(ddict["x"], ddict["y"]) if motorsValuesAtCursor is None: return self.motorPositionsWidget.table.updateTable( legList=[self.plot.getActiveImage().getLegend()], motList=[motorsValuesAtCursor]) if self._motors_first_update: self._select_motors() self._motors_first_update = False def _select_motors(self): """This methods sets the motors in the comboboxes when the widget is first initialized.""" for i, combobox in enumerate(self.motorPositionsWidget.table.header.boxes): # First item (index 0) in combobox is "", so first motor name is at index 1. # First combobox in header.boxes is at index 1 (boxes[0] is None). if i == 0: continue if i < combobox.count(): combobox.setCurrentIndex(i) def _getPositionersFromXY(self, x, y): """Return positioner values for a stack pixel identified by it's (x, y) coordinates. """ activeImage = self.plot.getActiveImage() if activeImage is None: return None info = activeImage.getInfo() if not info or not isinstance(info, dict): return None positioners = info.get("positioners", {}) nRows, nCols = activeImage.getData().shape xScale, yScale = activeImage.getScale() xOrigin, yOrigin = activeImage.getOrigin() r, c = convertToRowAndColumn( x, y, shape=(nRows, nCols), xScale=(xOrigin, xScale), yScale=(yOrigin, yScale), safe=True) idx1d = r * nCols + c positionersAtIdx = {} for motorName, motorValues in positioners.items(): if numpy.isscalar(motorValues): positionersAtIdx[motorName] = motorValues elif len(motorValues.shape) == 1: positionersAtIdx[motorName] = motorValues[idx1d] else: positionersAtIdx[motorName] = motorValues.reshape((-1,))[idx1d] return positionersAtIdx # widgets visibility toggling def setBackgroundActionVisible(self, visible): """Set visibility of the background toolbar button. :param visible: True to show tool button, False to hide it. """ self.bgAction.setVisible(visible) def setProfileToolbarVisible(self, visible): """Set visibility of the profile toolbar :param visible: True to show toolbar, False to hide it. """ self._profile.setVisible(visible) def setMedianFilterWidgetVisible(self, visible): """Set visibility of the median filter parametrs widget. :param visible: True to show widget, False to hide it. """ self._medianParametersWidget.setVisible(visible) def setMotorPositionsVisible(self, flag): """Show or hide motor positions widget""" self.motorPositionsWidget.setVisible(flag) def setAlphaSliderVisible(self, visible): """Set visibility of the transparency slider widget in the right toolbar area. :param visible: True to show widget, False to hide it. """ self._alphaSliderToolbar.setVisible(visible) def setImagesAlpha(self, alpha): """Set the opacity of the images layer. Full opacity means that the background images layer will not be visible. :param float alpha: Opacity of images layer, in [0., 1.] """ self._alphaSlider.setValue(round(alpha * 255)) def getMaskAction(self): """QAction toggling image mask dock widget :rtype: QAction """ return self._getMaskToolsDockWidget().toggleViewAction() def _emitMaskImageWidgetSignal(self): mask = self.getSelectionMask() if not mask.size: # workaround to ignore the empty mask emitted when the mask widget # is initialized return self.sigMaskImageWidget.emit( {"event": "selectionMaskChanged", "current": self.getSelectionMask(), "id": id(self)}) def setSelectionMask(self, mask, copy=True): """Set the mask to a new array. :param numpy.ndarray mask: The array to use for the mask. Mask type: array of uint8 of dimension 2, Array of other types are converted. :param bool copy: True (the default) to copy the array, False to use it as is if possible. :return: None if failed, shape of mask as 2-tuple if successful. The mask can be cropped or padded to fit active image, the returned shape is that of the active image. """ # disconnect temporarily to avoid infinite loop self._getMaskToolsDockWidget().sigMaskChanged.disconnect( self._emitMaskImageWidgetSignal) if mask is None and silx.version_info <= (0, 7, 0): self._getMaskToolsDockWidget().resetSelectionMask() ret = None else: # from silx 0.8 onwards, setSelectionMask(None) is supported ret = self._getMaskToolsDockWidget().setSelectionMask(mask, copy=copy) self._getMaskToolsDockWidget().sigMaskChanged.connect( self._emitMaskImageWidgetSignal) return ret def getSelectionMask(self, copy=True): """Get the current mask as a 2D array. :param bool copy: True (default) to get a copy of the mask. If False, the returned array MUST not be modified. :return: The array of the mask with dimension of the 'active' image. If there is no active image, None is returned. :rtype: 2D numpy.ndarray of uint8 """ return self._getMaskToolsDockWidget().getSelectionMask(copy=copy) @staticmethod def _RgbaToGrayscale(image): """Convert RGBA image to 2D array of grayscale values (Luma coding) :param image: RGBA image, as a numpy array of shapes (nrows, ncols, 3/4) :return: Image as a 2D array """ if len(image.shape) == 2: return image assert len(image.shape) == 3 imageData = image[:, :, 0] * 0.299 +\ image[:, :, 1] * 0.587 +\ image[:, :, 2] * 0.114 return imageData def getImageData(self): """Return current image data to be sent to RGB correlator :return: Image as a 2D array """ index = self.slider.value() image = self._images[index] return self._RgbaToGrayscale(image) def getFirstBgImageData(self): """Return first bg image data to be sent to RGB correlator :return: Image as a 2D array """ image = self._bg_images[0] return self._RgbaToGrayscale(image) def getBgImagesDict(self): """Return a dict containing the data for all background images.""" bgimages = {} for i, label in enumerate(self._bg_labels): data = self._bg_images[i] origin = self._bg_origins[i] delta_w, delta_h = self._bg_deltaXY[i] w, h = delta_w * data.shape[1], delta_h * data.shape[0] bgimages[label] = {"data": data, "origin": origin, "width": w, "height": h} return bgimages def _addImageClicked(self): imageData = self.getImageData() ddict = { 'event': "addImageClicked", 'image': imageData, 'title': self.plot.getGraphTitle(), 'id': id(self)} self.sigMaskImageWidget.emit(ddict) def _replaceImageClicked(self): imageData = self.getImageData() ddict = { 'event': "replaceImageClicked", 'image': imageData, 'title': self.plot.getGraphTitle(), 'id': id(self)} self.sigMaskImageWidget.emit(ddict) def _removeImageClicked(self): imageData = self.getImageData() ddict = { 'event': "removeImageClicked", 'image': imageData, 'title': self.plot.getGraphTitle(), 'id': id(self)} self.sigMaskImageWidget.emit(ddict) def showImage(self, index=0): """Show data image corresponding to index. Update slider to index. """ if not self._images: return assert index < len(self._images) bg_index = None if self.backgroundButton.isChecked(): for i, imageName in enumerate(self._labels): if imageName.lower().endswith('background'): bg_index = i break mf_text = "" a = self._medianParameters['row_width'] b = self._medianParameters['column_width'] if max(a, b) > 1: mf_text = "MF(%d,%d) " % (a, b) imdata = self._getMedianData(self._images[index]) if bg_index is None: self.plot.setGraphTitle(mf_text + self._labels[index]) else: self.plot.setGraphTitle(mf_text + self._labels[index] + " Net") imdata -= self._images[bg_index] self.plot.addImage(imdata, legend="current", origin=self._origin, scale=self._deltaXY, replace=False, z=0, info=self._infos[index]) self.plot.setActiveImage("current") self.slider.setValue(index) def _getMedianData(self, data): data = copy.copy(data) if max(self._medianParameters['row_width'], self._medianParameters['column_width']) > 1: data = medfilt2d(data, [self._medianParameters['row_width'], self._medianParameters['column_width']], conditional=self._medianParameters['conditional']) return data def setImages(self, images, labels=None, origin=None, height=None, width=None, infos=None): """Set the list of data images. All images share the same origin, width and height. :param images: List of 2D or 3D (for RGBA data) numpy arrays of image data. All images must have the same shape. :type images: List of ndarrays :param labels: list of image names :param origin: Image origin: coordinate (x, y) of sample located at (row, column) = (0, 0). If None, use (0., 0.) :param height: Image height in Y axis units. If None, use the image height in number of pixels. :param width: Image width in X axis units. If None, use the image width in number of pixels. :param infos: List of info dicts, one per image, or None. """ self._images = images if labels is None: labels = ["Image %d" % (i + 1) for i in range(len(images))] if infos is None: infos = [{} for _img in images] self._labels = labels self._infos = infos height_pixels, width_pixels = images[0].shape[0:2] height = height or height_pixels width = width or width_pixels self._deltaXY = (float(width) / width_pixels, float(height) / height_pixels) self._origin = origin or (0., 0.) current = self.slider.value() self.slider.setMaximum(len(self._images) - 1) if current < len(self._images): self.showImage(current) else: self.showImage(0) # _maskParamsCache = width, height, self._origin, self._deltaXY # if _maskParamsCache != self._maskParamsCache: # self._maskParamsCache = _maskParamsCache # self.resetMask(width, height, self._origin, self._deltaXY) def _updateBgScales(self, heights, widths): """Recalculate BG scales (e.g after a crop operation on :attr:`_bg_images`)""" self._bg_deltaXY = [] for w, h, img in zip(widths, heights, self._bg_images): self._bg_deltaXY.append( (float(w) / img.shape[1], float(h) / img.shape[0]) ) def setBackgroundImages(self, images, labels=None, origins=None, heights=None, widths=None): """Set the list of background images. Each image should be a tile and have an origin (x, y) tuple, a height and a width defined, so that all images can be plotted on the same background layer. :param images: List of 2D or 3D (for RGBA data) numpy arrays of image data. All images must have the same shape. :type images: List of ndarrays :param labels: list of image names :param origins: Images origins: list of coordinate tuples (x, y) of sample located at (row, column) = (0, 0). If None, use (0., 0.) for all images. :param heights: Image height in Y axis units. If None, use the image height in number of pixels. :param widths: Image width in X axis units. If None, use the image width in number of pixels. """ self._bg_images = images if labels is None: labels = ["Background image %d" % (i + 1) for i in range(len(images))] # delete existing images for label in self._bg_labels: self.plot.removeImage(label) self._bg_labels = labels if heights is None: heights = [image.shape[0] for image in images] else: assert len(heights) == len(images) if widths is None: widths = [image.shape[1] for image in images] else: assert len(widths) == len(images) if origins is None: origins = [(0, 0) for _img in images] else: assert len(origins) == len(images) self._bg_origins = origins self._updateBgScales(heights, widths) for bg_deltaXY, bg_orig, label, img in zip(self._bg_deltaXY, self._bg_origins, labels, images): # FIXME: we use z=-1 because the mask is always on z=1, # so the data must be on z=0. To be fixed after the silx mask # is improved self.plot.addImage(img, origin=bg_orig, scale=bg_deltaXY, legend=label, replace=False, z=-1) # TODO: z=0 def getCurrentIndex(self): """ :return: Index of slider widget used for image selection. """ return self.slider.value() def getCurrentColormap(self): """Return colormap dict associated with the current image. If the current image is a RGBA Image, return None. See doc of silx.gui.plot.Plot for an explanation about the colormap dictionary. """ image = self.plot.getImage(legend="current") if not hasattr(image, "getColormap"): # isinstance(image, silx.gui.plot.items.ImageRgba): return None return self.plot.getImage(legend="current").getColormap() def showAndRaise(self): self.show() self.raise_()