summaryrefslogtreecommitdiff
path: root/silx/gui/plot3d/items/volume.py
diff options
context:
space:
mode:
Diffstat (limited to 'silx/gui/plot3d/items/volume.py')
-rw-r--r--silx/gui/plot3d/items/volume.py173
1 files changed, 138 insertions, 35 deletions
diff --git a/silx/gui/plot3d/items/volume.py b/silx/gui/plot3d/items/volume.py
index a7b5923..ca22f1f 100644
--- a/silx/gui/plot3d/items/volume.py
+++ b/silx/gui/plot3d/items/volume.py
@@ -41,10 +41,11 @@ from silx.math.marchingcubes import MarchingCubes
from ... import qt
from ...colors import rgba
-from ..scene import cutplane, primitives, transform
+from ..scene import cutplane, primitives, transform, utils
-from .core import DataItem3D, Item3D, ItemChangedType, Item3DChangedType
+from .core import BaseNodeItem, Item3D, ItemChangedType, Item3DChangedType
from .mixins import ColormapMixIn, InterpolationMixIn, PlaneMixIn
+from ._pick import PickingResult
_logger = logging.getLogger(__name__)
@@ -77,7 +78,8 @@ class CutPlane(Item3D, ColormapMixIn, InterpolationMixIn, PlaneMixIn):
def _parentChanged(self, event):
"""Handle data change in the parent this plane belongs to"""
if event == ItemChangedType.DATA:
- self._getPlane().setData(self.sender().getData(), copy=False)
+ self._getPlane().setData(self.sender().getData(copy=False),
+ copy=False)
# Store data range info as 3-tuple of values
self._dataRange = self.sender().getDataRange()
@@ -113,6 +115,53 @@ class CutPlane(Item3D, ColormapMixIn, InterpolationMixIn, PlaneMixIn):
"""
return self._dataRange
+ def getData(self, copy=True):
+ """Return 3D dataset.
+
+ :param bool copy:
+ True (default) to get a copy,
+ False to get the internal data (DO NOT modify!)
+ :return: The data set (or None if not set)
+ """
+ parent = self.parent()
+ return None if parent is None else parent.getData(copy=copy)
+
+ def _pickFull(self, context):
+ """Perform picking in this item at given widget position.
+
+ :param PickContext context: Current picking context
+ :return: Object holding the results or None
+ :rtype: Union[None,PickingResult]
+ """
+ rayObject = context.getPickingSegment(frame=self._getScenePrimitive())
+ if rayObject is None:
+ return None
+
+ points = utils.segmentPlaneIntersect(
+ rayObject[0, :3],
+ rayObject[1, :3],
+ planeNorm=self.getNormal(),
+ planePt=self.getPoint())
+
+ if len(points) == 1: # Single intersection
+ if numpy.any(points[0] < 0.):
+ return None # Outside volume
+ z, y, x = int(points[0][2]), int(points[0][1]), int(points[0][0])
+
+ data = self.getData(copy=False)
+ if data is None:
+ return None # No dataset
+
+ depth, height, width = data.shape
+ if z < depth and y < height and x < width:
+ return PickingResult(self,
+ positions=[points[0]],
+ indices=([z], [y], [x]))
+ else:
+ return None # Outside image
+ else: # Either no intersection or segment and image are coplanar
+ return None
+
class Isosurface(Item3D):
"""Class representing an iso-surface in a :class:`ScalarField3D` item.
@@ -122,24 +171,28 @@ class Isosurface(Item3D):
def __init__(self, parent):
Item3D.__init__(self, parent=parent)
+ assert isinstance(parent, ScalarField3D)
+ parent.sigItemChanged.connect(self._scalarField3DChanged)
self._level = float('nan')
self._autoLevelFunction = None
self._color = rgba('#FFD700FF')
- self._data = None
+ self._updateScenePrimitive()
- # TODO register to ScalarField3D signal instead?
- def _setData(self, data, copy=True):
- """Set the data set from which to build the iso-surface.
+ def _scalarField3DChanged(self, event):
+ """Handle parent's ScalarField3D sigItemChanged"""
+ if event == ItemChangedType.DATA:
+ self._updateScenePrimitive()
- :param numpy.ndarray data: The 3D data set or None
- :param bool copy: True to make a copy, False to use as is if possible
- """
- if data is None:
- self._data = None
- else:
- self._data = numpy.array(data, copy=copy, order='C')
+ def getData(self, copy=True):
+ """Return 3D dataset.
- self._updateScenePrimitive()
+ :param bool copy:
+ True (default) to get a copy,
+ False to get the internal data (DO NOT modify!)
+ :return: The data set (or None if not set)
+ """
+ parent = self.parent()
+ return None if parent is None else parent.getData(copy=copy)
def getLevel(self):
"""Return the level of this iso-surface (float)"""
@@ -203,7 +256,9 @@ class Isosurface(Item3D):
"""Update underlying mesh"""
self._getScenePrimitive().children = []
- if self._data is None:
+ data = self.getData(copy=False)
+
+ if data is None:
if self.isAutoLevel():
self._level = float('nan')
@@ -211,7 +266,7 @@ class Isosurface(Item3D):
if self.isAutoLevel():
st = time.time()
try:
- level = float(self.getAutoLevelFunction()(self._data))
+ level = float(self.getAutoLevelFunction()(data))
except Exception:
module_ = self.getAutoLevelFunction().__module__
@@ -236,7 +291,7 @@ class Isosurface(Item3D):
st = time.time()
vertices, normals, indices = MarchingCubes(
- self._data,
+ data,
isolevel=self._level)
_logger.info('Computed iso-surface in %f s.', time.time() - st)
@@ -250,15 +305,73 @@ class Isosurface(Item3D):
indices=indices)
self._getScenePrimitive().children = [mesh]
+ def _pickFull(self, context):
+ """Perform picking in this item at given widget position.
+
+ :param PickContext context: Current picking context
+ :return: Object holding the results or None
+ :rtype: Union[None,PickingResult]
+ """
+ rayObject = context.getPickingSegment(frame=self._getScenePrimitive())
+ if rayObject is None:
+ return None
+ rayObject = rayObject[:, :3]
+
+ data = self.getData(copy=False)
+ bins = utils.segmentVolumeIntersect(
+ rayObject, numpy.array(data.shape) - 1)
+ if bins is None:
+ return None
-class ScalarField3D(DataItem3D):
+ # gather bin data
+ offsets = [(i, j, k) for i in (0, 1) for j in (0, 1) for k in (0, 1)]
+ indices = bins[:, numpy.newaxis, :] + offsets
+ binsData = data[indices[:, :, 0], indices[:, :, 1], indices[:, :, 2]]
+ # binsData.shape = nbins, 8
+ # TODO up-to this point everything can be done once for all isosurfaces
+
+ # check bin candidates
+ level = self.getLevel()
+ mask = numpy.logical_and(numpy.nanmin(binsData, axis=1) <= level,
+ level <= numpy.nanmax(binsData, axis=1))
+ bins = bins[mask]
+ binsData = binsData[mask]
+
+ if len(bins) == 0:
+ return None # No bin candidate
+
+ # do picking on candidates
+ intersections = []
+ depths = []
+ for currentBin, data in zip(bins, binsData):
+ mc = MarchingCubes(data.reshape(2, 2, 2), isolevel=level)
+ points = mc.get_vertices() + currentBin
+ triangles = points[mc.get_indices()]
+ t = utils.segmentTrianglesIntersection(rayObject, triangles)[1]
+ t = numpy.unique(t) # Duplicates happen on triangle edges
+ if len(t) != 0:
+ # Compute intersection points and get closest data point
+ points = t.reshape(-1, 1) * (rayObject[1] - rayObject[0]) + rayObject[0]
+ # Get closest data points by rounding to int
+ intersections.extend(points)
+ depths.extend(t)
+
+ if len(intersections) == 0:
+ return None # No intersected triangles
+
+ intersections = numpy.array(intersections)[numpy.argsort(depths)]
+ indices = numpy.transpose(numpy.round(intersections).astype(numpy.int))
+ return PickingResult(self, positions=intersections, indices=indices)
+
+
+class ScalarField3D(BaseNodeItem):
"""3D scalar field on a regular grid.
:param parent: The View widget this item belongs to.
"""
def __init__(self, parent=None):
- DataItem3D.__init__(self, parent=parent)
+ BaseNodeItem.__init__(self, parent=parent)
# Gives this item the shape of the data, no matter
# of the isosurface/cut plane size
@@ -327,10 +440,6 @@ class ScalarField3D(DataItem3D):
self._boundedGroup.shape = self._data.shape
- # Update iso-surfaces
- for isosurface in self.getIsosurfaces():
- isosurface._setData(self._data, copy=False)
-
self._updated(ItemChangedType.DATA)
def getData(self, copy=True):
@@ -401,7 +510,6 @@ class ScalarField3D(DataItem3D):
isosurface.setAutoLevelFunction(level)
else:
isosurface.setLevel(level)
- isosurface._setData(self._data, copy=False)
isosurface.sigItemChanged.connect(self._isosurfaceItemChanged)
self._isosurfaces.append(isosurface)
@@ -448,16 +556,11 @@ class ScalarField3D(DataItem3D):
key=lambda isosurface: - isosurface.getLevel())
self._isogroup.children = [iso._getScenePrimitive() for iso in sortedIso]
- def visit(self, included=True):
- """Generator visiting the ScalarField3D content.
+ # BaseNodeItem
- It first access cut planes and then isosurface
+ def getItems(self):
+ """Returns the list of items currently present in the ScalarField3D.
- :param bool included: True (default) to include self in visit
+ :rtype: tuple
"""
- if included:
- yield self
- for cutPlane in self.getCutPlanes():
- yield cutPlane
- for isosurface in self.getIsosurfaces():
- yield isosurface
+ return self.getCutPlanes() + self.getIsosurfaces()