summaryrefslogtreecommitdiff
path: root/silx/gui/plot/ComplexImageView.py
diff options
context:
space:
mode:
Diffstat (limited to 'silx/gui/plot/ComplexImageView.py')
-rw-r--r--silx/gui/plot/ComplexImageView.py314
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.