summaryrefslogtreecommitdiff
path: root/silx/gui/plot/StackView.py
diff options
context:
space:
mode:
Diffstat (limited to 'silx/gui/plot/StackView.py')
-rw-r--r--silx/gui/plot/StackView.py479
1 files changed, 304 insertions, 175 deletions
diff --git a/silx/gui/plot/StackView.py b/silx/gui/plot/StackView.py
index 9bb0cf0..938447b 100644
--- a/silx/gui/plot/StackView.py
+++ b/silx/gui/plot/StackView.py
@@ -69,18 +69,14 @@ Example::
__authors__ = ["P. Knobel", "H. Payno"]
__license__ = "MIT"
-__date__ = "20/01/2017"
+__date__ = "11/09/2017"
import numpy
-try:
- import h5py
-except ImportError:
- h5py = None
-
from silx.gui import qt
from .. import icons
-from . import items, PlotWindow, PlotActions
+from . import items, PlotWindow, actions
+from .Colormap import Colormap
from .Colors import cursorColorForColormap
from .PlotTools import LimitsToolBar
from .Profile import Profile3DToolBar
@@ -88,6 +84,16 @@ from ..widgets.FrameBrowser import HorizontalSliderWithBrowser
from silx.utils.array_like import DatasetView, ListOfImages
from silx.math import calibration
+from silx.utils.deprecation import deprecated_warning
+
+try:
+ import h5py
+except ImportError:
+ def is_dataset(obj):
+ return False
+ h5py = None
+else:
+ from silx.io.utils import is_dataset
class StackView(qt.QMainWindow):
@@ -100,7 +106,7 @@ class StackView(qt.QMainWindow):
:param QWidget parent: the Qt parent, or None
:param backend: The backend to use for the plot (default: matplotlib).
- See :class:`.Plot` for the list of supported backend.
+ See :class:`.PlotWidget` for the list of supported backend.
:type backend: str or :class:`BackendBase.BackendBase`
:param bool resetzoom: Toggle visibility of reset zoom action.
:param bool autoScale: Toggle visibility of axes autoscale actions.
@@ -181,6 +187,10 @@ class StackView(qt.QMainWindow):
self._first_stack_dimension = 0
"""Used for dimension labels and combobox"""
+ self._titleCallback = self._defaultTitleCallback
+ """Function returning the plot title based on the frame index.
+ It can be set to a custom function using :meth:`setTitleCallback`"""
+
central_widget = qt.QWidget(self)
self._plot = PlotWindow(parent=central_widget, backend=backend,
@@ -195,11 +205,13 @@ class StackView(qt.QMainWindow):
self.sigActiveImageChanged = self._plot.sigActiveImageChanged
self.sigPlotSignal = self._plot.sigPlotSignal
+ self._addColorBarAction()
+
self._plot.profile = Profile3DToolBar(parent=self._plot,
- plot=self)
+ stackview=self)
self._plot.addToolBar(self._plot.profile)
- self._plot.setGraphXLabel('Columns')
- self._plot.setGraphYLabel('Rows')
+ self._plot.getXAxis().setLabel('Columns')
+ self._plot.getYAxis().setLabel('Rows')
self._plot.sigPlotSignal.connect(self._plotCallback)
self.__planeSelection = PlanesWidget(self._plot)
@@ -227,14 +239,15 @@ class StackView(qt.QMainWindow):
self.__planeSelection.sigPlaneSelectionChanged.connect(
self._plot.profile.clearProfile)
- def setOptionVisible(self, isVisible):
- """
- Set the visibility of the browsing options.
-
- :param bool isVisible: True to have the options visible, else False
- """
- self._browser.setVisible(isVisible)
- self.__planeSelection.setVisible(isVisible)
+ def _addColorBarAction(self):
+ self._plot.getColorBarWidget().setVisible(True)
+ actions = self._plot.toolBar().actions()
+ for index, action in enumerate(actions):
+ if action is self._plot.getColormapAction():
+ break
+ self._plot.toolBar().insertAction(
+ actions[index + 1],
+ self._plot.getColorBarWidget().getToggleViewAction())
def _plotCallback(self, eventDict):
"""Callback for plot events.
@@ -242,7 +255,7 @@ class StackView(qt.QMainWindow):
Emit :attr:`valueChanged` signal, with (x, y, value) tuple of the
cursor location in the plot."""
if eventDict['event'] == 'mouseMoved':
- activeImage = self.getActiveImage()
+ activeImage = self._plot.getActiveImage()
if activeImage is not None:
data = activeImage.getData()
height, width = data.shape
@@ -305,8 +318,7 @@ class StackView(qt.QMainWindow):
if isinstance(self._stack, numpy.ndarray):
self.__transposed_view = self._stack
- elif h5py is not None and isinstance(self._stack, h5py.Dataset) or \
- isinstance(self._stack, DatasetView):
+ elif is_dataset(self._stack) or isinstance(self._stack, DatasetView):
self.__transposed_view = DatasetView(self._stack)
elif isinstance(self._stack, ListOfImages):
@@ -321,13 +333,6 @@ class StackView(qt.QMainWindow):
self._browser.setRange(0, self.__transposed_view.shape[0] - 1)
self._browser.setValue(0)
- def setFrameNumber(self, number):
- """Set the frame selection to a specific value\
-
- :param int number: Number of the frame
- """
- self._browser.setValue(number)
-
def __updateFrameNumber(self, index):
"""Update the current image.
@@ -339,7 +344,7 @@ class StackView(qt.QMainWindow):
scale=self._getImageScale(),
legend=self.__imageLegend,
resetzoom=False, replace=False)
- self._plot.setGraphTitle("Image z=%g" % self._getImageZ(index))
+ self._updateTitle()
def _set3DScaleAndOrigin(self, calibrations):
"""Set scale and origin for all 3 axes, to be used when plotting
@@ -396,7 +401,14 @@ class StackView(qt.QMainWindow):
_xcalib, _ycalib, zcalib = self._getXYZCalibs()
return zcalib(index)
- # public API
+ def _updateTitle(self):
+ frame_idx = self._browser.value()
+ self._plot.setGraphTitle(self._titleCallback(frame_idx))
+
+ def _defaultTitleCallback(self, index):
+ return "Image z=%g" % self._getImageZ(index)
+
+ # public API, stack specific methods
def setStack(self, stack, perspective=0, reset=True, calibrations=None):
"""Set the 3D stack.
@@ -426,7 +438,7 @@ class StackView(qt.QMainWindow):
# stack as list of 2D arrays: must be converted into an array_like
if not isinstance(stack, numpy.ndarray):
- if h5py is None or not isinstance(stack, h5py.Dataset):
+ if not is_dataset(stack):
try:
assert hasattr(stack, "__len__")
for img in stack:
@@ -492,7 +504,7 @@ class StackView(qt.QMainWindow):
:return: 3D stack and parameters.
:rtype: (numpy.ndarray, dict)
"""
- image = self.getActiveImage()
+ image = self._plot.getActiveImage()
if image is None:
return None
@@ -543,7 +555,7 @@ class StackView(qt.QMainWindow):
:return: 3D stack and parameters.
:rtype: (numpy.ndarray, dict)
"""
- image = self.getActiveImage()
+ image = self._plot.getActiveImage()
if image is None:
return None
@@ -567,18 +579,57 @@ class StackView(qt.QMainWindow):
return numpy.array(self.__transposed_view, copy=copy), params
return self.__transposed_view, params
- def getActiveImage(self, just_legend=False):
- """Returns the currently active image object.
+ def setFrameNumber(self, number):
+ """Set the frame selection to a specific value
- It returns None in case of not having an active image.
+ :param int number: Number of the frame
+ """
+ self._browser.setValue(number)
- :param bool just_legend: True to get the legend of the image,
- False (the default) to get the image data and info.
- Note: :class:`StackView` uses the same legend for all frames.
- :return: legend or image object
- :rtype: str or list or None
+ def setFirstStackDimension(self, first_stack_dimension):
+ """When viewing the last 3 dimensions of an n-D array (n>3), you can
+ use this method to change the text in the combobox.
+
+ For instance, for a 7-D array, first stack dim is 4, so the default
+ "Dim1-Dim2" text should be replaced with "Dim5-Dim6" (dimensions
+ numbers are 0-based).
+
+ :param int first_stack_dim: First stack dimension (n-3) when viewing the
+ last 3 dimensions of an n-D array.
"""
- return self._plot.getActiveImage(just_legend=just_legend)
+ old_state = self.__planeSelection.blockSignals(True)
+ self.__planeSelection.setFirstStackDimension(first_stack_dimension)
+ self.__planeSelection.blockSignals(old_state)
+ self._first_stack_dimension = first_stack_dimension
+ self._browser_label.setText("Image index (Dim%d):" % first_stack_dimension)
+
+ def setTitleCallback(self, callback):
+ """Set a user defined function to generate the plot title based on the
+ image/frame index.
+
+ The callback function must accept an integer as a its first positional
+ parameter and must not require any other mandatory parameter.
+ It must return a string.
+
+ To switch back the default behavior, you can pass ``None``::
+
+ mystackview.setTitleCallback(None)
+
+ To have no title, pass a function that returns an empty string::
+
+ mystackview.setTitleCallback(lambda idx: "")
+
+ :param callback: Callback function generating the stack title based
+ on the frame number.
+ """
+
+ if callback is None:
+ self._titleCallback = self._defaultTitleCallback
+ elif callable(callback):
+ self._titleCallback = callback
+ else:
+ raise TypeError("Provided callback is not callable")
+ self._updateTitle()
def clear(self):
"""Clear the widget:
@@ -592,22 +643,6 @@ class StackView(qt.QMainWindow):
self._browser.setEnabled(False)
self._plot.clear()
- def resetZoom(self):
- """Reset the plot limits to the bounds of the data and redraw the plot.
- """
- self._plot.resetZoom()
-
- def getGraphTitle(self):
- """Return the plot main title as a str."""
- return self._plot.getGraphTitle()
-
- def setGraphTitle(self, title=""):
- """Set the plot main title.
-
- :param str title: Main title of the plot (default: '')
- """
- return self._plot.setGraphTitle(title)
-
def setLabels(self, labels=None):
"""Set the labels to be displayed on the plot axes.
@@ -635,55 +670,13 @@ class StackView(qt.QMainWindow):
self.__dimensionsLabels = new_labels
self.__updatePlotLabels()
- def getGraphXLabel(self):
- """Return the current horizontal axis label as a str."""
- return self._plot.getGraphXLabel()
-
- def setGraphXLabel(self, label=None):
- """Set the plot horizontal axis label.
-
- :param str label: The horizontal axis label
- """
- if label is None:
- label = self.__dimensionsLabels[1 if self._perspective == 2 else 2]
- self._plot.setGraphXLabel(label)
-
- def getGraphYLabel(self, axis='left'):
- """Return the current vertical axis label as a str.
-
- :param str axis: The Y axis for which to get the label (left or right)
- """
- return self._plot.getGraphYLabel(axis)
-
- def setGraphYLabel(self, label=None, axis='left'):
- """Set the vertical axis label on the plot.
-
- :param str label: The Y axis label
- :param str axis: The Y axis for which to set the label (left or right)
- """
- if label is None:
- label = self.__dimensionsLabels[1 if self._perspective == 0 else 0]
- self._plot.setGraphYLabel(label, axis)
-
- def setYAxisInverted(self, flag=True):
- """Set the Y axis orientation.
+ def getLabels(self):
+ """Return dimension labels displayed on the plot axes
- :param bool flag: True for Y axis going from top to bottom,
- False for Y axis going from bottom to top
- """
- self._plot.setYAxisInverted(flag)
-
- def isYAxisInverted(self):
- """Return True if Y axis goes from top to bottom, False otherwise."""
- return self._backend.isYAxisInverted()
-
- def getSupportedColormaps(self):
- """Get the supported colormap names as a tuple of str.
-
- The list should at least contain and start by:
- ('gray', 'reversed gray', 'temperature', 'red', 'green', 'blue')
+ :return: List of three strings corresponding to the 3 dimensions
+ of the stack: (name_dim0, name_dim1, name_dim2)
"""
- return self._plot.getSupportedColormaps()
+ return self.__dimensionsLabels
def getColormap(self):
"""Get the current colormap description.
@@ -720,12 +713,12 @@ class StackView(qt.QMainWindow):
:param colormap: Name of the colormap in
'gray', 'reversed gray', 'temperature', 'red', 'green', 'blue'.
- Or the description of the colormap as a dict.
+ Or a :class`.Colormap` object.
:type colormap: dict or str.
:param str normalization: Colormap mapping: 'linear' or 'log'.
:param bool autoscale: Whether to use autoscale or [vmin, vmax] range.
- Default value of autoscale is True if data is a numpy array,
- False if data is a h5py dataset.
+ Default value of autoscale is False. This option is not compatible
+ with h5py datasets.
:param float vmin: The minimum value of the range to use if
'autoscale' is False.
:param float vmax: The maximum value of the range to use if
@@ -733,84 +726,85 @@ class StackView(qt.QMainWindow):
:param numpy.ndarray colors: Only used if name is None.
Custom colormap colors as Nx3 or Nx4 RGB or RGBA arrays
"""
- cmapDict = self.getColormap()
-
- if isinstance(colormap, dict):
+ # if is a colormap object or a dictionary
+ if isinstance(colormap, Colormap) or isinstance(colormap, dict):
# Support colormap parameter as a dict
- errmsg = "If colormap is provided as a dict, all other parameters"
+ errmsg = "If colormap is provided as a Colormap object, all other parameters"
errmsg += " must not be specified when calling setColormap"
assert normalization is None, errmsg
assert autoscale is None, errmsg
assert vmin is None, errmsg
assert vmax is None, errmsg
assert colors is None, errmsg
- cmapDict.update(colormap)
+ if isinstance(colormap, dict):
+ reason = 'colormap parameter should now be an object'
+ replacement = 'Colormap()'
+ since_version = '0.6'
+ deprecated_warning(type_='function',
+ name='setColormap',
+ reason=reason,
+ replacement=replacement,
+ since_version=since_version)
+ _colormap = Colormap._fromDict(colormap)
+ else:
+ _colormap = colormap
else:
- if colormap is not None:
- cmapDict['name'] = colormap
- if normalization is not None:
- cmapDict['normalization'] = normalization
- if colors is not None:
- cmapDict['colors'] = colors
-
- # Default meaning of autoscale is to reset min and max
- # each time a new image is added to the plot.
- # We want to use min and max of global volume,
- # and not change them when browsing slides
- cmapDict['autoscale'] = False
-
+ norm = normalization if normalization is not None else 'linear'
+ name = colormap if colormap is not None else 'gray'
+ _colormap = Colormap(name=name,
+ normalization=norm,
+ vmin=vmin,
+ vmax=vmax,
+ colors=colors)
+
+ # Patch: since we don't apply this colormap to a single 2D data but
+ # a 2D stack we have to deal manually with vmin, vmax
if autoscale is None:
# set default
autoscale = False
- # TODO: assess cost of computing min/max for large 3D array
- # if isinstance(self._stack, numpy.ndarray):
- # autoscale = True
- # else: # h5py.Dataset
- # autoscale = False
- elif autoscale and isinstance(self._stack, h5py.Dataset):
+ elif autoscale and is_dataset(self._stack):
# h5py dataset has no min()/max() methods
raise RuntimeError(
"Cannot auto-scale colormap for a h5py dataset")
else:
autoscale = autoscale
self.__autoscaleCmap = autoscale
+
if autoscale and (self._stack is not None):
- cmapDict['vmin'] = self._stack.min()
- cmapDict['vmax'] = self._stack.max()
+ _vmin, _vmax = _colormap.getColormapRange(data=self._stack)
+ _colormap.setVRange(vmin=_vmin, vmax=_vmax)
else:
- if vmin is not None:
- cmapDict['vmin'] = vmin
- if vmax is not None:
- cmapDict['vmax'] = vmax
+ if vmin is None and self._stack is not None:
+ _colormap.setVMin(self._stack.min())
+ else:
+ _colormap.setVMin(vmin)
+ if vmax is None and self._stack is not None:
+ _colormap.setVMax(self._stack.max())
+ else:
+ _colormap.setVMax(vmax)
- cursorColor = cursorColorForColormap(cmapDict['name'])
+ cursorColor = cursorColorForColormap(_colormap.getName())
self._plot.setInteractiveMode('zoom', color=cursorColor)
- self._plot.setDefaultColormap(cmapDict)
+ self._plot.setDefaultColormap(_colormap)
# Update active image colormap
activeImage = self._plot.getActiveImage()
if isinstance(activeImage, items.ColormapMixIn):
activeImage.setColormap(self.getColormap())
- def isKeepDataAspectRatio(self):
- """Returns whether the plot is keeping data aspect ratio or not."""
- return self._plot.isKeepDataAspectRatio()
+ def getPlot(self):
+ """Return the :class:`PlotWidget`.
- def setKeepDataAspectRatio(self, flag=True):
- """Set whether the plot keeps data aspect ratio or not.
+ This gives access to advanced plot configuration options.
+ Be warned that modifying the plot can cause issues, and some changes
+ you make to the plot could be overwritten by the :class:`StackView`
+ widget's internal methods and callbacks.
- :param bool flag: True to respect data aspect ratio
+ :return: instance of :class:`PlotWidget` used in widget
"""
- self._plot.setKeepDataAspectRatio(flag)
-
- def getProfileToolbar(self):
- """Profile tools attached to this plot
-
- See :class:`silx.gui.plot.Profile.Profile3DToolBar`
- """
- return self._plot.profile
+ return self._plot
def getProfileWindow1D(self):
"""Plot window used to display 1D profile curve.
@@ -826,7 +820,158 @@ class StackView(qt.QMainWindow):
"""
return self._plot.profile.getProfileWindow2D()
+ def setOptionVisible(self, isVisible):
+ """
+ Set the visibility of the browsing options.
+
+ :param bool isVisible: True to have the options visible, else False
+ """
+ self._browser.setVisible(isVisible)
+ self.__planeSelection.setVisible(isVisible)
+
+ # proxies to PlotWidget or PlotWindow methods
+ def getProfileToolbar(self):
+ """Profile tools attached to this plot
+
+ See :class:`silx.gui.plot.Profile.Profile3DToolBar`
+ """
+ return self._plot.profile
+
+ def getGraphTitle(self):
+ """Return the plot main title as a str.
+ """
+ return self._plot.getGraphTitle()
+
+ def setGraphTitle(self, title=""):
+ """Set the plot main title.
+
+ :param str title: Main title of the plot (default: '')
+ """
+ return self._plot.setGraphTitle(title)
+
+ def getGraphXLabel(self):
+ """Return the current horizontal axis label as a str.
+ """
+ return self._plot.getXAxis().getLabel()
+
+ def setGraphXLabel(self, label=None):
+ """Set the plot horizontal axis label.
+
+ :param str label: The horizontal axis label
+ """
+ if label is None:
+ label = self.__dimensionsLabels[1 if self._perspective == 2 else 2]
+ self._plot.getXAxis().setLabel(label)
+
+ def getGraphYLabel(self, axis='left'):
+ """Return the current vertical axis label as a str.
+
+ :param str axis: The Y axis for which to get the label (left or right)
+ """
+ return self._plot.getYAxis().getLabel(axis)
+
+ def setGraphYLabel(self, label=None, axis='left'):
+ """Set the vertical axis label on the plot.
+
+ :param str label: The Y axis label
+ :param str axis: The Y axis for which to set the label (left or right)
+ """
+ if label is None:
+ label = self.__dimensionsLabels[1 if self._perspective == 0 else 0]
+ self._plot.getYAxis(axis=axis).setLabel(label)
+
+ def resetZoom(self):
+ """Reset the plot limits to the bounds of the data and redraw the plot.
+
+ This method is a simple proxy to the legacy :class:`PlotWidget` method
+ of the same name. Using the object oriented approach is now
+ preferred::
+
+ stackview.getPlot().resetZoom()
+ """
+ self._plot.resetZoom()
+
+ def setYAxisInverted(self, flag=True):
+ """Set the Y axis orientation.
+
+ This method is a simple proxy to the legacy :class:`PlotWidget` method
+ of the same name. Using the object oriented approach is now
+ preferred::
+
+ stackview.getPlot().setYAxisInverted(flag)
+
+ :param bool flag: True for Y axis going from top to bottom,
+ False for Y axis going from bottom to top
+ """
+ self._plot.setYAxisInverted(flag)
+
+ def isYAxisInverted(self):
+ """Return True if Y axis goes from top to bottom, False otherwise.
+
+ This method is a simple proxy to the legacy :class:`PlotWidget` method
+ of the same name. Using the object oriented approach is now
+ preferred::
+
+ stackview.getPlot().isYAxisInverted()"""
+ return self._plot.isYAxisInverted()
+
+ def getSupportedColormaps(self):
+ """Get the supported colormap names as a tuple of str.
+
+ The list should at least contain and start by:
+ ('gray', 'reversed gray', 'temperature', 'red', 'green', 'blue')
+
+ This method is a simple proxy to the legacy :class:`PlotWidget` method
+ of the same name. Using the object oriented approach is now
+ preferred::
+
+ stackview.getPlot().getSupportedColormaps()
+ """
+ return self._plot.getSupportedColormaps()
+
+ def isKeepDataAspectRatio(self):
+ """Returns whether the plot is keeping data aspect ratio or not.
+
+ This method is a simple proxy to the legacy :class:`PlotWidget` method
+ of the same name. Using the object oriented approach is now
+ preferred::
+
+ stackview.getPlot().isKeepDataAspectRatio()"""
+ return self._plot.isKeepDataAspectRatio()
+
+ def setKeepDataAspectRatio(self, flag=True):
+ """Set whether the plot keeps data aspect ratio or not.
+
+ This method is a simple proxy to the legacy :class:`PlotWidget` method
+ of the same name. Using the object oriented approach is now
+ preferred::
+
+ stackview.getPlot().setKeepDataAspectRatio(flag)
+
+ :param bool flag: True to respect data aspect ratio
+ """
+ self._plot.setKeepDataAspectRatio(flag)
+
# kind of private methods, but needed by Profile
+ def getActiveImage(self, just_legend=False):
+ """Returns the currently active image object.
+
+ It returns None in case of not having an active image.
+
+ This method is a simple proxy to the legacy :class:`PlotWidget` method
+ of the same name. Using the object oriented approach is now
+ preferred::
+
+ stackview.getPlot().getActiveImage()
+
+ :param bool just_legend: True to get the legend of the image,
+ False (the default) to get the image data and info.
+ Note: :class:`StackView` uses the same legend for all frames.
+ :return: legend or image object
+ :rtype: str or list or None
+ """
+ return self._plot.getActiveImage(just_legend=just_legend)
+
def remove(self, legend=None,
kind=('curve', 'image', 'item', 'marker')):
"""See :meth:`Plot.Plot.remove`"""
@@ -844,23 +989,6 @@ class StackView(qt.QMainWindow):
"""
self._plot.addItem(*args, **kwargs)
- def setFirstStackDimension(self, first_stack_dimension):
- """When viewing the last 3 dimensions of an n-D array (n>3), you can
- use this method to change the text in the combobox.
-
- For instance, for a 7-D array, first stack dim is 4, so the default
- "Dim1-Dim2" text should be replaced with "Dim5-Dim6" (dimensions
- numbers are 0-based).
-
- :param int first_stack_dim: First stack dimension (n-3) when viewing the
- last 3 dimensions of an n-D array.
- """
- old_state = self.__planeSelection.blockSignals(True)
- self.__planeSelection.setFirstStackDimension(first_stack_dimension)
- self.__planeSelection.blockSignals(old_state)
- self._first_stack_dimension = first_stack_dimension
- self._browser_label.setText("Image index (Dim%d):" % first_stack_dimension)
-
class PlanesWidget(qt.QWidget):
"""Widget for the plane/perspective selection
@@ -974,11 +1102,12 @@ class StackViewMainWindow(StackView):
menu.addSeparator()
menu.addAction(self._plot.resetZoomAction)
menu.addAction(self._plot.colormapAction)
- menu.addAction(PlotActions.KeepAspectRatioAction(self._plot, self))
- menu.addAction(PlotActions.YAxisInvertedAction(self._plot, self))
+ menu.addAction(self._plot.getColorBarWidget().getToggleViewAction())
+
+ menu.addAction(actions.control.KeepAspectRatioAction(self._plot, self))
+ menu.addAction(actions.control.YAxisInvertedAction(self._plot, self))
menu = self.menuBar().addMenu('Profile')
- menu.addAction(self._plot.profile.browseAction)
menu.addAction(self._plot.profile.hLineAction)
menu.addAction(self._plot.profile.vLineAction)
menu.addAction(self._plot.profile.lineAction)