def __init__(self, apart, dpart, 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 interval axis : `array-like`, shape ``(3,)``, optional Fixed rotation axis defined by a 3-element vector det_init_pos : `array-like`, shape ``(3,)``, optional Initial position of the detector reference point. The zero vector is only allowed if ``det_init_axes`` is explicitly given. By default, a `perpendicular_vector` to ``axis`` is used. det_init_axes : 2-tuple of `array-like`'s (shape ``(3,)``), optional Initial axes defining the detector orientation. By default, the normalized cross product of ``axis`` and ``det_init_pos`` is used as first axis and ``axis`` as second. """ AxisOrientedGeometry.__init__(self, axis) det_init_pos = kwargs.pop('det_init_pos', perpendicular_vector(axis)) det_init_axes = kwargs.pop('det_init_axes', None) if det_init_axes is None: if np.linalg.norm(det_init_pos) <= 1e-10: raise ValueError('initial detector position {} is close to ' 'zero. This is only allowed for explicit ' '`det_init_axes`.'.format(det_init_pos)) det_init_axis_0 = np.cross(self.axis, det_init_pos) det_init_axis_0 /= np.linalg.norm(det_init_axis_0) det_init_axes = (det_init_axis_0, axis) self.det_init_axes = det_init_axes detector = Flat2dDetector(part=dpart, axes=det_init_axes) super().__init__(ndim=3, apart=apart, detector=detector, det_init_pos=det_init_pos) if self.motion_partition.ndim != 1: raise ValueError('`apart` has dimension {}, expected 1' ''.format(self.motion_partition.ndim))
def __init__(self, apart, dpart, **kwargs): """Initialize a new instance. Parameters ---------- apart : 2- or 3-dim. `RectPartition` Partition of the angle parameter set dpart : 2-dim. `RectPartition` Partition of the detector parameter interval det_init_pos : `array-like`, shape ``(3,)``, optional Initial position of the detector reference point. The zero vector is only allowed if ``det_init_axes`` is explicitly given. Default: ``(1, 0, 0)`` det_init_axes : 2-tuple of `array-like`'s (shape ``(3,)``), optional Initial axes defining the detector orientation. By default, a normalized `perpendicular_vector` to ``det_init_pos`` is taken as first axis, and the normalized cross product of these two as second. """ det_init_pos = kwargs.pop('det_init_pos', (1.0, 0.0, 0.0)) det_init_axes = kwargs.pop('det_init_axes', None) if det_init_axes is None: if np.linalg.norm(det_init_pos) <= 1e-10: raise ValueError('`det_init_pos` {} is close to ' 'zero. This is only allowed for explicit ' '`det_init_axes`.'.format(det_init_pos)) det_init_axis_0 = perpendicular_vector(det_init_pos) det_init_axis_1 = np.cross(det_init_pos, det_init_axis_0) det_init_axes = (det_init_axis_0, det_init_axis_1) detector = Flat2dDetector(part=dpart, axes=det_init_axes) super().__init__(ndim=3, apart=apart, detector=detector, det_init_pos=det_init_pos) if self.motion_partition.ndim not in (2, 3): raise ValueError('`apart` has dimension {}, expected ' '2 or 3'.format(self.motion_partition.ndim))
def __init__(self, apart, dpart, src_radius, det_radius, pitch, 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 pitch : float Constant vertical distance that a point on the helix traverses when increasing the angle parameter by ``2 * pi`` axis : `array-like`, shape ``(3,)``, optional Fixed rotation axis, the symmetry axis of the helix Other Parameters ---------------- 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. By default, a `perpendicular_vector` to ``axis`` is used. det_init_axes : 2-tuple of `array-like`'s (shape ``(2,)``), optional Initial axes defining the detector orientation. By default, the normalized cross product of ``axis`` and ``src_to_det_init`` is used as first axis and ``axis`` as second. pitch_offset : float, optional Offset along the ``axis`` at ``angle=0`` """ AxisOrientedGeometry.__init__(self, axis) src_to_det_init = kwargs.pop('src_to_det_init', perpendicular_vector(self.axis)) if np.linalg.norm(src_to_det_init) <= 1e-10: raise ValueError('initial source to detector vector {} is too ' 'close to zero'.format(src_to_det_init)) self._src_to_det_init = (np.array(src_to_det_init) / np.linalg.norm(src_to_det_init)) det_init_axes = kwargs.pop('det_init_axes', None) if det_init_axes is None: det_init_axis_0 = np.cross(self.axis, self._src_to_det_init) det_init_axes = (det_init_axis_0, axis) detector = Flat2dDetector(dpart, det_init_axes) super().__init__(ndim=3, motion_part=apart, detector=detector) self._pitch = float(pitch) self._pitch_offset = float(kwargs.pop('pitch_offset', 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')
def __init__(self, apart, dpart, 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. axis : `array-like`, shape ``(3,)``, optional Vector defining the fixed rotation axis of this geometry. Other Parameters ---------------- det_pos_init : `array-like`, shape ``(3,)``, optional Initial position of the detector reference point. The default depends on ``axis``, see Notes. det_axes_init : 2-tuple of `array-like`'s (shape ``(3,)``), 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 detector reference point position 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) det_pos_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: >>> apart = odl.uniform_partition(0, np.pi, 10) >>> dpart = odl.uniform_partition([-1, -1], [1, 1], (20, 20)) >>> geom = Parallel3dAxisGeometry(apart, dpart) >>> geom.det_refpoint(0) array([ 0., 1., 0.]) >>> geom.det_point_position(0, [-1, 1]) array([-1., 1., 1.]) Checking the default orientation: >>> e_x, e_y, e_z = np.eye(3) # standard unit vectors >>> np.allclose(geom.axis, e_z) True >>> np.allclose(geom.det_pos_init, e_y) True >>> np.allclose(geom.det_axes_init, (e_x, e_z)) True Specifying an axis by default rotates the standard configuration to this position: >>> geom = Parallel3dAxisGeometry(apart, dpart, axis=(0, 1, 0)) >>> np.allclose(geom.axis, e_y) True >>> np.allclose(geom.det_pos_init, -e_z) True >>> np.allclose(geom.det_axes_init, (e_x, e_y)) True >>> geom = Parallel3dAxisGeometry(apart, dpart, axis=(1, 0, 0)) >>> np.allclose(geom.axis, e_x) True >>> np.allclose(geom.det_pos_init, e_y) True >>> np.allclose(geom.det_axes_init, (-e_z, e_x)) True The initial detector position and axes can also be set explicitly: >>> geom = Parallel3dAxisGeometry( ... apart, dpart, det_pos_init=(-1, 0, 0), ... det_axes_init=((0, 1, 0), (0, 0, 1))) >>> np.allclose(geom.axis, e_z) True >>> np.allclose(geom.det_pos_init, -e_x) True >>> np.allclose(geom.det_axes_init, (e_y, e_z)) True """ default_axis = self._default_config['axis'] default_det_pos_init = self._default_config['det_pos_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. det_pos_init = kwargs.pop('det_pos_init', None) det_axes_init = kwargs.pop('det_axes_init', None) # Store some stuff for repr if det_pos_init is not None: self._det_pos_init_arg = np.asarray(det_pos_init, dtype=float) else: self._det_pos_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 det_pos_init is None: vecs_to_transform.append(default_det_pos_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 det_pos_init is None: det_pos_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 == [] # Translate the absolute vectors by the given translation translation = np.asarray(kwargs.pop('translation', (0, 0, 0)), dtype=float) det_pos_init += translation # Initialize stuff. Normalization of the detector axis happens in # the detector class. AxisOrientedGeometry.__init__(self, axis) detector = Flat2dDetector(dpart, det_axes_init) super().__init__(ndim=3, apart=apart, detector=detector, det_pos_init=det_pos_init, translation=translation) 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 curved_to_flat(detector): """Transforms a curved detector to a flat. Parameters ---------- detector : `CircularDetector`, `CylindricalDetector` or `SphericalDetector` The detector on which the data was sampled. Returns ------- transformed_detector : `Flat1dDetector` or `Flat2dDetector` A new transformed detector. Examples -------- Transforming a circular detector with range [-pi/4, pi/4] to a flat with range [-1, 1]. >>> part = odl.uniform_partition(-np.pi / 4, np.pi / 4, 3, ... nodes_on_bdry=True) >>> det = odl.tomo.CircularDetector(part, axis=[1, 0], radius=1) >>> new_det = curved_to_flat(det) >>> new_det Flat1dDetector( uniform_partition(-1.0, 1.0, 3, nodes_on_bdry=True), axis='[ 1., 0.]' ) Transforming a cylindrical detector with radius 1 and height 2 to a flat with height 2sqrt(2) (since edge point are projected higher). >>> part = odl.uniform_partition([-np.pi / 4, -1], [np.pi / 4, 1], (3, 2), ... nodes_on_bdry=True) >>> det = odl.tomo.CylindricalDetector(part, ... axes=[[1, 0, 0], [0, 0, 1]], ... radius=1) >>> new_det = curved_to_flat(det) >>> new_det Flat2dDetector( uniform_partition([-1. , -1.4142], [ 1. , 1.4142], (3, 2), nodes_on_bdry=True), axes=('[ 1., 0., 0.]', '[ 0., 0., 1.]') ) """ part = detector.partition if isinstance(detector, CircularDetector): min_pt = detector.radius * np.tan(part.min_pt) max_pt = detector.radius * np.tan(part.max_pt) transformed_part = uniform_partition(min_pt, max_pt, part.shape, nodes_on_bdry=part.nodes_on_bdry) transformed_det = Flat1dDetector(transformed_part, detector.axis, check_bounds=detector.check_bounds) elif isinstance(detector, CylindricalDetector): r = detector.radius R = r / np.minimum(np.cos(part.min_pt[0]), np.cos(part.max_pt[0])) min_pt = [r * np.tan(part.min_pt[0]), R / r * part.min_pt[1]] max_pt = [r * np.tan(part.max_pt[0]), R / r * part.max_pt[1]] transformed_part = uniform_partition(min_pt, max_pt, part.shape, nodes_on_bdry=part.nodes_on_bdry) transformed_det = Flat2dDetector(transformed_part, detector.axes, check_bounds=detector.check_bounds) elif isinstance(detector, SphericalDetector): r = detector.radius R = r / np.minimum(np.cos(part.min_pt[0]), np.cos(part.max_pt[0])) min_pt = [r * np.tan(part.min_pt[0]), R * np.tan(part.min_pt[1])] max_pt = [r * np.tan(part.max_pt[0]), R * np.tan(part.max_pt[1])] transformed_part = uniform_partition(min_pt, max_pt, part.shape, nodes_on_bdry=part.nodes_on_bdry) transformed_det = Flat2dDetector(transformed_part, detector.axes, check_bounds=detector.check_bounds) else: raise ValueError('Detector must be curved, got '.format(detector)) return transformed_det