diff --git a/src/silx/gui/data/DataViews.py b/src/silx/gui/data/DataViews.py index ed688b8e2e..5107227965 100644 --- a/src/silx/gui/data/DataViews.py +++ b/src/silx/gui/data/DataViews.py @@ -37,6 +37,8 @@ from silx.io.nxdata import get_attr_as_unicode from silx.gui.colors import Colormap from silx.gui.dialog.ColormapDialog import ColormapDialog +from silx.gui.plot.items.image import ImageDataAggregated +from silx.gui.plot.actions.image import AggregationModeAction __authors__ = ["V. Valls", "P. Knobel"] __license__ = "MIT" @@ -1066,6 +1068,18 @@ def createWidget(self, parent): widget.setDefaultColormap(self.defaultColormap()) widget.getColormapAction().setColormapDialog(self.defaultColorDialog()) widget.getIntensityHistogramAction().setVisible(True) + + self.__aggregationModeAction = AggregationModeAction(parent=widget) + widget.toolBar().addAction(self.__aggregationModeAction) + self.__aggregationModeAction.sigAggregationModeChanged.connect(self._aggregationModeChanged) + + self.__imageItem = ImageDataAggregated() + self.__imageItem.setAggregationMode(self.__aggregationModeAction.getAggregationMode()) + self.__imageItem.setName("data") + self.__imageItem.setColormap(widget.getDefaultColormap()) + widget.addItem(self.__imageItem) + widget.setActiveImage(self.__imageItem) + widget.setKeepDataAspectRatio(True) widget.getXAxis().setLabel("X") widget.getYAxis().setLabel("Y") @@ -1073,8 +1087,11 @@ def createWidget(self, parent): maskToolsWidget.setItemMaskUpdated(True) return widget + def _aggregationModeChanged(self): + self.__imageItem.setAggregationMode(self.__aggregationModeAction.getAggregationMode()) + def clear(self): - self.getWidget().clear() + self.__imageItem.setData(numpy.zeros((0, 0), dtype=numpy.float32)) self.__resetZoomNextTime = True def normalizeData(self, data): @@ -1084,9 +1101,11 @@ def normalizeData(self, data): def setData(self, data): data = self.normalizeData(data) - self.getWidget().addImage( - legend="data", data=data, resetzoom=self.__resetZoomNextTime - ) + plot = self.getWidget() + + self.__imageItem.setData(data=data) + if self.__resetZoomNextTime: + plot.resetZoom() self.__resetZoomNextTime = False def setDataSelection(self, selection): diff --git a/src/silx/gui/data/NXdataWidgets.py b/src/silx/gui/data/NXdataWidgets.py index a2bab7a3d8..147bcfcac9 100644 --- a/src/silx/gui/data/NXdataWidgets.py +++ b/src/silx/gui/data/NXdataWidgets.py @@ -34,6 +34,8 @@ from silx.gui.data.NumpyAxesSelector import NumpyAxesSelector from silx.gui.plot import Plot1D, Plot2D, StackView, ScatterView, items from silx.gui.plot.ComplexImageView import ComplexImageView +from silx.gui.plot.items.image_aggregated import ImageDataAggregated +from silx.gui.plot.actions.image import AggregationModeAction from silx.gui.colors import Colormap from silx.gui.widgets.FrameBrowser import HorizontalSliderWithBrowser @@ -422,7 +424,25 @@ def __init__(self, parent=None): layout.addWidget(self._auxSigSlider) self.setLayout(layout) + + self.__aggregationModeAction = AggregationModeAction(parent=self) + self.getPlot().toolBar().addAction(self.__aggregationModeAction) + self.__aggregationModeAction.sigAggregationModeChanged.connect(self._aggregationModeChanged) + def getAggregationModeAction(self) -> AggregationModeAction: + """Action toggling the aggregation mode action + """ + return self.__aggregationModeAction + + def _aggregationModeChanged(self): + item = self.getPlot()._getItem("image") + + if item is None: + return + + if isinstance(item, ImageDataAggregated): + item.setAggregationMode(self.getAggregationModeAction().getAggregationMode()) + def _sliderIdxChanged(self, value): self._updateImage() @@ -555,6 +575,7 @@ def _updateImage(self): "image", ) ) + if xcalib.is_affine() and ycalib.is_affine(): # regular image xorigin, xscale = xcalib(0), xcalib.get_slope() @@ -564,14 +585,16 @@ def _updateImage(self): self._plot.getXAxis().setScale("linear") self._plot.getYAxis().setScale("linear") - self._plot.addImage( - image, - legend=legend, - origin=origin, - scale=scale, - replace=True, - resetzoom=False, - ) + + imageItem = ImageDataAggregated() + imageItem.setName(legend) + imageItem.setData(image) + imageItem.setOrigin(origin) + imageItem.setScale(scale) + imageItem.setColormap(self._plot.getDefaultColormap()) + imageItem.setAggregationMode(self.getAggregationModeAction().getAggregationMode()) + self._plot.addItem(imageItem) + self._plot.setActiveImage(imageItem) else: xaxisscale, yaxisscale = self._axis_scales diff --git a/src/silx/gui/plot/ImageView.py b/src/silx/gui/plot/ImageView.py index 37710f03c5..31684c66d0 100644 --- a/src/silx/gui/plot/ImageView.py +++ b/src/silx/gui/plot/ImageView.py @@ -65,6 +65,7 @@ from . import _utils from .tools.profile import rois from .actions import PlotAction +from .actions.image import AggregationModeAction from silx._utils import NP_OPTIONAL_COPY _logger = logging.getLogger(__name__) @@ -353,90 +354,6 @@ def _actionTriggered(self, checked=False): self.plot.setSideHistogramDisplayed(checked) -class AggregationModeAction(qt.QWidgetAction): - """Action providing few filters to the image""" - - sigAggregationModeChanged = qt.Signal() - - def __init__(self, parent): - qt.QWidgetAction.__init__(self, parent) - - toolButton = qt.QToolButton(parent) - - filterAction = qt.QAction(self) - filterAction.setText("No filter") - filterAction.setCheckable(True) - filterAction.setChecked(True) - filterAction.setProperty( - "aggregation", items.ImageDataAggregated.Aggregation.NONE - ) - densityNoFilterAction = filterAction - - filterAction = qt.QAction(self) - filterAction.setText("Max filter") - filterAction.setCheckable(True) - filterAction.setProperty( - "aggregation", items.ImageDataAggregated.Aggregation.MAX - ) - densityMaxFilterAction = filterAction - - filterAction = qt.QAction(self) - filterAction.setText("Mean filter") - filterAction.setCheckable(True) - filterAction.setProperty( - "aggregation", items.ImageDataAggregated.Aggregation.MEAN - ) - densityMeanFilterAction = filterAction - - filterAction = qt.QAction(self) - filterAction.setText("Min filter") - filterAction.setCheckable(True) - filterAction.setProperty( - "aggregation", items.ImageDataAggregated.Aggregation.MIN - ) - densityMinFilterAction = filterAction - - densityGroup = qt.QActionGroup(self) - densityGroup.setExclusive(True) - densityGroup.addAction(densityNoFilterAction) - densityGroup.addAction(densityMaxFilterAction) - densityGroup.addAction(densityMeanFilterAction) - densityGroup.addAction(densityMinFilterAction) - densityGroup.triggered.connect(self._aggregationModeChanged) - self.__densityGroup = densityGroup - - filterMenu = qt.QMenu(toolButton) - filterMenu.addAction(densityNoFilterAction) - filterMenu.addAction(densityMaxFilterAction) - filterMenu.addAction(densityMeanFilterAction) - filterMenu.addAction(densityMinFilterAction) - - toolButton.setPopupMode(qt.QToolButton.InstantPopup) - toolButton.setMenu(filterMenu) - toolButton.setText("Data filters") - toolButton.setToolTip("Enable/disable filter on the image") - icon = icons.getQIcon("aggregation-mode") - toolButton.setIcon(icon) - toolButton.setText("Pixel aggregation filter") - - self.setDefaultWidget(toolButton) - - def _aggregationModeChanged(self): - self.sigAggregationModeChanged.emit() - - def setAggregationMode(self, mode): - """Set an Aggregated enum from ImageDataAggregated""" - for a in self.__densityGroup.actions(): - if a.property("aggregation") is mode: - a.setChecked(True) - - def getAggregationMode(self): - """Returns an Aggregated enum from ImageDataAggregated""" - densityAction = self.__densityGroup.checkedAction() - if densityAction is None: - return items.ImageDataAggregated.Aggregation.NONE - return densityAction.property("aggregation") - class ImageView(PlotWindow): """Display a single image with horizontal and vertical histograms. diff --git a/src/silx/gui/plot/StackView.py b/src/silx/gui/plot/StackView.py index 85bc50f944..86a2dbe938 100644 --- a/src/silx/gui/plot/StackView.py +++ b/src/silx/gui/plot/StackView.py @@ -86,6 +86,8 @@ from silx._utils import NP_OPTIONAL_COPY from silx.gui.plot.actions import io as silx_io +from silx.gui.plot.items.image_aggregated import ImageDataAggregated +from silx.gui.plot.actions.image import AggregationModeAction from silx.io.nxdata import save_NXdata from silx.utils.array_like import DatasetView, ListOfImages from silx.math import calibration @@ -289,6 +291,18 @@ def __init__( self.__planeSelection.sigPlaneSelectionChanged.connect( self._profileToolBar.clearProfile ) + + self.__aggregationModeAction = AggregationModeAction(parent=self) + self._plot.toolBar().addAction(self.__aggregationModeAction) + self.__aggregationModeAction.sigAggregationModeChanged.connect(self._aggregationModeChanged) + + def getAggregationModeAction(self) -> AggregationModeAction: + """Action toggling the aggregation mode action + """ + return self.__aggregationModeAction + + def _aggregationModeChanged(self): + self._stackItem.setAggregationMode(self.getAggregationModeAction().getAggregationMode()) def _saveImageStack(self, plot, filename, nameFilter): """Save all images from the stack into a volume. diff --git a/src/silx/gui/plot/actions/__init__.py b/src/silx/gui/plot/actions/__init__.py index 3e606c6ec3..fd3072e6ed 100644 --- a/src/silx/gui/plot/actions/__init__.py +++ b/src/silx/gui/plot/actions/__init__.py @@ -39,3 +39,4 @@ from . import control from . import mode from . import io +from . import image diff --git a/src/silx/gui/plot/actions/image.py b/src/silx/gui/plot/actions/image.py new file mode 100644 index 0000000000..387f117a64 --- /dev/null +++ b/src/silx/gui/plot/actions/image.py @@ -0,0 +1,127 @@ +# /*########################################################################## +# +# Copyright (c) 2024 European Synchrotron Radiation Facility +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +# ###########################################################################*/ +""" +:mod:`silx.gui.plot.actions.image` provides a set of QAction relative to data processing +and outputs for a :class:`.PlotWidget`. + +The following QAction are available: + +- :class:`AggregationModeAction` +""" + +__authors__ = ["T. Vincent"] +__license__ = "MIT" +__date__ = "19/09/2024" + +import logging +from silx.gui import qt +from silx.gui import icons +from ..items.image_aggregated import ImageDataAggregated + +_logger = logging.getLogger(__name__) + +class AggregationModeAction(qt.QWidgetAction): + """Action providing filters for an aggregated image""" + + sigAggregationModeChanged = qt.Signal() + """Signal emitted when the aggregation mode has changed""" + + def __init__(self, parent): + qt.QWidgetAction.__init__(self, parent) + + toolButton = qt.QToolButton(parent) + + filterAction = qt.QAction(self) + filterAction.setText("No filter") + filterAction.setCheckable(True) + filterAction.setChecked(True) + filterAction.setProperty( + "aggregation", ImageDataAggregated.Aggregation.NONE + ) + densityNoFilterAction = filterAction + + filterAction = qt.QAction(self) + filterAction.setText("Max filter") + filterAction.setCheckable(True) + filterAction.setProperty( + "aggregation", ImageDataAggregated.Aggregation.MAX + ) + densityMaxFilterAction = filterAction + + filterAction = qt.QAction(self) + filterAction.setText("Mean filter") + filterAction.setCheckable(True) + filterAction.setProperty( + "aggregation", ImageDataAggregated.Aggregation.MEAN + ) + densityMeanFilterAction = filterAction + + filterAction = qt.QAction(self) + filterAction.setText("Min filter") + filterAction.setCheckable(True) + filterAction.setProperty( + "aggregation", ImageDataAggregated.Aggregation.MIN + ) + densityMinFilterAction = filterAction + + densityGroup = qt.QActionGroup(self) + densityGroup.setExclusive(True) + densityGroup.addAction(densityNoFilterAction) + densityGroup.addAction(densityMaxFilterAction) + densityGroup.addAction(densityMeanFilterAction) + densityGroup.addAction(densityMinFilterAction) + densityGroup.triggered.connect(self._aggregationModeChanged) + self.__densityGroup = densityGroup + + filterMenu = qt.QMenu(toolButton) + filterMenu.addAction(densityNoFilterAction) + filterMenu.addAction(densityMaxFilterAction) + filterMenu.addAction(densityMeanFilterAction) + filterMenu.addAction(densityMinFilterAction) + + toolButton.setPopupMode(qt.QToolButton.InstantPopup) + toolButton.setMenu(filterMenu) + toolButton.setText("Data filters") + toolButton.setToolTip("Enable/disable filter on the image") + icon = icons.getQIcon("aggregation-mode") + toolButton.setIcon(icon) + toolButton.setText("Pixel aggregation filter") + + self.setDefaultWidget(toolButton) + + def _aggregationModeChanged(self): + self.sigAggregationModeChanged.emit() + + def setAggregationMode(self, mode): + """Set an Aggregated enum from ImageDataAggregated""" + for a in self.__densityGroup.actions(): + if a.property("aggregation") is mode: + a.setChecked(True) + + def getAggregationMode(self): + """Returns an Aggregated enum from ImageDataAggregated""" + densityAction = self.__densityGroup.checkedAction() + if densityAction is None: + return ImageDataAggregated.Aggregation.NONE + return densityAction.property("aggregation") diff --git a/src/silx/gui/plot/items/image.py b/src/silx/gui/plot/items/image.py index 4046b65630..72fe1796fd 100644 --- a/src/silx/gui/plot/items/image.py +++ b/src/silx/gui/plot/items/image.py @@ -1,4 +1,4 @@ -# /*########################################################################## +# /*######################################################################### # # Copyright (c) 2017-2023 European Synchrotron Radiation Facility # @@ -20,7 +20,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # -# ###########################################################################*/ +# ##########################################################################*/ """This module provides the :class:`ImageData` and :class:`ImageRgba` items of the :class:`Plot`. """ @@ -595,8 +595,9 @@ class MaskImageData(ImageData): pass +from .image_aggregated import ImageDataAggregated -class ImageStack(ImageData): +class ImageStack(ImageDataAggregated): """Item to store a stack of images and to show it in the plot as one of the images of the stack. @@ -605,7 +606,7 @@ class ImageStack(ImageData): """ def __init__(self): - ImageData.__init__(self) + ImageDataAggregated.__init__(self) self.__stack = None """A 3D numpy array (or a mimic one, see ListOfImages)""" self.__stackPosition = None