def frommatrix(cls, apart, dpart, det_radius, init_matrix): """Create a `ParallelHoleCollimatorGeometry` using a matrix. This alternative constructor uses a matrix to rotate and translate the default configuration. It is most useful when the transformation to be applied is already given as a matrix. Parameters ---------- apart : 1-dim. `RectPartition` Partition of the parameter interval. dpart : 2-dim. `RectPartition` Partition of the detector parameter set. det_radius : positive float Radius of the circular detector orbit. init_matrix : `array_like`, shape ``(3, 3)`` or ``(3, 4)``, optional Transformation matrix whose left ``(3, 3)`` block is multiplied with the default ``det_pos_init`` and ``det_axes_init`` to determine the new vectors. If present, the fourth column acts as a translation after the initial transformation. The resulting ``det_axes_init`` will be normalized. Returns ------- geometry : `ParallelHoleCollimatorGeometry` The resulting geometry. """ # Get transformation and translation parts from `init_matrix` init_matrix = np.asarray(init_matrix, dtype=float) if init_matrix.shape not in ((3, 3), (3, 4)): raise ValueError('`matrix` must have shape (3, 3) or (3, 4), ' 'got array with shape {}' ''.format(init_matrix.shape)) trafo_matrix = init_matrix[:, :3] translation = init_matrix[:, 3:].squeeze() # Transform the default vectors default_axis = cls._default_config['axis'] # Normalized version, just in case default_orig_to_det_init = ( np.array(cls._default_config['det_pos_init'], dtype=float) / np.linalg.norm(cls._default_config['det_pos_init'])) default_det_axes_init = cls._default_config['det_axes_init'] vecs_to_transform = ((default_orig_to_det_init, ) + default_det_axes_init) transformed_vecs = transform_system(default_axis, None, vecs_to_transform, matrix=trafo_matrix) # Use the standard constructor with these vectors axis, orig_to_det, det_axis_0, det_axis_1 = transformed_vecs if translation.size == 0: kwargs = {} else: kwargs = {'translation': translation} return cls(apart, dpart, det_radius, axis, orig_to_det_init=orig_to_det, det_axes_init=[det_axis_0, det_axis_1], **kwargs)
def frommatrix(cls, apart, dpart, src_radius, det_radius, init_matrix, pitch=0, **kwargs): """Create an instance of `ConeFlatGeometry` using a matrix. This alternative constructor uses a matrix to rotate and translate the default configuration. It is most useful when the transformation to be applied is already given as a matrix. Parameters ---------- apart : 1-dim. `RectPartition` Partition of the parameter interval. dpart : 2-dim. `RectPartition` Partition of the detector parameter set. src_radius : nonnegative float Radius of the source circle. det_radius : nonnegative float Radius of the detector circle. Must be nonzero if ``src_radius`` is zero. init_matrix : `array_like`, shape ``(3, 3)`` or ``(3, 4)``, optional Transformation matrix whose left ``(3, 3)`` block is multiplied with the default ``det_pos_init`` and ``det_axes_init`` to determine the new vectors. If present, the fourth column acts as a translation after the initial transformation. The resulting ``det_axes_init`` will be normalized. pitch : float, optional Constant distance along the rotation axis that a point on the helix traverses when increasing the angle parameter by ``2 * pi``. The default case ``pitch=0`` results in a circular cone beam geometry. Other Parameters ---------------- offset_along_axis : float, optional Offset along the ``axis`` at angle 0. Default: 0. Returns ------- geometry : `ConeFlatGeometry` Examples -------- Map unit vectors ``e_y -> e_z`` and ``e_z -> -e_y``, keeping the right-handedness: >>> apart = odl.uniform_partition(0, 2 * np.pi, 10) >>> dpart = odl.uniform_partition([-1, -1], [1, 1], (20, 20)) >>> matrix = np.array([[1, 0, 0], ... [0, 0, -1], ... [0, 1, 0]]) >>> geom = ConeFlatGeometry.frommatrix( ... apart, dpart, src_radius=5, det_radius=10, pitch=2, ... init_matrix=matrix) >>> geom.axis array([ 0., -1., 0.]) >>> geom.src_to_det_init array([ 0., 0., 1.]) >>> geom.det_axes_init (array([ 1., 0., 0.]), array([ 0., -1., 0.])) Adding a translation with a fourth matrix column: >>> matrix = np.array([[0, 0, -1, 0], ... [0, 1, 0, 1], ... [1, 0, 0, 1]]) >>> geom = ConeFlatGeometry.frommatrix( ... apart, dpart, src_radius=5, det_radius=10, pitch=2, ... init_matrix=matrix) >>> geom.translation array([ 0., 1., 1.]) >>> geom.det_refpoint(0) # (0, 10, 0) + (0, 1, 1) array([ 0., 11., 1.]) """ for key in ('axis', 'src_to_det_init', 'det_axes_init', 'translation'): if key in kwargs: raise TypeError('got unknown keyword argument {!r}' ''.format(key)) # Get transformation and translation parts from `init_matrix` init_matrix = np.asarray(init_matrix, dtype=float) if init_matrix.shape not in ((3, 3), (3, 4)): raise ValueError('`matrix` must have shape (3, 3) or (3, 4), ' 'got array with shape {}' ''.format(init_matrix.shape)) trafo_matrix = init_matrix[:, :3] translation = init_matrix[:, 3:].squeeze() # Transform the default vectors default_axis = cls._default_config['axis'] default_src_to_det_init = cls._default_config['src_to_det_init'] default_det_axes_init = cls._default_config['det_axes_init'] vecs_to_transform = (default_src_to_det_init, ) + default_det_axes_init transformed_vecs = transform_system(default_axis, None, vecs_to_transform, matrix=trafo_matrix) # Use the standard constructor with these vectors axis, src_to_det, det_axis_0, det_axis_1 = transformed_vecs if translation.size == 0: pass else: kwargs['translation'] = translation return cls(apart, dpart, src_radius, det_radius, pitch, axis, src_to_det_init=src_to_det, det_axes_init=[det_axis_0, det_axis_1], **kwargs)
def __init__(self, apart, dpart, src_radius, det_radius, src_to_det_init=(0, 1), **kwargs): """Initialize a new instance. Parameters ---------- apart : 1-dim. `RectPartition` Partition of the angle interval. dpart : 1-dim. `RectPartition` Partition of the detector parameter interval. src_radius : nonnegative float Radius of the source circle. det_radius : nonnegative float Radius of the detector circle. Must be nonzero if ``src_radius`` is zero. src_to_det_init : `array-like` (shape ``(2,)``), optional Initial state of the vector pointing from source to detector reference point. The zero vector is not allowed. Other Parameters ---------------- det_axis_init : `array-like` (shape ``(2,)``), optional Initial axis defining the detector orientation. The default depends on ``src_to_det_init``, see Notes. translation : `array-like`, shape ``(2,)``, optional Global translation of the geometry. This is added last in any method that computes an absolute vector, e.g., `det_refpoint`, and also shifts the center of rotation. Notes ----- In the default configuration, the initial source-to-detector vector is ``(0, 1)``, and the initial detector axis is ``(1, 0)``. If a different ``src_to_det_init`` is chosen, the new default axis is given as a rotation of the original one by a matrix that transforms ``(0, 1)`` to the new (normalized) ``src_to_det_init``. This matrix is calculated with the `rotation_matrix_from_to` function. Expressed in code, we have :: init_rot = rotation_matrix_from_to((0, 1), src_to_det_init) det_axis_init = init_rot.dot((1, 0)) Examples -------- Initialization with default parameters and some radii: >>> apart = odl.uniform_partition(0, 2 * np.pi, 10) >>> dpart = odl.uniform_partition(-1, 1, 20) >>> geom = FanFlatGeometry(apart, dpart, src_radius=1, det_radius=5) >>> geom.src_position(0) array([ 0., -1.]) >>> geom.det_refpoint(0) array([ 0., 5.]) >>> geom.det_point_position(0, 1) # (0, 5) + 1 * (1, 0) array([ 1., 5.]) Checking the default orientation: >>> geom.src_to_det_init array([ 0., 1.]) >>> geom.det_axis_init array([ 1., 0.]) Specifying an initial detector position by default rotates the standard configuration to this position: >>> e_x, e_y = np.eye(2) # standard unit vectors >>> geom = FanFlatGeometry(apart, dpart, src_radius=1, det_radius=5, ... src_to_det_init=(1, 0)) >>> np.allclose(geom.src_to_det_init, e_x) True >>> np.allclose(geom.det_axis_init, -e_y) True >>> geom = FanFlatGeometry(apart, dpart, src_radius=1, det_radius=5, ... src_to_det_init=(0, -1)) >>> np.allclose(geom.src_to_det_init, -e_y) True >>> np.allclose(geom.det_axis_init, -e_x) True The initial detector axis can also be set explicitly: >>> geom = FanFlatGeometry( ... apart, dpart, src_radius=1, det_radius=5, ... src_to_det_init=(1, 0), det_axis_init=(0, 1)) >>> np.allclose(geom.src_to_det_init, e_x) True >>> np.allclose(geom.det_axis_init, e_y) True """ default_src_to_det_init = self._default_config['src_to_det_init'] default_det_axis_init = self._default_config['det_axis_init'] # Handle the initial coordinate system. We need to assign `None` to # the vectors first in order to signalize to the `transform_system` # utility that they should be transformed from default since they # were not explicitly given. det_axis_init = kwargs.pop('det_axis_init', None) if src_to_det_init is not None: self._src_to_det_init_arg = np.asarray(src_to_det_init, dtype=float) else: self._src_to_det_init_arg = None if det_axis_init is not None: self._det_axis_init_arg = np.asarray(det_axis_init, dtype=float) else: self._det_axis_init_arg = None # Compute the transformed system and the transition matrix. We # transform only those vectors that were not explicitly given. vecs_to_transform = [] if det_axis_init is None: vecs_to_transform.append(default_det_axis_init) transformed_vecs = transform_system(src_to_det_init, default_src_to_det_init, vecs_to_transform) transformed_vecs = list(transformed_vecs) src_to_det_init = transformed_vecs.pop(0) if det_axis_init is None: det_axis_init = transformed_vecs.pop(0) assert transformed_vecs == [] # Check and normalize `src_to_det_init`. Detector axes are # normalized in the detector class. if np.array_equiv(src_to_det_init, 0): raise ValueError('`src_to_det_init` cannot be the zero vector') else: src_to_det_init /= np.linalg.norm(src_to_det_init) # Initialize stuff self.__src_to_det_init = src_to_det_init detector = Flat1dDetector(dpart, det_axis_init) translation = kwargs.pop('translation', None) super().__init__(ndim=2, motion_part=apart, detector=detector, translation=translation) self.__src_radius = float(src_radius) if self.src_radius < 0: raise ValueError('source circle radius {} is negative' ''.format(src_radius)) self.__det_radius = float(det_radius) if self.det_radius < 0: raise ValueError('detector circle radius {} is negative' ''.format(det_radius)) if self.src_radius == 0 and self.det_radius == 0: raise ValueError('source and detector circle radii cannot both be ' '0') if self.motion_partition.ndim != 1: raise ValueError('`apart` has dimension {}, expected 1' ''.format(self.motion_partition.ndim)) # Make sure there are no leftover kwargs if kwargs: raise TypeError('got unexpected keyword arguments {}' ''.format(kwargs))
def __init__(self, apart, dpart, src_radius, det_radius, pitch=0, axis=(0, 0, 1), **kwargs): """Initialize a new instance. Parameters ---------- apart : 1-dim. `RectPartition` Partition of the angle interval. dpart : 2-dim. `RectPartition` Partition of the detector parameter rectangle. src_radius : nonnegative float Radius of the source circle. det_radius : nonnegative float Radius of the detector circle. Must be nonzero if ``src_radius`` is zero. pitch : float, optional Constant distance along ``axis`` that a point on the helix traverses when increasing the angle parameter by ``2 * pi``. The default case ``pitch=0`` results in a circular cone beam geometry. axis : `array-like`, shape ``(3,)``, optional Vector defining the fixed rotation axis of this geometry. Other Parameters ---------------- offset_along_axis : float, optional Scalar offset along the ``axis`` at ``angle=0``, i.e., the translation along the axis at angle 0 is ``offset_along_axis * axis``. Default: 0. src_to_det_init : `array-like`, shape ``(2,)``, optional Initial state of the vector pointing from source to detector reference point. The zero vector is not allowed. The default depends on ``axis``, see Notes. det_axes_init : 2-tuple of `array-like`'s (shape ``(2,)``), optional Initial axes defining the detector orientation. The default depends on ``axis``, see Notes. translation : `array-like`, shape ``(3,)``, optional Global translation of the geometry. This is added last in any method that computes an absolute vector, e.g., `det_refpoint`, and also shifts the axis of rotation. Notes ----- In the default configuration, the rotation axis is ``(0, 0, 1)``, the initial source-to-detector direction is ``(0, 1, 0)``, and the default detector axes are ``[(1, 0, 0), (0, 0, 1)]``. If a different ``axis`` is provided, the new default initial position and the new default axes are the computed by rotating the original ones by a matrix that transforms ``(0, 0, 1)`` to the new (normalized) ``axis``. This matrix is calculated with the `rotation_matrix_from_to` function. Expressed in code, we have :: init_rot = rotation_matrix_from_to((0, 0, 1), axis) src_to_det_init = init_rot.dot((0, 1, 0)) det_axes_init[0] = init_rot.dot((1, 0, 0)) det_axes_init[1] = init_rot.dot((0, 0, 1)) Examples -------- Initialization with default parameters and some (arbitrary) choices for pitch and radii: >>> apart = odl.uniform_partition(0, 4 * np.pi, 10) >>> dpart = odl.uniform_partition([-1, -1], [1, 1], (20, 20)) >>> geom = ConeFlatGeometry( ... apart, dpart, src_radius=5, det_radius=10, pitch=2) >>> geom.src_position(0) array([ 0., -5., 0.]) >>> geom.det_refpoint(0) array([ 0., 10., 0.]) >>> np.allclose(geom.src_position(2 * np.pi), ... geom.src_position(0) + (0, 0, 2)) # z shift by pitch True Checking the default orientation: >>> geom.axis array([ 0., 0., 1.]) >>> geom.src_to_det_init array([ 0., 1., 0.]) >>> geom.det_axes_init (array([ 1., 0., 0.]), array([ 0., 0., 1.])) Specifying an axis by default rotates the standard configuration to this position: >>> e_x, e_y, e_z = np.eye(3) # standard unit vectors >>> geom = ConeFlatGeometry( ... apart, dpart, src_radius=5, det_radius=10, pitch=2, ... axis=(0, 1, 0)) >>> np.allclose(geom.axis, e_y) True >>> np.allclose(geom.src_to_det_init, -e_z) True >>> np.allclose(geom.det_axes_init, (e_x, e_y)) True >>> geom = ConeFlatGeometry( ... apart, dpart, src_radius=5, det_radius=10, pitch=2, ... axis=(1, 0, 0)) >>> np.allclose(geom.axis, e_x) True >>> np.allclose(geom.src_to_det_init, e_y) True >>> np.allclose(geom.det_axes_init, (-e_z, e_x)) True The initial source-to-detector vector and the detector axes can also be set explicitly: >>> geom = ConeFlatGeometry( ... apart, dpart, src_radius=5, det_radius=10, pitch=2, ... src_to_det_init=(-1, 0, 0), ... det_axes_init=((0, 1, 0), (0, 0, 1))) >>> np.allclose(geom.axis, e_z) True >>> np.allclose(geom.src_to_det_init, -e_x) True >>> np.allclose(geom.det_axes_init, (e_y, e_z)) True """ default_axis = self._default_config['axis'] default_src_to_det_init = self._default_config['src_to_det_init'] default_det_axes_init = self._default_config['det_axes_init'] # Handle initial coordinate system. We need to assign `None` to # the vectors first since we want to check that `init_matrix` # is not used together with those other parameters. src_to_det_init = kwargs.pop('src_to_det_init', None) det_axes_init = kwargs.pop('det_axes_init', None) # Store some stuff for repr if src_to_det_init is not None: self._src_to_det_init_arg = np.asarray(src_to_det_init, dtype=float) else: self._src_to_det_init_arg = None if det_axes_init is not None: self._det_axes_init_arg = tuple( np.asarray(a, dtype=float) for a in det_axes_init) else: self._det_axes_init_arg = None # Compute the transformed system and the transition matrix. We # transform only those vectors that were not explicitly given. vecs_to_transform = [] if src_to_det_init is None: vecs_to_transform.append(default_src_to_det_init) if det_axes_init is None: vecs_to_transform.extend(default_det_axes_init) transformed_vecs = transform_system(axis, default_axis, vecs_to_transform) transformed_vecs = list(transformed_vecs) axis = transformed_vecs.pop(0) if src_to_det_init is None: src_to_det_init = transformed_vecs.pop(0) if det_axes_init is None: det_axes_init = (transformed_vecs.pop(0), transformed_vecs.pop(0)) assert transformed_vecs == [] # Check and normalize `src_to_det_init`. Detector axes are # normalized in the detector class. if np.linalg.norm(src_to_det_init) <= 1e-10: raise ValueError('`src_to_det_init` norm {} too close to 0' ''.format(np.linalg.norm(src_to_det_init))) else: src_to_det_init /= np.linalg.norm(src_to_det_init) # Initialize stuff self.__src_to_det_init = src_to_det_init AxisOrientedGeometry.__init__(self, axis) detector = Flat2dDetector(dpart, det_axes_init) translation = kwargs.pop('translation', None) super().__init__(ndim=3, motion_part=apart, detector=detector, translation=translation) self.__pitch = float(pitch) self.__offset_along_axis = float(kwargs.pop('offset_along_axis', 0)) self.__src_radius = float(src_radius) if self.src_radius < 0: raise ValueError('source circle radius {} is negative' ''.format(src_radius)) self.__det_radius = float(det_radius) if self.det_radius < 0: raise ValueError('detector circle radius {} is negative' ''.format(det_radius)) if self.src_radius == 0 and self.det_radius == 0: raise ValueError('source and detector circle radii cannot both be ' '0') if self.motion_partition.ndim != 1: raise ValueError('`apart` has dimension {}, expected 1' ''.format(self.motion_partition.ndim)) # Make sure there are no leftover kwargs if kwargs: raise TypeError('got unexpected keyword arguments {}' ''.format(kwargs))
def frommatrix(cls, apart, dpart, src_radius, det_radius, init_matrix): """Create an instance of `FanFlatGeometry` using a matrix. This alternative constructor uses a matrix to rotate and translate the default configuration. It is most useful when the transformation to be applied is already given as a matrix. Parameters ---------- Parameters ---------- apart : 1-dim. `RectPartition` Partition of the angle interval. dpart : 1-dim. `RectPartition` Partition of the detector parameter interval. src_radius : nonnegative float Radius of the source circle. det_radius : nonnegative float Radius of the detector circle. Must be nonzero if ``src_radius`` is zero. init_matrix : `array_like`, shape ``(2, 2)`` or ``(2, 3)``, optional Transformation matrix whose left ``(2, 2)`` block is multiplied with the default ``det_pos_init`` and ``det_axis_init`` to determine the new vectors. If present, the third column acts as a translation after the initial transformation. The resulting ``det_axis_init`` will be normalized. Returns ------- geometry : `FanFlatGeometry` Examples -------- Mirror the second unit vector, creating a left-handed system: >>> apart = odl.uniform_partition(0, np.pi, 10) >>> dpart = odl.uniform_partition(-1, 1, 20) >>> matrix = np.array([[1, 0], ... [0, -1]]) >>> geom = FanFlatGeometry.frommatrix( ... apart, dpart, src_radius=1, det_radius=5, init_matrix=matrix) >>> geom.det_refpoint(0) array([ 0., -5.]) >>> geom.det_axis_init array([ 1., 0.]) >>> geom.translation array([ 0., 0.]) Adding a translation with a third matrix column: >>> matrix = np.array([[1, 0, 1], ... [0, -1, 1]]) >>> geom = FanFlatGeometry.frommatrix( ... apart, dpart, src_radius=1, det_radius=5, init_matrix=matrix) >>> geom.translation array([ 1., 1.]) >>> geom.det_refpoint(0) # (0, -5) + (1, 1) array([ 1., -4.]) """ # Get transformation and translation parts from `init_matrix` init_matrix = np.asarray(init_matrix, dtype=float) if init_matrix.shape not in ((2, 2), (2, 3)): raise ValueError('`matrix` must have shape (2, 2) or (2, 3), ' 'got array with shape {}' ''.format(init_matrix.shape)) trafo_matrix = init_matrix[:, :2] translation = init_matrix[:, 2:].squeeze() # Transform the default vectors default_src_to_det_init = cls._default_config['src_to_det_init'] default_det_axis_init = cls._default_config['det_axis_init'] vecs_to_transform = [default_det_axis_init] transformed_vecs = transform_system(default_src_to_det_init, None, vecs_to_transform, matrix=trafo_matrix) # Use the standard constructor with these vectors src_to_det, det_axis = transformed_vecs if translation.size == 0: kwargs = {} else: kwargs = {'translation': translation} return cls(apart, dpart, src_radius, det_radius, src_to_det, det_axis_init=det_axis, **kwargs)