diff options
Diffstat (limited to 'silx/gui/plot/ComplexImageView.py')
-rw-r--r-- | silx/gui/plot/ComplexImageView.py | 314 |
1 files changed, 68 insertions, 246 deletions
diff --git a/silx/gui/plot/ComplexImageView.py b/silx/gui/plot/ComplexImageView.py index 1463293..ebff175 100644 --- a/silx/gui/plot/ComplexImageView.py +++ b/silx/gui/plot/ComplexImageView.py @@ -1,7 +1,7 @@ # coding: utf-8 # /*########################################################################## # -# Copyright (c) 2017 European Synchrotron Radiation Facility +# Copyright (c) 2017-2018 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 @@ -32,144 +32,22 @@ from __future__ import absolute_import __authors__ = ["Vincent Favre-Nicolin", "T. Vincent"] __license__ = "MIT" -__date__ = "02/10/2017" +__date__ = "19/01/2018" import logging +import collections import numpy from .. import qt, icons from .PlotWindow import Plot2D -from .Colormap import Colormap from . import items +from .items import ImageComplexData from silx.gui.widgets.FloatEdit import FloatEdit _logger = logging.getLogger(__name__) -_PHASE_COLORMAP = Colormap( - name='hsv', - vmin=-numpy.pi, - vmax=numpy.pi) -"""Colormap to use for phase""" - -# Complex colormap functions - -def _phase2rgb(data): - """Creates RGBA image with colour-coded phase. - - :param numpy.ndarray data: The data to convert - :return: Array of RGBA colors - :rtype: numpy.ndarray - """ - if data.size == 0: - return numpy.zeros((0, 0, 4), dtype=numpy.uint8) - - phase = numpy.angle(data) - return _PHASE_COLORMAP.applyToData(phase) - - -def _complex2rgbalog(data, amin=0., dlogs=2, smax=None): - """Returns RGBA colors: colour-coded phases and log10(amplitude) in alpha. - - :param numpy.ndarray data: the complex data array to convert to RGBA - :param float amin: the minimum value for the alpha channel - :param float dlogs: amplitude range displayed, in log10 units - :param float smax: - if specified, all values above max will be displayed with an alpha=1 - """ - if data.size == 0: - return numpy.zeros((0, 0, 4), dtype=numpy.uint8) - - rgba = _phase2rgb(data) - sabs = numpy.absolute(data) - if smax is not None: - sabs[sabs > smax] = smax - a = numpy.log10(sabs + 1e-20) - a -= a.max() - dlogs # display dlogs orders of magnitude - rgba[..., 3] = 255 * (amin + a / dlogs * (1 - amin) * (a > 0)) - return rgba - - -def _complex2rgbalin(data, gamma=1.0, smax=None): - """Returns RGBA colors: colour-coded phase and linear amplitude in alpha. - - :param numpy.ndarray data: - :param float gamma: Optional exponent gamma applied to the amplitude - :param float smax: - """ - if data.size == 0: - return numpy.zeros((0, 0, 4), dtype=numpy.uint8) - - rgba = _phase2rgb(data) - a = numpy.absolute(data) - if smax is not None: - a[a > smax] = smax - a /= a.max() - rgba[..., 3] = 255 * a**gamma - return rgba - - -# Dedicated plot item - -class _ImageComplexData(items.ImageData): - """Specific plot item to force colormap when using complex colormap. - - This is returning the specific colormap when displaying - colored phase + amplitude. - """ - - def __init__(self): - super(_ImageComplexData, self).__init__() - self._readOnlyColormap = False - self._mode = 'absolute' - self._colormaps = { # Default colormaps for all modes - 'absolute': Colormap(), - 'phase': _PHASE_COLORMAP.copy(), - 'real': Colormap(), - 'imaginary': Colormap(), - 'amplitude_phase': _PHASE_COLORMAP.copy(), - 'log10_amplitude_phase': _PHASE_COLORMAP.copy(), - } - - _READ_ONLY_MODES = 'amplitude_phase', 'log10_amplitude_phase' - """Modes that requires a read-only colormap.""" - - def setVisualizationMode(self, mode): - """Set the visualization mode to use. - - :param str mode: - """ - mode = str(mode) - assert mode in self._colormaps - - if mode != self._mode: - # Save current colormap - self._colormaps[self._mode] = self.getColormap() - self._mode = mode - - # Set colormap for new mode - self.setColormap(self._colormaps[mode]) - - def getVisualizationMode(self): - """Returns the visualization mode in use.""" - return self._mode - - def _isReadOnlyColormap(self): - """Returns True if colormap should not be modified.""" - return self.getVisualizationMode() in self._READ_ONLY_MODES - - def setColormap(self, colormap): - if not self._isReadOnlyColormap(): - super(_ImageComplexData, self).setColormap(colormap) - - def getColormap(self): - if self._isReadOnlyColormap(): - return _PHASE_COLORMAP.copy() - else: - return super(_ImageComplexData, self).getColormap() - - # Widgets class _AmplitudeRangeDialog(qt.QDialog): @@ -291,13 +169,19 @@ class _ComplexDataToolButton(qt.QToolButton): :param plot: The :class:`ComplexImageView` to control """ - _MODES = [ - ('absolute', 'math-amplitude', 'Amplitude'), - ('phase', 'math-phase', 'Phase'), - ('real', 'math-real', 'Real part'), - ('imaginary', 'math-imaginary', 'Imaginary part'), - ('amplitude_phase', 'math-phase-color', 'Amplitude and Phase'), - ('log10_amplitude_phase', 'math-phase-color-log', 'Log10(Amp.) and Phase')] + _MODES = collections.OrderedDict([ + (ImageComplexData.Mode.ABSOLUTE, ('math-amplitude', 'Amplitude')), + (ImageComplexData.Mode.SQUARE_AMPLITUDE, + ('math-square-amplitude', 'Square amplitude')), + (ImageComplexData.Mode.PHASE, ('math-phase', 'Phase')), + (ImageComplexData.Mode.REAL, ('math-real', 'Real part')), + (ImageComplexData.Mode.IMAGINARY, + ('math-imaginary', 'Imaginary part')), + (ImageComplexData.Mode.AMPLITUDE_PHASE, + ('math-phase-color', 'Amplitude and Phase')), + (ImageComplexData.Mode.LOG10_AMPLITUDE_PHASE, + ('math-phase-color-log', 'Log10(Amp.) and Phase')) + ]) _RANGE_DIALOG_TEXT = 'Set Amplitude Range...' @@ -311,8 +195,10 @@ class _ComplexDataToolButton(qt.QToolButton): menu.triggered.connect(self._triggered) self.setMenu(menu) - for _, icon, text in self._MODES: + 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) @@ -328,13 +214,10 @@ class _ComplexDataToolButton(qt.QToolButton): def _modeChanged(self, mode): """Handle change of visualization modes""" - for actionMode, icon, text in self._MODES: - if actionMode == mode: - self.setIcon(icons.getQIcon(icon)) - self.setToolTip('Display the ' + text.lower()) - break - - self._rangeDialogAction.setEnabled(mode == 'log10_amplitude_phase') + icon, text = self._MODES[mode] + self.setIcon(icons.getQIcon(icon)) + self.setToolTip('Display the ' + text.lower()) + self._rangeDialogAction.setEnabled(mode == ImageComplexData.Mode.LOG10_AMPLITUDE_PHASE) def _triggered(self, action): """Handle triggering of menu actions""" @@ -360,9 +243,9 @@ class _ComplexDataToolButton(qt.QToolButton): dialog.sigRangeChanged.disconnect(self._rangeChanged) else: # update mode - for mode, _, text in self._MODES: - if actionText == text: - self._plot2DComplex.setVisualizationMode(mode) + mode = action.data() + if isinstance(mode, ImageComplexData.Mode): + self._plot2DComplex.setVisualizationMode(mode) def _rangeChanged(self, range_): """Handle updates of range in the dialog""" @@ -375,10 +258,13 @@ class ComplexImageView(qt.QWidget): :param parent: See :class:`QMainWindow` """ + Mode = ImageComplexData.Mode + """Also expose the modes inside the class""" + sigDataChanged = qt.Signal() """Signal emitted when data has changed.""" - sigVisualizationModeChanged = qt.Signal(str) + sigVisualizationModeChanged = qt.Signal(object) """Signal emitted when the visualization mode has changed. It provides the new visualization mode. @@ -389,11 +275,6 @@ class ComplexImageView(qt.QWidget): if parent is None: self.setWindowTitle('ComplexImageView') - self._mode = 'absolute' - self._amplitudeRangeInfo = None, 2 - self._data = numpy.zeros((0, 0), dtype=numpy.complex) - self._displayedData = numpy.zeros((0, 0), dtype=numpy.float) - self._plot2D = Plot2D(self) layout = qt.QHBoxLayout(self) @@ -403,10 +284,9 @@ class ComplexImageView(qt.QWidget): self.setLayout(layout) # Create and add image to the plot - self._plotImage = _ImageComplexData() + self._plotImage = ImageComplexData() self._plotImage._setLegend('__ComplexImageView__complex_image__') - self._plotImage.setData(self._displayedData) - self._plotImage.setVisualizationMode(self._mode) + self._plotImage.sigItemChanged.connect(self._itemChanged) self._plot2D._add(self._plotImage) self._plot2D.setActiveImage(self._plotImage.getLegend()) @@ -416,57 +296,18 @@ class ComplexImageView(qt.QWidget): 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.getVisualizationMode() + self.sigVisualizationModeChanged.emit(mode) + def getPlot(self): """Return the PlotWidget displaying the data""" return self._plot2D - def _convertData(self, data, mode): - """Convert complex data according to provided mode. - - :param numpy.ndarray data: The complex data to convert - :param str mode: The visualization mode - :return: The data corresponding to the mode - :rtype: 2D numpy.ndarray of float or RGBA image - """ - if mode == 'absolute': - return numpy.absolute(data) - elif mode == 'phase': - return numpy.angle(data) - elif mode == 'real': - return numpy.real(data) - elif mode == 'imaginary': - return numpy.imag(data) - elif mode == 'amplitude_phase': - return _complex2rgbalin(data) - elif mode == 'log10_amplitude_phase': - max_, delta = self._getAmplitudeRangeInfo() - return _complex2rgbalog(data, dlogs=delta, smax=max_) - else: - _logger.error( - 'Unsupported conversion mode: %s, fallback to absolute', - str(mode)) - return numpy.absolute(data) - - def _updatePlot(self): - """Update the image in the plot""" - - mode = self.getVisualizationMode() - - self.getPlot().getColormapAction().setDisabled( - mode in ('amplitude_phase', 'log10_amplitude_phase')) - - self._plotImage.setVisualizationMode(mode) - - image = self.getDisplayedData(copy=False) - if mode in ('amplitude_phase', 'log10_amplitude_phase'): - # Combined view - absolute = numpy.absolute(self.getData(copy=False)) - self._plotImage.setData( - absolute, alternative=image, copy=False) - else: - self._plotImage.setData( - image, alternative=None, copy=False) - def setData(self, data=None, copy=True): """Set the complex data to display. @@ -476,22 +317,13 @@ class ComplexImageView(qt.QWidget): """ if data is None: data = numpy.zeros((0, 0), dtype=numpy.complex) - else: - data = numpy.array(data, copy=copy) - - assert data.ndim == 2 - if data.dtype.kind != 'c': # Convert to complex - data = numpy.array(data, dtype=numpy.complex) - shape_changed = (self._data.shape != data.shape) - self._data = data - self._displayedData = self._convertData( - data, self.getVisualizationMode()) - - self._updatePlot() - if shape_changed: - self.getPlot().resetZoom() - self.sigDataChanged.emit() + 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. @@ -501,7 +333,7 @@ class ComplexImageView(qt.QWidget): :return: The complex data array. :rtype: numpy.ndarray of complex with 2 dimensions """ - return numpy.array(self._data, copy=copy) + return self._plotImage.getComplexData(copy=copy) def getDisplayedData(self, copy=True): """Returns the displayed data depending on the visualization mode @@ -512,7 +344,12 @@ class ComplexImageView(qt.QWidget): False to return internal data (do not modify!) :rtype: numpy.ndarray of float with 2 dims or RGBA image (uint8). """ - return numpy.array(self._displayedData, copy=copy) + mode = self.getVisualizationMode() + if mode in (self.Mode.AMPLITUDE_PHASE, + self.Mode.LOG10_AMPLITUDE_PHASE): + return self._plotImage.getRgbaImageData(copy=copy) + else: + return self._plotImage.getData(copy=copy) @staticmethod def getSupportedVisualizationModes(): @@ -530,12 +367,7 @@ class ComplexImageView(qt.QWidget): :rtype: tuple of str """ - return ('absolute', - 'phase', - 'real', - 'imaginary', - 'amplitude_phase', - 'log10_amplitude_phase') + return tuple(ImageComplexData.Mode) def setVisualizationMode(self, mode): """Set the mode of visualization of the complex data. @@ -545,20 +377,14 @@ class ComplexImageView(qt.QWidget): :param str mode: The mode to use. """ - assert mode in self.getSupportedVisualizationModes() - if mode != self._mode: - self._mode = mode - self._displayedData = self._convertData( - self.getData(copy=False), mode) - self._updatePlot() - self.sigVisualizationModeChanged.emit(mode) + self._plotImage.setVisualizationMode(mode) def getVisualizationMode(self): """Get the current visualization mode of the complex data. - :rtype: str + :rtype: Mode """ - return self._mode + return self._plotImage.getVisualizationMode() def _setAmplitudeRangeInfo(self, max_=None, delta=2): """Set the amplitude range to display for 'log10_amplitude_phase' mode. @@ -567,39 +393,35 @@ class ComplexImageView(qt.QWidget): If None it autoscales to data max. :param float delta: Delta range in log10 to display """ - self._amplitudeRangeInfo = max_, float(delta) - mode = self.getVisualizationMode() - if mode == 'log10_amplitude_phase': - self._displayedData = self._convertData( - self.getData(copy=False), mode) - self._updatePlot() + 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._amplitudeRangeInfo + return self._plotImage._getAmplitudeRangeInfo() # Image item proxy - def setColormap(self, colormap): + 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 Colormap colormap: The colormap + :param ~silx.gui.plot.Colormap.Colormap colormap: The colormap + :param Mode mode: If specified, set the colormap of this specific mode """ - self._plotImage.setColormap(colormap) + self._plotImage.setColormap(colormap, mode) - def getColormap(self): + def getColormap(self, mode=None): """Returns the colormap used to display the data. - :rtype: Colormap + :param Mode mode: If specified, set the colormap of this specific mode + :rtype: ~silx.gui.plot.Colormap.Colormap """ - # Returns internal colormap and bypass forcing colormap - return items.ImageData.getColormap(self._plotImage) + return self._plotImage.getColormap(mode=mode) def getOrigin(self): """Returns the offset from origin at which to display the image. |