summaryrefslogtreecommitdiff
path: root/src/silx/gui/plot3d/scene/camera.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/silx/gui/plot3d/scene/camera.py')
-rw-r--r--src/silx/gui/plot3d/scene/camera.py353
1 files changed, 353 insertions, 0 deletions
diff --git a/src/silx/gui/plot3d/scene/camera.py b/src/silx/gui/plot3d/scene/camera.py
new file mode 100644
index 0000000..90de7ed
--- /dev/null
+++ b/src/silx/gui/plot3d/scene/camera.py
@@ -0,0 +1,353 @@
+# coding: utf-8
+# /*##########################################################################
+#
+# Copyright (c) 2015-2018 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 classes to handle a perspective projection in 3D."""
+
+from __future__ import absolute_import, division, unicode_literals
+
+__authors__ = ["T. Vincent"]
+__license__ = "MIT"
+__date__ = "25/07/2016"
+
+
+import numpy
+
+from . import transform
+
+
+# CameraExtrinsic #############################################################
+
+class CameraExtrinsic(transform.Transform):
+ """Transform matrix to handle camera position and orientation.
+
+ :param position: Coordinates of the point of view.
+ :type position: numpy.ndarray-like of 3 float32.
+ :param direction: Sight direction vector.
+ :type direction: numpy.ndarray-like of 3 float32.
+ :param up: Vector pointing upward in the image plane.
+ :type up: numpy.ndarray-like of 3 float32.
+ """
+
+ def __init__(self, position=(0., 0., 0.),
+ direction=(0., 0., -1.),
+ up=(0., 1., 0.)):
+
+ super(CameraExtrinsic, self).__init__()
+ self._position = None
+ self.position = position # set _position
+ self._side = 1., 0., 0.
+ self._up = 0., 1., 0.
+ self._direction = 0., 0., -1.
+ self.setOrientation(direction=direction, up=up) # set _direction, _up
+
+ def _makeMatrix(self):
+ return transform.mat4LookAtDir(self._position,
+ self._direction, self._up)
+
+ def copy(self):
+ """Return an independent copy"""
+ return CameraExtrinsic(self.position, self.direction, self.up)
+
+ def setOrientation(self, direction=None, up=None):
+ """Set the rotation of the point of view.
+
+ :param direction: Sight direction vector or
+ None to keep the current one.
+ :type direction: numpy.ndarray-like of 3 float32 or None.
+ :param up: Vector pointing upward in the image plane or
+ None to keep the current one.
+ :type up: numpy.ndarray-like of 3 float32 or None.
+ :raises RuntimeError: if the direction and up are parallel.
+ """
+ if direction is None: # Use current direction
+ direction = self.direction
+ else:
+ assert len(direction) == 3
+ direction = numpy.array(direction, copy=True, dtype=numpy.float32)
+ direction /= numpy.linalg.norm(direction)
+
+ if up is None: # Use current up
+ up = self.up
+ else:
+ assert len(up) == 3
+ up = numpy.array(up, copy=True, dtype=numpy.float32)
+
+ # Update side and up to make sure they are perpendicular and normalized
+ side = numpy.cross(direction, up)
+ sidenormal = numpy.linalg.norm(side)
+ if sidenormal == 0.:
+ raise RuntimeError('direction and up vectors are parallel.')
+ # Alternative: when one of the input parameter is None, it is
+ # possible to guess correct vectors using previous direction and up
+ side /= sidenormal
+ up = numpy.cross(side, direction)
+ up /= numpy.linalg.norm(up)
+
+ self._side = side
+ self._up = up
+ self._direction = direction
+ self.notify()
+
+ @property
+ def position(self):
+ """Coordinates of the point of view as a numpy.ndarray of 3 float32."""
+ return self._position.copy()
+
+ @position.setter
+ def position(self, position):
+ assert len(position) == 3
+ self._position = numpy.array(position, copy=True, dtype=numpy.float32)
+ self.notify()
+
+ @property
+ def direction(self):
+ """Sight direction (ndarray of 3 float32)."""
+ return self._direction.copy()
+
+ @direction.setter
+ def direction(self, direction):
+ self.setOrientation(direction=direction)
+
+ @property
+ def up(self):
+ """Vector pointing upward in the image plane (ndarray of 3 float32).
+ """
+ return self._up.copy()
+
+ @up.setter
+ def up(self, up):
+ self.setOrientation(up=up)
+
+ @property
+ def side(self):
+ """Vector pointing towards the side of the image plane.
+
+ ndarray of 3 float32"""
+ return self._side.copy()
+
+ def move(self, direction, step=1.):
+ """Move the camera relative to the image plane.
+
+ :param str direction: Direction relative to image plane.
+ One of: 'up', 'down', 'left', 'right',
+ 'forward', 'backward'.
+ :param float step: The step of the pan to perform in the coordinate
+ in which the camera position is defined.
+ """
+ if direction in ('up', 'down'):
+ vector = self.up * (1. if direction == 'up' else -1.)
+ elif direction in ('left', 'right'):
+ vector = self.side * (1. if direction == 'right' else -1.)
+ elif direction in ('forward', 'backward'):
+ vector = self.direction * (1. if direction == 'forward' else -1.)
+ else:
+ raise ValueError('Unsupported direction: %s' % direction)
+
+ self.position += step * vector
+
+ def rotate(self, direction, angle=1.):
+ """First-person rotation of the camera towards the direction.
+
+ :param str direction: Direction of movement relative to image plane.
+ In: 'up', 'down', 'left', 'right'.
+ :param float angle: The angle in degrees of the rotation.
+ """
+ if direction in ('up', 'down'):
+ axis = self.side * (1. if direction == 'up' else -1.)
+ elif direction in ('left', 'right'):
+ axis = self.up * (1. if direction == 'left' else -1.)
+ else:
+ raise ValueError('Unsupported direction: %s' % direction)
+
+ matrix = transform.mat4RotateFromAngleAxis(numpy.radians(angle), *axis)
+ newdir = numpy.dot(matrix[:3, :3], self.direction)
+
+ if direction in ('up', 'down'):
+ # Rotate up to avoid up and new direction to be (almost) co-linear
+ newup = numpy.dot(matrix[:3, :3], self.up)
+ self.setOrientation(newdir, newup)
+ else:
+ # No need to rotate up here as it is the rotation axis
+ self.direction = newdir
+
+ def orbit(self, direction, center=(0., 0., 0.), angle=1.):
+ """Rotate the camera around a point.
+
+ :param str direction: Direction of movement relative to image plane.
+ In: 'up', 'down', 'left', 'right'.
+ :param center: Position around which to rotate the point of view.
+ :type center: numpy.ndarray-like of 3 float32.
+ :param float angle: he angle in degrees of the rotation.
+ """
+ if direction in ('up', 'down'):
+ axis = self.side * (1. if direction == 'down' else -1.)
+ elif direction in ('left', 'right'):
+ axis = self.up * (1. if direction == 'right' else -1.)
+ else:
+ raise ValueError('Unsupported direction: %s' % direction)
+
+ # Rotate viewing direction
+ rotmatrix = transform.mat4RotateFromAngleAxis(
+ numpy.radians(angle), *axis)
+ self.direction = numpy.dot(rotmatrix[:3, :3], self.direction)
+
+ # Rotate position around center
+ center = numpy.array(center, copy=False, dtype=numpy.float32)
+ matrix = numpy.dot(transform.mat4Translate(*center), rotmatrix)
+ matrix = numpy.dot(matrix, transform.mat4Translate(*(-center)))
+ position = numpy.append(self.position, 1.)
+ self.position = numpy.dot(matrix, position)[:3]
+
+ _RESET_CAMERA_ORIENTATIONS = {
+ 'side': ((-1., -1., -1.), (0., 1., 0.)),
+ 'front': ((0., 0., -1.), (0., 1., 0.)),
+ 'back': ((0., 0., 1.), (0., 1., 0.)),
+ 'top': ((0., -1., 0.), (0., 0., -1.)),
+ 'bottom': ((0., 1., 0.), (0., 0., 1.)),
+ 'right': ((-1., 0., 0.), (0., 1., 0.)),
+ 'left': ((1., 0., 0.), (0., 1., 0.))
+ }
+
+ def reset(self, face=None):
+ """Reset the camera position to pre-defined orientations.
+
+ :param str face: The direction of the camera in:
+ side, front, back, top, bottom, right, left.
+ """
+ if face not in self._RESET_CAMERA_ORIENTATIONS:
+ raise ValueError('Unsupported face: %s' % face)
+
+ distance = numpy.linalg.norm(self.position)
+ direction, up = self._RESET_CAMERA_ORIENTATIONS[face]
+ self.setOrientation(direction, up)
+ self.position = - self.direction * distance
+
+
+class Camera(transform.Transform):
+ """Combination of camera projection and position.
+
+ See :class:`Perspective` and :class:`CameraExtrinsic`.
+
+ :param float fovy: Vertical field-of-view in degrees.
+ :param float near: The near clipping plane Z coord (strictly positive).
+ :param float far: The far clipping plane Z coord (> near).
+ :param size:
+ Viewport's size used to compute the aspect ratio (width, height).
+ :type size: 2-tuple of float
+ :param position: Coordinates of the point of view.
+ :type position: numpy.ndarray-like of 3 float32.
+ :param direction: Sight direction vector.
+ :type direction: numpy.ndarray-like of 3 float32.
+ :param up: Vector pointing upward in the image plane.
+ :type up: numpy.ndarray-like of 3 float32.
+ """
+
+ def __init__(self, fovy=30., near=0.1, far=1., size=(1., 1.),
+ position=(0., 0., 0.),
+ direction=(0., 0., -1.), up=(0., 1., 0.)):
+ super(Camera, self).__init__()
+ self._intrinsic = transform.Perspective(fovy, near, far, size)
+ self._intrinsic.addListener(self._transformChanged)
+ self._extrinsic = CameraExtrinsic(position, direction, up)
+ self._extrinsic.addListener(self._transformChanged)
+
+ def _makeMatrix(self):
+ return numpy.dot(self.intrinsic.matrix, self.extrinsic.matrix)
+
+ def _transformChanged(self, source):
+ """Listener of intrinsic and extrinsic camera parameters instances."""
+ if source is not self:
+ self.notify()
+
+ def resetCamera(self, bounds):
+ """Change camera to have the bounds in the viewing frustum.
+
+ It updates the camera position and depth extent.
+ Camera sight direction and up are not affected.
+
+ :param bounds: The axes-aligned bounds to include.
+ :type bounds: numpy.ndarray: ((xMin, yMin, zMin), (xMax, yMax, zMax))
+ """
+
+ center = 0.5 * (bounds[0] + bounds[1])
+ radius = numpy.linalg.norm(0.5 * (bounds[1] - bounds[0]))
+ if radius == 0.: # bounds are all collapsed
+ radius = 1.
+
+ if isinstance(self.intrinsic, transform.Perspective):
+ # Get the viewpoint distance from the bounds center
+ minfov = numpy.radians(self.intrinsic.fovy)
+ width, height = self.intrinsic.size
+ if width < height:
+ minfov *= width / height
+
+ offset = radius / numpy.sin(0.5 * minfov)
+
+ # Update camera
+ self.extrinsic.position = \
+ center - offset * self.extrinsic.direction
+ self.intrinsic.setDepthExtent(offset - radius, offset + radius)
+
+ elif isinstance(self.intrinsic, transform.Orthographic):
+ # Y goes up
+ self.intrinsic.setClipping(
+ left=center[0] - radius,
+ right=center[0] + radius,
+ bottom=center[1] - radius,
+ top=center[1] + radius)
+
+ # Update camera
+ self.extrinsic.position = 0, 0, 0
+ self.intrinsic.setDepthExtent(center[2] - radius,
+ center[2] + radius)
+ else:
+ raise RuntimeError('Unsupported camera: %s' % self.intrinsic)
+
+ @property
+ def intrinsic(self):
+ """Intrinsic camera parameters, i.e., projection matrix."""
+ return self._intrinsic
+
+ @intrinsic.setter
+ def intrinsic(self, intrinsic):
+ self._intrinsic.removeListener(self._transformChanged)
+ self._intrinsic = intrinsic
+ self._intrinsic.addListener(self._transformChanged)
+
+ @property
+ def extrinsic(self):
+ """Extrinsic camera parameters, i.e., position and orientation."""
+ return self._extrinsic
+
+ def move(self, *args, **kwargs):
+ """See :meth:`CameraExtrinsic.move`."""
+ self.extrinsic.move(*args, **kwargs)
+
+ def rotate(self, *args, **kwargs):
+ """See :meth:`CameraExtrinsic.rotate`."""
+ self.extrinsic.rotate(*args, **kwargs)
+
+ def orbit(self, *args, **kwargs):
+ """See :meth:`CameraExtrinsic.orbit`."""
+ self.extrinsic.orbit(*args, **kwargs)