summaryrefslogtreecommitdiff
path: root/silx/gui/plot3d/scene
diff options
context:
space:
mode:
authorAlexandre Marie <alexandre.marie@synchrotron-soleil.fr>2018-12-17 12:28:45 +0100
committerAlexandre Marie <alexandre.marie@synchrotron-soleil.fr>2018-12-17 12:28:45 +0100
commitc49572a2e90b398e90a43f86b490f27ee6c58de6 (patch)
treed130cf7dfc3cf73157e1bece8173331bb4bc7156 /silx/gui/plot3d/scene
parent0bbc8ab933e62c1fa6d548e879ae6d98cbd461f1 (diff)
parentcebdc9244c019224846cb8d2668080fe386a6adc (diff)
Merge tag 'upstream/0.9.0+dfsg'
Upstream version 0.9.0+dfsg
Diffstat (limited to 'silx/gui/plot3d/scene')
-rw-r--r--silx/gui/plot3d/scene/event.py4
-rw-r--r--silx/gui/plot3d/scene/function.py4
-rw-r--r--silx/gui/plot3d/scene/primitives.py2
-rw-r--r--silx/gui/plot3d/scene/setup.py41
-rw-r--r--silx/gui/plot3d/scene/transform.py42
-rw-r--r--silx/gui/plot3d/scene/utils.py180
6 files changed, 224 insertions, 49 deletions
diff --git a/silx/gui/plot3d/scene/event.py b/silx/gui/plot3d/scene/event.py
index 7b85434..98f8f8b 100644
--- a/silx/gui/plot3d/scene/event.py
+++ b/silx/gui/plot3d/scene/event.py
@@ -28,7 +28,7 @@ from __future__ import absolute_import, division, unicode_literals
__authors__ = ["T. Vincent"]
__license__ = "MIT"
-__date__ = "25/07/2016"
+__date__ = "17/07/2018"
import logging
@@ -66,7 +66,7 @@ class Notifier(object):
try:
self._listeners.remove(listener)
except ValueError:
- _logger.warn('Trying to remove a listener that is not registered')
+ _logger.warning('Trying to remove a listener that is not registered')
def notify(self, *args, **kwargs):
"""Notify all registered listeners with the given parameters.
diff --git a/silx/gui/plot3d/scene/function.py b/silx/gui/plot3d/scene/function.py
index ba4c4ca..2921d48 100644
--- a/silx/gui/plot3d/scene/function.py
+++ b/silx/gui/plot3d/scene/function.py
@@ -28,7 +28,7 @@ from __future__ import absolute_import, division, unicode_literals
__authors__ = ["T. Vincent"]
__license__ = "MIT"
-__date__ = "08/11/2016"
+__date__ = "17/07/2018"
import contextlib
@@ -428,7 +428,7 @@ class Colormap(event.Notifier, ProgramFunction):
range_ = float(range_[0]), float(range_[1])
if self.norm == 'log' and (range_[0] <= 0. or range_[1] <= 0.):
- _logger.warn(
+ _logger.warning(
"Log normalization and negative range: updating range.")
minPos = numpy.finfo(numpy.float32).tiny
range_ = max(range_[0], minPos), max(range_[1], minPos)
diff --git a/silx/gui/plot3d/scene/primitives.py b/silx/gui/plot3d/scene/primitives.py
index af00b6d..474581a 100644
--- a/silx/gui/plot3d/scene/primitives.py
+++ b/silx/gui/plot3d/scene/primitives.py
@@ -201,7 +201,7 @@ class Geometry(core.Elem):
array = self._glReadyArray(array, copy=copy)
if name not in self._ATTR_INFO:
- _logger.info('Not checking attribute %s dimensions', name)
+ _logger.debug('Not checking attribute %s dimensions', name)
else:
checks = self._ATTR_INFO[name]
diff --git a/silx/gui/plot3d/scene/setup.py b/silx/gui/plot3d/scene/setup.py
deleted file mode 100644
index ff4c0a6..0000000
--- a/silx/gui/plot3d/scene/setup.py
+++ /dev/null
@@ -1,41 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2015-2017 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.
-#
-# ###########################################################################*/
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "25/07/2016"
-
-from numpy.distutils.misc_util import Configuration
-
-
-def configuration(parent_package='', top_path=None):
- config = Configuration('scene', parent_package, top_path)
- config.add_subpackage('test')
- return config
-
-
-if __name__ == "__main__":
- from numpy.distutils.core import setup
-
- setup(configuration=configuration)
diff --git a/silx/gui/plot3d/scene/transform.py b/silx/gui/plot3d/scene/transform.py
index 4061e81..1b82397 100644
--- a/silx/gui/plot3d/scene/transform.py
+++ b/silx/gui/plot3d/scene/transform.py
@@ -305,6 +305,44 @@ class Transform(event.Notifier):
# Multiplication with vectors
+ def transformPoints(self, points, direct=True, perspectiveDivide=False):
+ """Apply the transform to an array of points.
+
+ :param points: 2D array of N vectors of 3 or 4 coordinates
+ :param bool direct: Whether to apply the direct (True, the default)
+ or inverse (False) transform.
+ :param bool perspectiveDivide: Whether to apply the perspective divide
+ (True) or not (False, the default).
+ :return: The transformed points.
+ :rtype: numpy.ndarray of same shape as points.
+ """
+ if direct:
+ matrix = self.getMatrix(copy=False)
+ else:
+ matrix = self.getInverseMatrix(copy=False)
+
+ points = numpy.array(points, copy=False)
+ assert points.ndim == 2
+
+ points = numpy.transpose(points)
+
+ dimension = points.shape[0]
+ assert dimension in (3, 4)
+
+ if dimension == 3: # Add 4th coordinate
+ points = numpy.append(
+ points,
+ numpy.ones((1, points.shape[1]), dtype=points.dtype),
+ axis=0)
+
+ result = numpy.transpose(numpy.dot(matrix, points))
+
+ if perspectiveDivide:
+ mask = result[:, 3] != 0.
+ result[mask] /= result[mask, 3][:, numpy.newaxis]
+
+ return result[:, :3] if dimension == 3 else result
+
@staticmethod
def _prepareVector(vector, w):
"""Add 4th coordinate (w) to vector if missing."""
@@ -317,8 +355,6 @@ class Transform(event.Notifier):
def transformPoint(self, point, direct=True, perspectiveDivide=False):
"""Apply the transform to a point.
- If len(point) == 3, apply perspective divide if possible.
-
:param point: Array-like vector of 3 or 4 coordinates.
:param bool direct: Whether to apply the direct (True, the default)
or inverse (False) transform.
@@ -373,7 +409,7 @@ class Transform(event.Notifier):
_CUBE_CORNERS = numpy.array(list(itertools.product((0., 1.), repeat=3)),
dtype=numpy.float32)
- """Unit cube corners used by :meth:`transformRectangularBox`"""
+ """Unit cube corners used by :meth:`transformBounds`"""
def transformBounds(self, bounds, direct=True):
"""Apply the transform to an axes-aligned rectangular box.
diff --git a/silx/gui/plot3d/scene/utils.py b/silx/gui/plot3d/scene/utils.py
index 3752289..1224f5e 100644
--- a/silx/gui/plot3d/scene/utils.py
+++ b/silx/gui/plot3d/scene/utils.py
@@ -435,6 +435,186 @@ def boxPlaneIntersect(boxVertices, boxLineIndices, planeNorm, planePt):
return points
+def clipSegmentToBounds(segment, bounds):
+ """Clip segment to volume aligned with axes.
+
+ :param numpy.ndarray segment: (p0, p1)
+ :param numpy.ndarray bounds: (lower corner, upper corner)
+ :return: Either clipped (p0, p1) or None if outside volume
+ :rtype: Union[None,List[numpy.ndarray]]
+ """
+ segment = numpy.array(segment, copy=False)
+ bounds = numpy.array(bounds, copy=False)
+
+ p0, p1 = segment
+ # Get intersection points of ray with volume boundary planes
+ # Line equation: P = offset * delta + p0
+ delta = p1 - p0
+ deltaNotZero = numpy.array(delta, copy=True)
+ deltaNotZero[deltaNotZero == 0] = numpy.nan # Invalidated to avoid division by zero
+ offsets = ((bounds - p0) / deltaNotZero).reshape(-1)
+ points = offsets.reshape(-1, 1) * delta + p0
+
+ # Avoid precision errors by using bounds value
+ points.shape = 2, 3, 3 # Reshape 1 point per bound value
+ for dim in range(3):
+ points[:, dim, dim] = bounds[:, dim]
+ points.shape = -1, 3 # Set back to 2D array
+
+ # Find intersection points that are included in the volume
+ mask = numpy.logical_and(numpy.all(bounds[0] <= points, axis=1),
+ numpy.all(points <= bounds[1], axis=1))
+ intersections = numpy.unique(offsets[mask])
+ if len(intersections) != 2:
+ return None
+
+ intersections.sort()
+ # Do p1 first as p0 is need to compute it
+ if intersections[1] < 1: # clip p1
+ segment[1] = intersections[1] * delta + p0
+ if intersections[0] > 0: # clip p0
+ segment[0] = intersections[0] * delta + p0
+ return segment
+
+
+def segmentVolumeIntersect(segment, nbins):
+ """Get bin indices intersecting with segment
+
+ It should work with N dimensions.
+ Coordinate convention (z, y, x) or (x, y, z) should not matter
+ as long as segment and nbins are consistent.
+
+ :param numpy.ndarray segment:
+ Segment end points as a 2xN array of coordinates
+ :param numpy.ndarray nbins:
+ Shape of the volume with same coordinates order as segment
+ :return: List of bins indices as a 2D array or None if no bins
+ :rtype: Union[None,numpy.ndarray]
+ """
+ segment = numpy.asarray(segment)
+ nbins = numpy.asarray(nbins)
+
+ assert segment.ndim == 2
+ assert segment.shape[0] == 2
+ assert nbins.ndim == 1
+ assert segment.shape[1] == nbins.size
+
+ dim = len(nbins)
+
+ bounds = numpy.array((numpy.zeros_like(nbins), nbins))
+ segment = clipSegmentToBounds(segment, bounds)
+ if segment is None:
+ return None # Segment outside volume
+ p0, p1 = segment
+
+ # Get intersections
+
+ # Get coordinates of bin edges crossing the segment
+ clipped = numpy.ceil(numpy.clip(segment, 0, nbins))
+ start = numpy.min(clipped, axis=0)
+ stop = numpy.max(clipped, axis=0) # stop is NOT included
+ edgesByDim = [numpy.arange(start[i], stop[i]) for i in range(dim)]
+
+ # Line equation: P = t * delta + p0
+ delta = p1 - p0
+
+ # Get bin edge/line intersections as sorted points along the line
+ # Get corresponding line parameters
+ t = []
+ if numpy.all(0 <= p0) and numpy.all(p0 <= nbins):
+ t.append([0.]) # p0 within volume, add it
+ t += [(edgesByDim[i] - p0[i]) / delta[i] for i in range(dim) if delta[i] != 0]
+ if numpy.all(0 <= p1) and numpy.all(p1 <= nbins):
+ t.append([1.]) # p1 within volume, add it
+ t = numpy.concatenate(t)
+ t.sort(kind='mergesort')
+
+ # Remove duplicates
+ unique = numpy.ones((len(t),), dtype=bool)
+ numpy.not_equal(t[1:], t[:-1], out=unique[1:])
+ t = t[unique]
+
+ if len(t) < 2:
+ return None # Not enough intersection points
+
+ # bin edges/line intersection points
+ points = t.reshape(-1, 1) * delta + p0
+ centers = (points[:-1] + points[1:]) / 2.
+ bins = numpy.floor(centers).astype(numpy.int)
+ return bins
+
+
+def segmentTrianglesIntersection(segment, triangles):
+ """Check for segment/triangles intersection.
+
+ This is based on signed tetrahedron volume comparison.
+
+ See A. Kensler, A., Shirley, P.
+ Optimizing Ray-Triangle Intersection via Automated Search.
+ Symposium on Interactive Ray Tracing, vol. 0, p33-38 (2006)
+
+ :param numpy.ndarray segment:
+ Segment end points as a 2x3 array of coordinates
+ :param numpy.ndarray triangles:
+ Nx3x3 array of triangles
+ :return: (triangle indices, segment parameter, barycentric coord)
+ Indices of intersected triangles, "depth" along the segment
+ of the intersection point and barycentric coordinates of intersection
+ point in the triangle.
+ :rtype: List[numpy.ndarray]
+ """
+ # TODO triangles from vertices + indices
+ # TODO early rejection? e.g., check segment bbox vs triangle bbox
+ segment = numpy.asarray(segment)
+ assert segment.ndim == 2
+ assert segment.shape == (2, 3)
+
+ triangles = numpy.asarray(triangles)
+ assert triangles.ndim == 3
+ assert triangles.shape[1] == 3
+
+ # Test line/triangles intersection
+ d = segment[1] - segment[0]
+ t0s0 = segment[0] - triangles[:, 0, :]
+ edge01 = triangles[:, 1, :] - triangles[:, 0, :]
+ edge02 = triangles[:, 2, :] - triangles[:, 0, :]
+
+ dCrossEdge02 = numpy.cross(d, edge02)
+ t0s0CrossEdge01 = numpy.cross(t0s0, edge01)
+ volume = numpy.sum(dCrossEdge02 * edge01, axis=1)
+ del edge01
+ subVolumes = numpy.empty((len(triangles), 3), dtype=triangles.dtype)
+ subVolumes[:, 1] = numpy.sum(dCrossEdge02 * t0s0, axis=1)
+ del dCrossEdge02
+ subVolumes[:, 2] = numpy.sum(t0s0CrossEdge01 * d, axis=1)
+ subVolumes[:, 0] = volume - subVolumes[:, 1] - subVolumes[:, 2]
+ intersect = numpy.logical_or(
+ numpy.all(subVolumes >= 0., axis=1), # All positive
+ numpy.all(subVolumes <= 0., axis=1)) # All negative
+ intersect = numpy.where(intersect)[0] # Indices of intersected triangles
+
+ # Get barycentric coordinates
+ barycentric = subVolumes[intersect] / volume[intersect].reshape(-1, 1)
+ del subVolumes
+
+ # Test segment/triangles intersection
+ volAlpha = numpy.sum(t0s0CrossEdge01[intersect] * edge02[intersect], axis=1)
+ t = volAlpha / volume[intersect] # segment parameter of intersected triangles
+ del t0s0CrossEdge01
+ del edge02
+ del volAlpha
+ del volume
+
+ inSegmentMask = numpy.logical_and(t >= 0., t <= 1.)
+ intersect = intersect[inSegmentMask]
+ t = t[inSegmentMask]
+ barycentric = barycentric[inSegmentMask]
+
+ # Sort intersecting triangles by t
+ indices = numpy.argsort(t)
+ return intersect[indices], t[indices], barycentric[indices]
+
+
# Plane #######################################################################
class Plane(event.Notifier):