# coding: utf-8 # /*########################################################################## # # Copyright (c) 2017-2020 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. # # ###########################################################################*/ """This module provides a widget to view 2D complex data. The :class:`ComplexImageView` widget is dedicated to visualize a single 2D dataset of complex data. """ from __future__ import absolute_import __authors__ = ["Vincent Favre-Nicolin", "T. Vincent"] __license__ = "MIT" __date__ = "24/04/2018" import logging import collections import numpy from ...utils.deprecation import deprecated from .. import qt, icons from .PlotWindow import Plot2D from . import items from .items import ImageComplexData from silx.gui.widgets.FloatEdit import FloatEdit _logger = logging.getLogger(__name__) # Widgets class _AmplitudeRangeDialog(qt.QDialog): """QDialog asking for the amplitude range to display.""" sigRangeChanged = qt.Signal(tuple) """Signal emitted when the range has changed. It provides the new range as a 2-tuple: (max, delta) """ def __init__(self, parent=None, amplitudeRange=None, displayedRange=(None, 2)): super(_AmplitudeRangeDialog, self).__init__(parent) self.setWindowTitle('Set Displayed Amplitude Range') if amplitudeRange is not None: amplitudeRange = min(amplitudeRange), max(amplitudeRange) self._amplitudeRange = amplitudeRange self._defaultDisplayedRange = displayedRange layout = qt.QFormLayout() self.setLayout(layout) if self._amplitudeRange is not None: min_, max_ = self._amplitudeRange layout.addRow( qt.QLabel('Data Amplitude Range: [%g, %g]' % (min_, max_))) self._maxLineEdit = FloatEdit(parent=self) self._maxLineEdit.validator().setBottom(0.) self._maxLineEdit.setAlignment(qt.Qt.AlignRight) self._maxLineEdit.editingFinished.connect(self._rangeUpdated) layout.addRow('Displayed Max.:', self._maxLineEdit) self._autoscale = qt.QCheckBox('autoscale') self._autoscale.toggled.connect(self._autoscaleCheckBoxToggled) layout.addRow('', self._autoscale) self._deltaLineEdit = FloatEdit(parent=self) self._deltaLineEdit.validator().setBottom(1.) self._deltaLineEdit.setAlignment(qt.Qt.AlignRight) self._deltaLineEdit.editingFinished.connect(self._rangeUpdated) layout.addRow('Displayed delta (log10 unit):', self._deltaLineEdit) buttons = qt.QDialogButtonBox(self) buttons.addButton(qt.QDialogButtonBox.Ok) buttons.addButton(qt.QDialogButtonBox.Cancel) buttons.accepted.connect(self.accept) buttons.rejected.connect(self.reject) layout.addRow(buttons) # Set dialog from default values self._resetDialogToDefault() self.rejected.connect(self._handleRejected) def _resetDialogToDefault(self): """Set Widgets of the dialog from range information """ max_, delta = self._defaultDisplayedRange if max_ is not None: # Not in autoscale displayedMax = max_ elif self._amplitudeRange is not None: # Autoscale with data displayedMax = self._amplitudeRange[1] else: # Autoscale without data displayedMax = '' if displayedMax == "": self._maxLineEdit.setText("") else: self._maxLineEdit.setValue(displayedMax) self._maxLineEdit.setEnabled(max_ is not None) self._deltaLineEdit.setValue(delta) self._autoscale.setChecked(self._defaultDisplayedRange[0] is None) def getRangeInfo(self): """Returns the current range as a 2-tuple (max, delta (in log10))""" if self._autoscale.isChecked(): max_ = None else: maxStr = self._maxLineEdit.text() max_ = self._maxLineEdit.value() if maxStr else None return max_, self._deltaLineEdit.value() if self._deltaLineEdit.text() else 2 def _handleRejected(self): """Reset range info to default when rejected""" self._resetDialogToDefault() self._rangeUpdated() def _rangeUpdated(self): """Handle QLineEdit editing finised""" self.sigRangeChanged.emit(self.getRangeInfo()) def _autoscaleCheckBoxToggled(self, checked): """Handle autoscale checkbox state changes""" if checked: # Use default values if self._amplitudeRange is None: max_ = '' else: max_ = self._amplitudeRange[1] if max_ == "": self._maxLineEdit.setText("") else: self._maxLineEdit.setValue(max_) self._maxLineEdit.setEnabled(not checked) self._rangeUpdated() class _ComplexDataToolButton(qt.QToolButton): """QToolButton providing choices of complex data visualization modes :param parent: See :class:`QToolButton` :param plot: The :class:`ComplexImageView` to control """ _MODES = collections.OrderedDict([ (ImageComplexData.ComplexMode.ABSOLUTE, ('math-amplitude', 'Amplitude')), (ImageComplexData.ComplexMode.SQUARE_AMPLITUDE, ('math-square-amplitude', 'Square amplitude')), (ImageComplexData.ComplexMode.PHASE, ('math-phase', 'Phase')), (ImageComplexData.ComplexMode.REAL, ('math-real', 'Real part')), (ImageComplexData.ComplexMode.IMAGINARY, ('math-imaginary', 'Imaginary part')), (ImageComplexData.ComplexMode.AMPLITUDE_PHASE, ('math-phase-color', 'Amplitude and Phase')), (ImageComplexData.ComplexMode.LOG10_AMPLITUDE_PHASE, ('math-phase-color-log', 'Log10(Amp.) and Phase')) ]) _RANGE_DIALOG_TEXT = 'Set Amplitude Range...' def __init__(self, parent=None, plot=None): super(_ComplexDataToolButton, self).__init__(parent=parent) assert plot is not None self._plot2DComplex = plot menu = qt.QMenu(self) menu.triggered.connect(self._triggered) self.setMenu(menu) for mode, info in self._MODES.items(): icon, text = info action = qt.QAction(icons.getQIcon(icon), text, self) action.setData(mode) action.setIconVisibleInMenu(True) menu.addAction(action) self._rangeDialogAction = qt.QAction(self) self._rangeDialogAction.setText(self._RANGE_DIALOG_TEXT) menu.addAction(self._rangeDialogAction) self.setPopupMode(qt.QToolButton.InstantPopup) self._modeChanged(self._plot2DComplex.getComplexMode()) self._plot2DComplex.sigVisualizationModeChanged.connect( self._modeChanged) def _modeChanged(self, mode): """Handle change of visualization modes""" icon, text = self._MODES[mode] self.setIcon(icons.getQIcon(icon)) self.setToolTip('Display the ' + text.lower()) self._rangeDialogAction.setEnabled( mode == ImageComplexData.ComplexMode.LOG10_AMPLITUDE_PHASE) def _triggered(self, action): """Handle triggering of menu actions""" actionText = action.text() if actionText == self._RANGE_DIALOG_TEXT: # Show dialog # Get amplitude range data = self._plot2DComplex.getData(copy=False) if data.size > 0: absolute = numpy.absolute(data) dataRange = (numpy.nanmin(absolute), numpy.nanmax(absolute)) else: dataRange = None # Show dialog dialog = _AmplitudeRangeDialog( parent=self, amplitudeRange=dataRange, displayedRange=self._plot2DComplex._getAmplitudeRangeInfo()) dialog.sigRangeChanged.connect(self._rangeChanged) dialog.exec_() dialog.sigRangeChanged.disconnect(self._rangeChanged) else: # update mode mode = action.data() if isinstance(mode, ImageComplexData.ComplexMode): self._plot2DComplex.setComplexMode(mode) def _rangeChanged(self, range_): """Handle updates of range in the dialog""" self._plot2DComplex._setAmplitudeRangeInfo(*range_) class ComplexImageView(qt.QWidget): """Display an image of complex data and allow to choose the visualization. :param parent: See :class:`QMainWindow` """ ComplexMode = ImageComplexData.ComplexMode """Complex Modes enumeration""" sigDataChanged = qt.Signal() """Signal emitted when data has changed.""" sigVisualizationModeChanged = qt.Signal(object) """Signal emitted when the visualization mode has changed. It provides the new visualization mode. """ def __init__(self, parent=None): super(ComplexImageView, self).__init__(parent) if parent is None: self.setWindowTitle('ComplexImageView') self._plot2D = Plot2D(self) layout = qt.QHBoxLayout(self) layout.setSpacing(0) layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(self._plot2D) self.setLayout(layout) # Create and add image to the plot self._plotImage = ImageComplexData() self._plotImage.setName('__ComplexImageView__complex_image__') self._plotImage.sigItemChanged.connect(self._itemChanged) self._plot2D.addItem(self._plotImage) self._plot2D.setActiveImage(self._plotImage.getName()) toolBar = qt.QToolBar('Complex', self) toolBar.addWidget( _ComplexDataToolButton(parent=self, plot=self)) self._plot2D.insertToolBar(self._plot2D.getProfileToolbar(), toolBar) def _itemChanged(self, event): """Handle item changed signal""" if event is items.ItemChangedType.DATA: self.sigDataChanged.emit() elif event is items.ItemChangedType.VISUALIZATION_MODE: mode = self.getComplexMode() self.sigVisualizationModeChanged.emit(mode) def getPlot(self): """Return the PlotWidget displaying the data""" return self._plot2D def setData(self, data=None, copy=True): """Set the complex data to display. :param numpy.ndarray data: 2D complex data :param bool copy: True (default) to copy the data, False to use provided data (do not modify!). """ if data is None: data = numpy.zeros((0, 0), dtype=numpy.complex64) previousData = self._plotImage.getComplexData(copy=False) self._plotImage.setData(data, copy=copy) if previousData.shape != data.shape: self.getPlot().resetZoom() def getData(self, copy=True): """Get the currently displayed complex data. :param bool copy: True (default) to return a copy of the data, False to return internal data (do not modify!). :return: The complex data array. :rtype: numpy.ndarray of complex with 2 dimensions """ return self._plotImage.getComplexData(copy=copy) def getDisplayedData(self, copy=True): """Returns the displayed data depending on the visualization mode WARNING: The returned data can be a uint8 RGBA image :param bool copy: True (default) to return a copy of the data, False to return internal data (do not modify!) :rtype: numpy.ndarray of float with 2 dims or RGBA image (uint8). """ mode = self.getComplexMode() if mode in (self.ComplexMode.AMPLITUDE_PHASE, self.ComplexMode.LOG10_AMPLITUDE_PHASE): return self._plotImage.getRgbaImageData(copy=copy) else: return self._plotImage.getData(copy=copy) # Backward compatibility Mode = ComplexMode @classmethod @deprecated(replacement='supportedComplexModes', since_version='0.11.0') def getSupportedVisualizationModes(cls): return cls.supportedComplexModes() @deprecated(replacement='setComplexMode', since_version='0.11.0') def setVisualizationMode(self, mode): return self.setComplexMode(mode) @deprecated(replacement='getComplexMode', since_version='0.11.0') def getVisualizationMode(self): return self.getComplexMode() # Image item proxy @staticmethod def supportedComplexModes(): """Returns the supported visualization modes. Supported visualization modes are: - amplitude: The absolute value provided by numpy.absolute - phase: The phase (or argument) provided by numpy.angle - real: Real part - imaginary: Imaginary part - amplitude_phase: Color-coded phase with amplitude as alpha. - log10_amplitude_phase: Color-coded phase with log10(amplitude) as alpha. :rtype: List[ComplexMode] """ return ImageComplexData.supportedComplexModes() def setComplexMode(self, mode): """Set the mode of visualization of the complex data. See :meth:`supportedComplexModes` for the list of supported modes. How-to change visualization mode:: widget = ComplexImageView() widget.setComplexMode(ComplexImageView.ComplexMode.PHASE) # or widget.setComplexMode('phase') :param Unions[ComplexMode,str] mode: The mode to use. """ self._plotImage.setComplexMode(mode) def getComplexMode(self): """Get the current visualization mode of the complex data. :rtype: ComplexMode """ return self._plotImage.getComplexMode() def _setAmplitudeRangeInfo(self, max_=None, delta=2): """Set the amplitude range to display for 'log10_amplitude_phase' mode. :param max_: Max of the amplitude range. If None it autoscales to data max. :param float delta: Delta range in log10 to display """ self._plotImage._setAmplitudeRangeInfo(max_, delta) def _getAmplitudeRangeInfo(self): """Returns the amplitude range to use for 'log10_amplitude_phase' mode. :return: (max, delta), if max is None, then it autoscales to data max :rtype: 2-tuple""" return self._plotImage._getAmplitudeRangeInfo() def setColormap(self, colormap, mode=None): """Set the colormap to use for amplitude, phase, real or imaginary. WARNING: This colormap is not used when displaying both amplitude and phase. :param ~silx.gui.colors.Colormap colormap: The colormap :param ComplexMode mode: If specified, set the colormap of this specific mode """ self._plotImage.setColormap(colormap, mode) def getColormap(self, mode=None): """Returns the colormap used to display the data. :param ComplexMode mode: If specified, set the colormap of this specific mode :rtype: ~silx.gui.colors.Colormap """ return self._plotImage.getColormap(mode=mode) def getOrigin(self): """Returns the offset from origin at which to display the image. :rtype: 2-tuple of float """ return self._plotImage.getOrigin() def setOrigin(self, origin): """Set the offset from origin at which to display the image. :param origin: (ox, oy) Offset from origin :type origin: float or 2-tuple of float """ self._plotImage.setOrigin(origin) def getScale(self): """Returns the scale of the image in data coordinates. :rtype: 2-tuple of float """ return self._plotImage.getScale() def setScale(self, scale): """Set the scale of the image :param scale: (sx, sy) Scale of the image :type scale: float or 2-tuple of float """ self._plotImage.setScale(scale) # PlotWidget API proxy def getXAxis(self): """Returns the X axis :rtype: :class:`.items.Axis` """ return self.getPlot().getXAxis() def getYAxis(self): """Returns an Y axis :rtype: :class:`.items.Axis` """ return self.getPlot().getYAxis(axis='left') def getGraphTitle(self): """Return the plot main title as a str.""" return self.getPlot().getGraphTitle() def setGraphTitle(self, title=""): """Set the plot main title. :param str title: Main title of the plot (default: '') """ self.getPlot().setGraphTitle(title) def setKeepDataAspectRatio(self, flag): """Set whether the plot keeps data aspect ratio or not. :param bool flag: True to respect data aspect ratio """ self.getPlot().setKeepDataAspectRatio(flag) def isKeepDataAspectRatio(self): """Returns whether the plot is keeping data aspect ratio or not.""" return self.getPlot().isKeepDataAspectRatio()