diff options
author | Alexandre Marie <alexandre.marie@synchrotron-soleil.fr> | 2018-12-17 12:28:45 +0100 |
---|---|---|
committer | Alexandre Marie <alexandre.marie@synchrotron-soleil.fr> | 2018-12-17 12:28:45 +0100 |
commit | c49572a2e90b398e90a43f86b490f27ee6c58de6 (patch) | |
tree | d130cf7dfc3cf73157e1bece8173331bb4bc7156 /silx/gui/plot3d/scene | |
parent | 0bbc8ab933e62c1fa6d548e879ae6d98cbd461f1 (diff) | |
parent | cebdc9244c019224846cb8d2668080fe386a6adc (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.py | 4 | ||||
-rw-r--r-- | silx/gui/plot3d/scene/function.py | 4 | ||||
-rw-r--r-- | silx/gui/plot3d/scene/primitives.py | 2 | ||||
-rw-r--r-- | silx/gui/plot3d/scene/setup.py | 41 | ||||
-rw-r--r-- | silx/gui/plot3d/scene/transform.py | 42 | ||||
-rw-r--r-- | silx/gui/plot3d/scene/utils.py | 180 |
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): |