diff options
Diffstat (limited to 'silx/gui/data')
-rw-r--r-- | silx/gui/data/ArrayTableModel.py | 5 | ||||
-rw-r--r-- | silx/gui/data/DataViews.py | 46 | ||||
-rw-r--r-- | silx/gui/data/NXdataWidgets.py | 68 | ||||
-rw-r--r-- | silx/gui/data/_VolumeWindow.py | 148 | ||||
-rw-r--r-- | silx/gui/data/test/test_dataviewer.py | 6 | ||||
-rw-r--r-- | silx/gui/data/test/test_textformatter.py | 4 |
6 files changed, 180 insertions, 97 deletions
diff --git a/silx/gui/data/ArrayTableModel.py b/silx/gui/data/ArrayTableModel.py index ad4d33a..8805241 100644 --- a/silx/gui/data/ArrayTableModel.py +++ b/silx/gui/data/ArrayTableModel.py @@ -1,7 +1,7 @@ # coding: utf-8 # /*########################################################################## # -# Copyright (c) 2016-2017 European Synchrotron Radiation Facility +# Copyright (c) 2016-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 @@ -245,8 +245,7 @@ class ArrayTableModel(qt.QAbstractTableModel): if index.isValid() and role == qt.Qt.EditRole: try: # cast value to same type as array - v = numpy.asscalar( - numpy.array(value, dtype=self._array.dtype)) + v = numpy.array(value, dtype=self._array.dtype).item() except ValueError: return False diff --git a/silx/gui/data/DataViews.py b/silx/gui/data/DataViews.py index 6575d0d..664090d 100644 --- a/silx/gui/data/DataViews.py +++ b/silx/gui/data/DataViews.py @@ -893,53 +893,27 @@ class _Plot3dView(DataView): label="Cube", icon=icons.getQIcon("view-3d")) try: - import silx.gui.plot3d #noqa + from ._VolumeWindow import VolumeWindow # noqa except ImportError: - _logger.warning("Plot3dView is not available") + _logger.warning("3D visualization is not available") _logger.debug("Backtrace", exc_info=True) raise self.__resetZoomNextTime = True def createWidget(self, parent): - from silx.gui.plot3d import ScalarFieldView - from silx.gui.plot3d import SFViewParamTree + from ._VolumeWindow import VolumeWindow - plot = ScalarFieldView.ScalarFieldView(parent) + plot = VolumeWindow(parent) plot.setAxesLabels(*reversed(self.axesNames(None, None))) - - def computeIsolevel(data): - data = data[numpy.isfinite(data)] - if len(data) == 0: - return 0 - else: - return numpy.mean(data) + numpy.std(data) - - plot.addIsosurface(computeIsolevel, '#FF0000FF') - - # Create a parameter tree for the scalar field view - options = SFViewParamTree.TreeView(plot) - options.setSfView(plot) - - # Add the parameter tree to the main window in a dock widget - dock = qt.QDockWidget() - dock.setWidget(options) - plot.addDockWidget(qt.Qt.RightDockWidgetArea, dock) - return plot def clear(self): - self.getWidget().setData(None) + self.getWidget().clear() self.__resetZoomNextTime = True - def normalizeData(self, data): - data = DataView.normalizeData(self, data) - data = _normalizeComplex(data) - return data - def setData(self, data): data = self.normalizeData(data) - plot = self.getWidget() - plot.setData(data) + self.getWidget().setData(data) self.__resetZoomNextTime = False def axesNames(self, data, info): @@ -973,10 +947,10 @@ class _ComplexImageView(DataView): def createWidget(self, parent): from silx.gui.plot.ComplexImageView import ComplexImageView widget = ComplexImageView(parent=parent) - widget.setColormap(self.defaultColormap(), mode=ComplexImageView.Mode.ABSOLUTE) - widget.setColormap(self.defaultColormap(), mode=ComplexImageView.Mode.SQUARE_AMPLITUDE) - widget.setColormap(self.defaultColormap(), mode=ComplexImageView.Mode.REAL) - widget.setColormap(self.defaultColormap(), mode=ComplexImageView.Mode.IMAGINARY) + widget.setColormap(self.defaultColormap(), mode=ComplexImageView.ComplexMode.ABSOLUTE) + widget.setColormap(self.defaultColormap(), mode=ComplexImageView.ComplexMode.SQUARE_AMPLITUDE) + widget.setColormap(self.defaultColormap(), mode=ComplexImageView.ComplexMode.REAL) + widget.setColormap(self.defaultColormap(), mode=ComplexImageView.ComplexMode.IMAGINARY) widget.getPlot().getColormapAction().setColorDialog(self.defaultColorDialog()) widget.getPlot().getIntensityHistogramAction().setVisible(True) widget.getPlot().setKeepDataAspectRatio(True) diff --git a/silx/gui/data/NXdataWidgets.py b/silx/gui/data/NXdataWidgets.py index e5a2550..c3aefd3 100644 --- a/silx/gui/data/NXdataWidgets.py +++ b/silx/gui/data/NXdataWidgets.py @@ -29,7 +29,6 @@ __license__ = "MIT" __date__ = "12/11/2018" import logging -import numbers import numpy from silx.gui import qt @@ -533,10 +532,10 @@ class ArrayComplexImagePlot(qt.QWidget): 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): + for mode in (ComplexImageView.ComplexMode.ABSOLUTE, + ComplexImageView.ComplexMode.SQUARE_AMPLITUDE, + ComplexImageView.ComplexMode.REAL, + ComplexImageView.ComplexMode.IMAGINARY): self._plot.setColormap(colormap, mode) self._plot.getPlot().getIntensityHistogramAction().setVisible(True) @@ -893,28 +892,9 @@ class ArrayVolumePlot(qt.QWidget): self.__x_axis = None self.__x_axis_name = None - from silx.gui.plot3d.ScalarFieldView import ScalarFieldView - from silx.gui.plot3d import SFViewParamTree + from ._VolumeWindow import VolumeWindow - 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._view = VolumeWindow(self) self._hline = qt.QFrame(self) self._hline.setFrameStyle(qt.QFrame.HLine) @@ -935,24 +915,10 @@ class ArrayVolumePlot(qt.QWidget): def getVolumeView(self): """Returns the plot used for the display - :rtype: ScalarFieldView + :rtype: SceneWindow """ 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, @@ -977,7 +943,6 @@ class ArrayVolumePlot(qt.QWidget): :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 @@ -994,9 +959,6 @@ class ArrayVolumePlot(qt.QWidget): 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 @@ -1017,7 +979,6 @@ class ArrayVolumePlot(qt.QWidget): 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 @@ -1049,15 +1010,16 @@ class ArrayVolumePlot(qt.QWidget): 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) + # Update SceneWidget + data = self._selector.selectedData() + + volumeView = self.getVolumeView() + volumeView.setData(data, offset=offset, scale=scale) + volumeView.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) + self.getVolumeView().clear() diff --git a/silx/gui/data/_VolumeWindow.py b/silx/gui/data/_VolumeWindow.py new file mode 100644 index 0000000..03b6876 --- /dev/null +++ b/silx/gui/data/_VolumeWindow.py @@ -0,0 +1,148 @@ +# coding: utf-8 +# /*########################################################################## +# +# Copyright (c) 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 +# 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 visualize 3D arrays""" + +__authors__ = ["T. Vincent"] +__license__ = "MIT" +__date__ = "22/03/2019" + + +import numpy + +from .. import qt +from ..plot3d.SceneWindow import SceneWindow +from ..plot3d.items import ScalarField3D, ComplexField3D, ItemChangedType + + +class VolumeWindow(SceneWindow): + """Extends SceneWindow with a convenient API for 3D array + + :param QWidget: parent + """ + + def __init__(self, parent): + super(VolumeWindow, self).__init__(parent) + self.__firstData = True + # Hide global parameter dock + self.getGroupResetWidget().parent().setVisible(False) + + def setAxesLabels(self, xlabel=None, ylabel=None, zlabel=None): + """Set the text labels of the axes. + + :param Union[str,None] xlabel: Label of the X axis + :param Union[str,None] ylabel: Label of the Y axis + :param Union[str,None] zlabel: Label of the Z axis + """ + sceneWidget = self.getSceneWidget() + sceneWidget.getSceneGroup().setAxesLabels( + 'X' if xlabel is None else xlabel, + 'Y' if ylabel is None else ylabel, + 'Z' if zlabel is None else zlabel) + + def clear(self): + """Clear any currently displayed data""" + sceneWidget = self.getSceneWidget() + items = sceneWidget.getItems() + if (len(items) == 1 and + isinstance(items[0], (ScalarField3D, ComplexField3D))): + items[0].setData(None) + else: # Safety net + sceneWidget.clearItems() + + @staticmethod + def __computeIsolevel(data): + """Returns a suitable isolevel value for data + + :param numpy.ndarray data: + :rtype: float + """ + data = data[numpy.isfinite(data)] + if len(data) == 0: + return 0 + else: + return numpy.mean(data) + numpy.std(data) + + def setData(self, data, offset=(0., 0., 0.), scale=(1., 1., 1.)): + """Set the 3D array data to display. + + :param numpy.ndarray data: 3D array of float or complex + :param List[float] offset: (tx, ty, tz) coordinates of the origin + :param List[float] scale: (sx, sy, sz) scale for each dimension + """ + sceneWidget = self.getSceneWidget() + dataMaxCoords = numpy.array(list(reversed(data.shape))) - 1 + + previousItems = sceneWidget.getItems() + if (len(previousItems) == 1 and + isinstance(previousItems[0], (ScalarField3D, ComplexField3D)) and + numpy.iscomplexobj(data) == isinstance(previousItems[0], ComplexField3D)): + # Reuse existing volume item + volume = sceneWidget.getItems()[0] + volume.setData(data, copy=False) + # Make sure the plane goes through the dataset + for plane in volume.getCutPlanes(): + point = numpy.array(plane.getPoint()) + if numpy.any(point < (0, 0, 0)) or numpy.any(point > dataMaxCoords): + plane.setPoint(dataMaxCoords // 2) + else: + # Add a new volume + sceneWidget.clearItems() + volume = sceneWidget.addVolume(data, copy=False) + volume.setLabel('Volume') + for plane in volume.getCutPlanes(): + # Make plane going through the center of the data + plane.setPoint(dataMaxCoords // 2) + plane.setVisible(False) + plane.sigItemChanged.connect(self.__cutPlaneUpdated) + volume.addIsosurface(self.__computeIsolevel, '#FF0000FF') + + # Expand the parameter tree + model = self.getParamTreeView().model() + index = qt.QModelIndex() # Invalid index for top level + while 1: + rowCount = model.rowCount(parent=index) + if rowCount == 0: + break + index = model.index(rowCount - 1, 0, parent=index) + self.getParamTreeView().setExpanded(index, True) + if not index.isValid(): + break + + volume.setTranslation(*offset) + volume.setScale(*scale) + + if self.__firstData: # Only center for first dataset + self.__firstData = False + sceneWidget.centerScene() + + def __cutPlaneUpdated(self, event): + """Handle the change of visibility of the cut plane + + :param event: Kind of update + """ + if event == ItemChangedType.VISIBLE: + plane = self.sender() + if plane.isVisible(): + self.getSceneWidget().selection().setCurrentItem(plane) diff --git a/silx/gui/data/test/test_dataviewer.py b/silx/gui/data/test/test_dataviewer.py index dc6fee8..12a640e 100644 --- a/silx/gui/data/test/test_dataviewer.py +++ b/silx/gui/data/test/test_dataviewer.py @@ -1,7 +1,7 @@ # coding: utf-8 # /*########################################################################## # -# Copyright (c) 2016-2018 European Synchrotron Radiation Facility +# Copyright (c) 2016-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 @@ -242,8 +242,8 @@ class AbstractDataViewerTests(TestCaseQt): self.assertTrue(replaced) nxdata_view = widget.getViewFromModeId(DataViews.NXDATA_MODE) self.assertNotIn(DataViews.NXDATA_INVALID_MODE, - [v.modeId() for v in nxdata_view.availableViews()]) - self.assertTrue(view in nxdata_view.availableViews()) + [v.modeId() for v in nxdata_view.getViews()]) + self.assertTrue(view in nxdata_view.getViews()) class TestDataViewer(AbstractDataViewerTests): diff --git a/silx/gui/data/test/test_textformatter.py b/silx/gui/data/test/test_textformatter.py index 935344a..1a63074 100644 --- a/silx/gui/data/test/test_textformatter.py +++ b/silx/gui/data/test/test_textformatter.py @@ -1,7 +1,7 @@ # coding: utf-8 # /*########################################################################## # -# Copyright (c) 2016-2018 European Synchrotron Radiation Facility +# Copyright (c) 2016-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 @@ -48,7 +48,7 @@ class TestTextFormatter(TestCaseQt): self.assertIsNot(formatter, copy) copy.setFloatFormat("%.3f") self.assertEqual(formatter.integerFormat(), copy.integerFormat()) - self.assertNotEquals(formatter.floatFormat(), copy.floatFormat()) + self.assertNotEqual(formatter.floatFormat(), copy.floatFormat()) self.assertEqual(formatter.useQuoteForText(), copy.useQuoteForText()) self.assertEqual(formatter.imaginaryUnit(), copy.imaginaryUnit()) |