diff options
Diffstat (limited to 'src/silx/gui/data/_VolumeWindow.py')
-rw-r--r-- | src/silx/gui/data/_VolumeWindow.py | 148 |
1 files changed, 148 insertions, 0 deletions
diff --git a/src/silx/gui/data/_VolumeWindow.py b/src/silx/gui/data/_VolumeWindow.py new file mode 100644 index 0000000..03b6876 --- /dev/null +++ b/src/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) |