def surface_measure(self, param): """Density function of the surface measure. This is the default implementation relying on the `surface_deriv` method. For a detector with `ndim` equal to 1, the density is given by the `Arc length`_, for a surface with `ndim` 2 in a 3D space, it is the length of the cross product of the partial derivatives of the parametrization, see Wikipedia's `Surface area`_ article. Parameters ---------- param : `array-like` or sequence Parameter value(s) at which to evaluate. If ``ndim >= 2``, a sequence of length `ndim` must be provided. Returns ------- measure : float or `numpy.ndarray` The density value(s) at the given parameter(s). If a single parameter is provided, a float is returned. Otherwise, an array is returned with shape - ``param.shape`` if `ndim` is 1, - ``broadcast(*param).shape`` otherwise. References ---------- .. _Arc length: https://en.wikipedia.org/wiki/Curve#Lengths_of_curves .. _Surface area: https://en.wikipedia.org/wiki/Surface_area """ # Checking is done by `surface_deriv` if self.ndim == 1: scalar_out = (np.shape(param) == ()) measure = np.linalg.norm(self.surface_deriv(param), axis=-1) if scalar_out: measure = float(measure) return measure elif self.ndim == 2 and self.space_ndim == 3: scalar_out = (np.shape(param) == (2, )) deriv = self.surface_deriv(param) if deriv.ndim > 2: # Vectorized, need to reshape (N, 2, 3) to (2, N, 3) deriv = moveaxis(deriv, -2, 0) cross = np.cross(*deriv, axis=-1) measure = np.linalg.norm(cross, axis=-1) if scalar_out: measure = float(measure) return measure else: raise NotImplementedError( 'no default implementation of `surface_measure` available ' 'for `ndim={}` and `space_ndim={}`' ''.format(self.ndim, self.space_ndim))
def surface_normal(self, param): """Unit vector perpendicular to the detector surface at ``param``. The orientation is chosen as follows: - In 2D, the system ``(normal, tangent)`` should be right-handed. - In 3D, the system ``(tangent[0], tangent[1], normal)`` should be right-handed. Here, ``tangent`` is the return value of `surface_deriv` at ``param``. Parameters ---------- param : `array-like` or sequence Parameter value(s) at which to evaluate. If ``ndim >= 2``, a sequence of length `ndim` must be provided. Returns ------- normal : `numpy.ndarray` Unit vector(s) perpendicular to the detector surface at ``param``. If ``param`` is a single parameter, an array of shape ``(space_ndim,)`` representing a single vector is returned. Otherwise the shape of the returned array is - ``param.shape + (space_ndim,)`` if `ndim` is 1, - ``param.shape[:-1] + (space_ndim,)`` otherwise. """ # Checking is done by `surface_deriv` if self.ndim == 1 and self.space_ndim == 2: return -perpendicular_vector(self.surface_deriv(param)) elif self.ndim == 2 and self.space_ndim == 3: deriv = self.surface_deriv(param) if deriv.ndim > 2: # Vectorized, need to reshape (N, 2, 3) to (2, N, 3) deriv = moveaxis(deriv, -2, 0) normal = np.cross(*deriv, axis=-1) normal /= np.linalg.norm(normal, axis=-1, keepdims=True) return normal else: raise NotImplementedError( 'no default implementation of `surface_normal` available ' 'for `ndim = {}` and `space_ndim = {}`' ''.format(self.ndim, self.space_ndim))
def astra_parallel_3d_geom_to_vec(geometry): """Create vectors for ASTRA projection geometries from ODL geometry. The 3D vectors are used to create an ASTRA projection geometry for parallel beam geometries, see ``'parallel3d_vec'`` in the `ASTRA projection geometry documentation`_. Each row of the returned vectors corresponds to a single projection and consists of :: (rayX, rayY, rayZ, dX, dY, dZ, uX, uY, uZ, vX, vY, vZ) with - ``ray``: the ray direction - ``d`` : the center of the detector - ``u`` : the vector from detector pixel ``(0,0)`` to ``(0,1)`` - ``v`` : the vector from detector pixel ``(0,0)`` to ``(1,0)`` Parameters ---------- geometry : `Geometry` ODL projection geometry from which to create the ASTRA geometry. Returns ------- vectors : `numpy.ndarray` Array of shape ``(num_angles, 12)`` containing the vectors. References ---------- .. _ASTRA projection geometry documentation: http://www.astra-toolbox.com/docs/geom3d.html#projection-geometries """ angles = geometry.angles mid_pt = geometry.det_params.mid_pt vectors = np.zeros((angles.shape[-1], 12)) # Ray direction = -(detector-to-source normal vector) vectors[:, 0:3] = -geometry.det_to_src(angles, mid_pt) # Center of the detector in 3D space vectors[:, 3:6] = geometry.det_point_position(angles, mid_pt) # Vectors from detector pixel (0, 0) to (1, 0) and (0, 0) to (0, 1) # `det_axes` gives shape (N, 2, 3), swap to get (2, N, 3) det_axes = moveaxis(geometry.det_axes(angles), -2, 0) px_sizes = geometry.det_partition.cell_sides # Swap detector axes to have better memory layout in projection data. # ASTRA produces `(v, theta, u)` layout, and to map to ODL layout # `(theta, u, v)` a complete roll must be performed, which is the # worst case (compeltely discontiguous). # Instead we swap `u` and `v`, resulting in the effective ASTRA result # `(u, theta, v)`. Here we only need to swap axes 0 and 1, which # keeps at least contiguous blocks in `v`. vectors[:, 9:12] = det_axes[0] * px_sizes[0] vectors[:, 6:9] = det_axes[1] * px_sizes[1] # ASTRA has (z, y, x) axis convention, in contrast to (x, y, z) in ODL, # so we need to adapt to this by changing the order. new_ind = [] for i in range(4): new_ind += [2 + 3 * i, 1 + 3 * i, 0 + 3 * i] vectors = vectors[:, new_ind] return vectors