diff options
Diffstat (limited to 'silx/gui/data/NXdataWidgets.py')
-rw-r--r-- | silx/gui/data/NXdataWidgets.py | 439 |
1 files changed, 417 insertions, 22 deletions
diff --git a/silx/gui/data/NXdataWidgets.py b/silx/gui/data/NXdataWidgets.py index f7c479d..e5a2550 100644 --- a/silx/gui/data/NXdataWidgets.py +++ b/silx/gui/data/NXdataWidgets.py @@ -1,7 +1,7 @@ # coding: utf-8 # /*########################################################################## # -# Copyright (c) 2017-2018 European Synchrotron Radiation Facility +# Copyright (c) 2017-2019 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 @@ -26,19 +26,25 @@ """ __authors__ = ["P. Knobel"] __license__ = "MIT" -__date__ = "10/10/2018" +__date__ = "12/11/2018" +import logging +import numbers import numpy from silx.gui import qt from silx.gui.data.NumpyAxesSelector import NumpyAxesSelector from silx.gui.plot import Plot1D, Plot2D, StackView, ScatterView +from silx.gui.plot.ComplexImageView import ComplexImageView from silx.gui.colors import Colormap from silx.gui.widgets.FrameBrowser import HorizontalSliderWithBrowser from silx.math.calibration import ArrayCalibration, NoCalibration, LinearCalibration +_logger = logging.getLogger(__name__) + + class ArrayCurvePlot(qt.QWidget): """ Widget for plotting a curve from a multi-dimensional signal array @@ -72,21 +78,16 @@ class ArrayCurvePlot(qt.QWidget): self._plot = Plot1D(self) - self.selectorDock = qt.QDockWidget("Data selector", self._plot) - # not closable - self.selectorDock.setFeatures(qt.QDockWidget.DockWidgetMovable | - qt.QDockWidget.DockWidgetFloatable) - self._selector = NumpyAxesSelector(self.selectorDock) + self._selector = NumpyAxesSelector(self) self._selector.setNamedAxesSelectorVisibility(False) self.__selector_is_connected = False - self.selectorDock.setWidget(self._selector) - self._plot.addTabbedDockWidget(self.selectorDock) self._plot.sigActiveCurveChanged.connect(self._setYLabelFromActiveLegend) - layout = qt.QGridLayout() + layout = qt.QVBoxLayout() layout.setContentsMargins(0, 0, 0, 0) - layout.addWidget(self._plot, 0, 0) + layout.addWidget(self._plot) + layout.addWidget(self._selector) self.setLayout(layout) @@ -130,9 +131,9 @@ class ArrayCurvePlot(qt.QWidget): self._selector.setAxisNames(["Y"]) if len(ys[0].shape) < 2: - self.selectorDock.hide() + self._selector.hide() else: - self.selectorDock.show() + self._selector.show() self._plot.setGraphTitle(title or "") self._updateCurve() @@ -182,6 +183,9 @@ class ArrayCurvePlot(qt.QWidget): break def clear(self): + old = self._selector.blockSignals(True) + self._selector.clear() + self._selector.blockSignals(old) self._plot.clear() @@ -339,11 +343,8 @@ class ArrayImagePlot(qt.QWidget): normalization=Colormap.LINEAR)) self._plot.getIntensityHistogramAction().setVisible(True) - self.selectorDock = qt.QDockWidget("Data selector", self._plot) # not closable - self.selectorDock.setFeatures(qt.QDockWidget.DockWidgetMovable | - qt.QDockWidget.DockWidgetFloatable) - self._selector = NumpyAxesSelector(self.selectorDock) + self._selector = NumpyAxesSelector(self) self._selector.setNamedAxesSelectorVisibility(False) self._selector.selectionChanged.connect(self._updateImage) @@ -355,9 +356,8 @@ class ArrayImagePlot(qt.QWidget): layout = qt.QVBoxLayout() layout.addWidget(self._plot) + layout.addWidget(self._selector) layout.addWidget(self._auxSigSlider) - self.selectorDock.setWidget(self._selector) - self._plot.addTabbedDockWidget(self.selectorDock) self.setLayout(layout) @@ -413,9 +413,9 @@ class ArrayImagePlot(qt.QWidget): self._selector.setData(signals[0]) if len(signals[0].shape) <= img_ndim: - self.selectorDock.hide() + self._selector.hide() else: - self.selectorDock.show() + self._selector.show() self._auxSigSlider.setMaximum(len(signals) - 1) if len(signals) > 1: @@ -425,6 +425,7 @@ class ArrayImagePlot(qt.QWidget): self._auxSigSlider.setValue(0) self._updateImage() + self._plot.resetZoom() self._selector.selectionChanged.connect(self._updateImage) self._auxSigSlider.valueChanged.connect(self._sliderIdxChanged) @@ -492,12 +493,202 @@ class ArrayImagePlot(qt.QWidget): self._plot.setGraphTitle(title) self._plot.getXAxis().setLabel(self.__x_axis_name) self._plot.getYAxis().setLabel(self.__y_axis_name) - self._plot.resetZoom() def clear(self): + old = self._selector.blockSignals(True) + self._selector.clear() + self._selector.blockSignals(old) self._plot.clear() +class ArrayComplexImagePlot(qt.QWidget): + """ + Widget for plotting an image of complex from a multi-dimensional signal array + and two 1D axes array. + + The signal array can have an arbitrary number of dimensions, the only + limitation being that the last two dimensions must have the same length as + the axes arrays. + + Sliders are provided to select indices on the first (n - 2) dimensions of + the signal array, and the plot is updated to show the image corresponding + to the selection. + + If one or both of the axes does not have regularly spaced values, the + the image is plotted as a coloured scatter plot. + """ + def __init__(self, parent=None, colormap=None): + """ + + :param parent: Parent QWidget + """ + super(ArrayComplexImagePlot, self).__init__(parent) + + self.__signals = None + self.__signals_names = None + self.__x_axis = None + self.__x_axis_name = None + self.__y_axis = None + self.__y_axis_name = None + + self._plot = ComplexImageView(self) + if colormap is not None: + for mode in (ComplexImageView.Mode.ABSOLUTE, + ComplexImageView.Mode.SQUARE_AMPLITUDE, + ComplexImageView.Mode.REAL, + ComplexImageView.Mode.IMAGINARY): + self._plot.setColormap(colormap, mode) + + self._plot.getPlot().getIntensityHistogramAction().setVisible(True) + self._plot.setKeepDataAspectRatio(True) + + # not closable + self._selector = NumpyAxesSelector(self) + self._selector.setNamedAxesSelectorVisibility(False) + self._selector.selectionChanged.connect(self._updateImage) + + self._auxSigSlider = HorizontalSliderWithBrowser(parent=self) + self._auxSigSlider.setMinimum(0) + self._auxSigSlider.setValue(0) + self._auxSigSlider.valueChanged[int].connect(self._sliderIdxChanged) + self._auxSigSlider.setToolTip("Select auxiliary signals") + + layout = qt.QVBoxLayout() + layout.addWidget(self._plot) + layout.addWidget(self._selector) + layout.addWidget(self._auxSigSlider) + + self.setLayout(layout) + + def _sliderIdxChanged(self, value): + self._updateImage() + + def getPlot(self): + """Returns the plot used for the display + + :rtype: PlotWidget + """ + return self._plot.getPlot() + + def setImageData(self, signals, + x_axis=None, y_axis=None, + signals_names=None, + xlabel=None, ylabel=None, + title=None): + """ + + :param signals: list of n-D datasets, whose last 2 dimensions are used as the + image's values, or list of 3D datasets interpreted as RGBA image. + :param x_axis: 1-D dataset used as the image's x coordinates. If + provided, its lengths must be equal to the length of the last + dimension of ``signal``. + :param y_axis: 1-D dataset used as the image's y. If provided, + its lengths must be equal to the length of the 2nd to last + dimension of ``signal``. + :param signals_names: Names for each image, used as subtitle and legend. + :param xlabel: Label for X axis + :param ylabel: Label for Y axis + :param title: Graph title + """ + self._selector.selectionChanged.disconnect(self._updateImage) + self._auxSigSlider.valueChanged.disconnect(self._sliderIdxChanged) + + self.__signals = signals + self.__signals_names = signals_names + self.__x_axis = x_axis + self.__x_axis_name = xlabel + self.__y_axis = y_axis + self.__y_axis_name = ylabel + self.__title = title + + self._selector.clear() + self._selector.setAxisNames(["Y", "X"]) + self._selector.setData(signals[0]) + + if len(signals[0].shape) <= 2: + self._selector.hide() + else: + self._selector.show() + + self._auxSigSlider.setMaximum(len(signals) - 1) + if len(signals) > 1: + self._auxSigSlider.show() + else: + self._auxSigSlider.hide() + self._auxSigSlider.setValue(0) + + self._updateImage() + self._plot.getPlot().resetZoom() + + self._selector.selectionChanged.connect(self._updateImage) + self._auxSigSlider.valueChanged.connect(self._sliderIdxChanged) + + def _updateImage(self): + selection = self._selector.selection() + auxSigIdx = self._auxSigSlider.value() + + images = [img[selection] for img in self.__signals] + image = images[auxSigIdx] + + x_axis = self.__x_axis + y_axis = self.__y_axis + + if x_axis is None and y_axis is None: + xcalib = NoCalibration() + ycalib = NoCalibration() + else: + if x_axis is None: + # no calibration + x_axis = numpy.arange(image.shape[1]) + elif numpy.isscalar(x_axis) or len(x_axis) == 1: + # constant axis + x_axis = x_axis * numpy.ones((image.shape[1], )) + elif len(x_axis) == 2: + # linear calibration + x_axis = x_axis[0] * numpy.arange(image.shape[1]) + x_axis[1] + + if y_axis is None: + y_axis = numpy.arange(image.shape[0]) + elif numpy.isscalar(y_axis) or len(y_axis) == 1: + y_axis = y_axis * numpy.ones((image.shape[0], )) + elif len(y_axis) == 2: + y_axis = y_axis[0] * numpy.arange(image.shape[0]) + y_axis[1] + + xcalib = ArrayCalibration(x_axis) + ycalib = ArrayCalibration(y_axis) + + self._plot.setData(image) + if xcalib.is_affine(): + xorigin, xscale = xcalib(0), xcalib.get_slope() + else: + _logger.warning("Unsupported complex image X axis calibration") + xorigin, xscale = 0., 1. + + if ycalib.is_affine(): + yorigin, yscale = ycalib(0), ycalib.get_slope() + else: + _logger.warning("Unsupported complex image Y axis calibration") + yorigin, yscale = 0., 1. + + self._plot.setOrigin((xorigin, yorigin)) + self._plot.setScale((xscale, yscale)) + + title = "" + if self.__title: + title += self.__title + if not title.strip().endswith(self.__signals_names[auxSigIdx]): + title += "\n" + self.__signals_names[auxSigIdx] + self._plot.setGraphTitle(title) + self._plot.getXAxis().setLabel(self.__x_axis_name) + self._plot.getYAxis().setLabel(self.__y_axis_name) + + def clear(self): + old = self._selector.blockSignals(True) + self._selector.clear() + self._selector.blockSignals(old) + self._plot.setData(None) + + class ArrayStackPlot(qt.QWidget): """ Widget for plotting a n-D array (n >= 3) as a stack of images. @@ -665,4 +856,208 @@ class ArrayStackPlot(qt.QWidget): self.__x_axis_name]) def clear(self): + old = self._selector.blockSignals(True) + self._selector.clear() + self._selector.blockSignals(old) self._stack_view.clear() + + +class ArrayVolumePlot(qt.QWidget): + """ + Widget for plotting a n-D array (n >= 3) as a 3D scalar field. + Three axis arrays can be provided to calibrate the axes. + + The signal array can have an arbitrary number of dimensions, the only + limitation being that the last 3 dimensions must have the same length as + the axes arrays. + + Sliders are provided to select indices on the first (n - 3) dimensions of + the signal array, and the plot is updated to load the stack corresponding + to the selection. + """ + def __init__(self, parent=None): + """ + + :param parent: Parent QWidget + """ + super(ArrayVolumePlot, self).__init__(parent) + + self.__signal = None + self.__signal_name = None + # the Z, Y, X axes apply to the last three dimensions of the signal + # (in that order) + self.__z_axis = None + self.__z_axis_name = None + self.__y_axis = None + self.__y_axis_name = None + self.__x_axis = None + self.__x_axis_name = None + + from silx.gui.plot3d.ScalarFieldView import ScalarFieldView + from silx.gui.plot3d import SFViewParamTree + + self._view = ScalarFieldView(self) + + def computeIsolevel(data): + data = data[numpy.isfinite(data)] + if len(data) == 0: + return 0 + else: + return numpy.mean(data) + numpy.std(data) + + self._view.addIsosurface(computeIsolevel, '#FF0000FF') + + # Create a parameter tree for the scalar field view + options = SFViewParamTree.TreeView(self._view) + options.setSfView(self._view) + + # Add the parameter tree to the main window in a dock widget + dock = qt.QDockWidget() + dock.setWidget(options) + self._view.addDockWidget(qt.Qt.RightDockWidgetArea, dock) + + self._hline = qt.QFrame(self) + self._hline.setFrameStyle(qt.QFrame.HLine) + self._hline.setFrameShadow(qt.QFrame.Sunken) + self._legend = qt.QLabel(self) + self._selector = NumpyAxesSelector(self) + self._selector.setNamedAxesSelectorVisibility(False) + self.__selector_is_connected = False + + layout = qt.QVBoxLayout() + layout.addWidget(self._view) + layout.addWidget(self._hline) + layout.addWidget(self._legend) + layout.addWidget(self._selector) + + self.setLayout(layout) + + def getVolumeView(self): + """Returns the plot used for the display + + :rtype: ScalarFieldView + """ + return self._view + + def normalizeComplexData(self, data): + """ + Converts a complex data array to its amplitude, if necessary. + :param data: the data to normalize + :return: + """ + if hasattr(data, "dtype"): + isComplex = numpy.issubdtype(data.dtype, numpy.complexfloating) + else: + isComplex = isinstance(data, numbers.Complex) + if isComplex: + data = numpy.absolute(data) + return data + + def setData(self, signal, + x_axis=None, y_axis=None, z_axis=None, + signal_name=None, + xlabel=None, ylabel=None, zlabel=None, + title=None): + """ + + :param signal: n-D dataset, whose last 3 dimensions are used as the + 3D stack values. + :param x_axis: 1-D dataset used as the image's x coordinates. If + provided, its lengths must be equal to the length of the last + dimension of ``signal``. + :param y_axis: 1-D dataset used as the image's y. If provided, + its lengths must be equal to the length of the 2nd to last + dimension of ``signal``. + :param z_axis: 1-D dataset used as the image's z. If provided, + its lengths must be equal to the length of the 3rd to last + dimension of ``signal``. + :param signal_name: Label used in the legend + :param xlabel: Label for X axis + :param ylabel: Label for Y axis + :param zlabel: Label for Z axis + :param title: Graph title + """ + signal = self.normalizeComplexData(signal) + if self.__selector_is_connected: + self._selector.selectionChanged.disconnect(self._updateVolume) + self.__selector_is_connected = False + + self.__signal = signal + self.__signal_name = signal_name or "" + self.__x_axis = x_axis + self.__x_axis_name = xlabel + self.__y_axis = y_axis + self.__y_axis_name = ylabel + self.__z_axis = z_axis + self.__z_axis_name = zlabel + + self._selector.setData(signal) + self._selector.setAxisNames(["Y", "X", "Z"]) + + self._view.setAxesLabels(self.__x_axis_name or 'X', + self.__y_axis_name or 'Y', + self.__z_axis_name or 'Z') + self._updateVolume() + + # the legend label shows the selection slice producing the volume + # (only interesting for ndim > 3) + if signal.ndim > 3: + self._selector.setVisible(True) + self._legend.setVisible(True) + self._hline.setVisible(True) + else: + self._selector.setVisible(False) + self._legend.setVisible(False) + self._hline.setVisible(False) + + if not self.__selector_is_connected: + self._selector.selectionChanged.connect(self._updateVolume) + self.__selector_is_connected = True + + def _updateVolume(self): + """Update displayed stack according to the current axes selector + data.""" + data = self._selector.selectedData() + x_axis = self.__x_axis + y_axis = self.__y_axis + z_axis = self.__z_axis + + offset = [] + scale = [] + for axis in [x_axis, y_axis, z_axis]: + if axis is None: + calibration = NoCalibration() + elif len(axis) == 2: + calibration = LinearCalibration( + y_intercept=axis[0], slope=axis[1]) + else: + calibration = ArrayCalibration(axis) + if not calibration.is_affine(): + _logger.warning("Axis has not linear values, ignored") + offset.append(0.) + scale.append(1.) + else: + offset.append(calibration(0)) + scale.append(calibration.get_slope()) + + legend = self.__signal_name + "[" + for sl in self._selector.selection(): + if sl == slice(None): + legend += ":, " + else: + legend += str(sl) + ", " + legend = legend[:-2] + "]" + self._legend.setText("Displayed data: " + legend) + + self._view.setData(data, copy=False) + self._view.setScale(*scale) + self._view.setTranslation(*offset) + self._view.setAxesLabels(self.__x_axis_name, + self.__y_axis_name, + self.__z_axis_name) + + def clear(self): + old = self._selector.blockSignals(True) + self._selector.clear() + self._selector.blockSignals(old) + self._view.setData(None) |