def generate_projectspace(self, geometry): dtype = self.imagespace.dtype proj_fspace = FunctionSpace(geometry.params, out_dtype=dtype) if not self.imagespace.is_weighted: weighting = None elif (isinstance(self.imagespace.weighting, ConstWeighting) and np.isclose(self.imagespace.weighting.const, self.imagespace.cell_volume)): extent = float(geometry.partition.extent.prod()) size = float(geometry.partition.size) weighting = extent / size else: raise NotImplementedError('unknown weighting of domain') proj_tspace = self.imagespace.tspace_type(geometry.partition.shape, weighting=weighting, dtype=dtype) if geometry.motion_partition.ndim == 0: angle_labels = [] if geometry.motion_partition.ndim == 1: angle_labels = ['$\\varphi$'] elif geometry.motion_partition.ndim == 2: # TODO: check order angle_labels = ['$\\vartheta$', '$\\varphi$'] elif geometry.motion_partition.ndim == 3: # TODO: check order angle_labels = ['$\\vartheta$', '$\\varphi$', '$\\psi$'] else: angle_labels = None if geometry.det_partition.ndim == 1: det_labels = ['$s$'] elif geometry.det_partition.ndim == 2: det_labels = ['$u$', '$v$'] else: det_labels = None if angle_labels is None or det_labels is None: # Fallback for unknown configuration axis_labels = None else: axis_labels = angle_labels + det_labels proj_interp = 'nearest' # proj_interp = kwargs.get('interp', 'nearest') return DiscreteLp(proj_fspace, geometry.partition, proj_tspace, interp=proj_interp, axis_labels=axis_labels)
def _resize_discr(discr, newshp, offset, discr_kwargs): """Return a space based on ``discr`` and ``newshp``. Use the domain of ``discr`` and its partition to create a new uniformly discretized space with ``newshp`` as shape. In axes where ``offset`` is given, it determines the number of added/removed cells to the left. Where ``offset`` is ``None``, the points are distributed evenly to left and right. The ``discr_kwargs`` parameter is passed to `uniform_discr` for further specification of discretization parameters. """ nodes_on_bdry = discr_kwargs.get('nodes_on_bdry', False) if np.shape(nodes_on_bdry) == (): nodes_on_bdry = ([(bool(nodes_on_bdry), bool(nodes_on_bdry))] * discr.ndim) elif discr.ndim == 1 and len(nodes_on_bdry) == 2: nodes_on_bdry = [nodes_on_bdry] elif len(nodes_on_bdry) != discr.ndim: raise ValueError('`nodes_on_bdry` has length {}, expected {}' ''.format(len(nodes_on_bdry), discr.ndim)) dtype = discr_kwargs.pop('dtype', discr.dtype) impl = discr_kwargs.pop('impl', discr.impl) exponent = discr_kwargs.pop('exponent', discr.exponent) interp = discr_kwargs.pop('interp', discr.interp) weighting = discr_kwargs.pop('weighting', discr.weighting) affected = np.not_equal(newshp, discr.shape) ndim = discr.ndim for i in range(ndim): if affected[i] and not discr.is_uniform_byaxis[i]: raise ValueError('cannot resize in non-uniformly discretized ' 'axis {}'.format(i)) grid_min, grid_max = discr.grid.min(), discr.grid.max() cell_size = discr.cell_sides new_minpt, new_maxpt = [], [] for axis, (n_orig, n_new, off, on_bdry) in enumerate( zip(discr.shape, newshp, offset, nodes_on_bdry)): if not affected[axis]: new_minpt.append(discr.min_pt[axis]) new_maxpt.append(discr.max_pt[axis]) continue n_diff = n_new - n_orig if off is None: num_r = n_diff // 2 num_l = n_diff - num_r else: num_r = n_diff - off num_l = off try: on_bdry_l, on_bdry_r = on_bdry except TypeError: on_bdry_l = on_bdry on_bdry_r = on_bdry if on_bdry_l: new_minpt.append(grid_min[axis] - num_l * cell_size[axis]) else: new_minpt.append(grid_min[axis] - (num_l + 0.5) * cell_size[axis]) if on_bdry_r: new_maxpt.append(grid_max[axis] + num_r * cell_size[axis]) else: new_maxpt.append(grid_max[axis] + (num_r + 0.5) * cell_size[axis]) fspace = FunctionSpace(IntervalProd(new_minpt, new_maxpt), out_dtype=dtype) tspace = tensor_space(newshp, dtype=dtype, impl=impl, exponent=exponent, weighting=weighting) # Stack together the (unchanged) nonuniform axes and the (new) uniform # axes in the right order part = uniform_partition([], [], ()) for i in range(ndim): if discr.is_uniform_byaxis[i]: part = part.append( uniform_partition(new_minpt[i], new_maxpt[i], newshp[i], nodes_on_bdry=nodes_on_bdry[i])) else: part = part.append(discr.partition.byaxis[i]) return DiscreteLp(fspace, part, tspace, interp=interp)
def __init__(self, reco_space, geometry, variant, **kwargs): """Initialize a new instance. Parameters ---------- reco_space : `DiscreteLp` Discretized reconstruction space, the domain of the forward operator or the range of the adjoint (back-projection). geometry : `Geometry` Geometry of the transform that contains information about the data structure. variant : {'forward', 'backward'} Variant of the transform, i.e., whether the ray transform or its back-projection should be created. Other Parameters ---------------- impl : {`None`, 'astra_cuda', 'astra_cpu', 'skimage'}, optional Implementation back-end for the transform. Supported back-ends: - ``'astra_cuda'``: ASTRA toolbox, using CUDA, 2D or 3D - ``'astra_cpu'``: ASTRA toolbox using CPU, only 2D - ``'skimage'``: scikit-image, only 2D parallel with square reconstruction space. For the default ``None``, the fastest available back-end is used. interp : {'nearest', 'linear'}, optional Interpolation type for the discretization of the projection space. This has no effect if ``proj_space`` is given explicitly. Default: ``'nearest'`` proj_space : `DiscreteLp`, optional Discretized projection (sinogram) space, the range of the forward operator or the domain of the adjoint (back-projection). Default: Inferred from parameters. use_cache : bool, optional If ``True``, data is cached. This gives a significant speed-up at the expense of a notable memory overhead, both on the GPU and on the CPU, since a full volume and a projection dataset are stored. That may be prohibitive in 3D. Default: True Notes ----- The ASTRA backend is faster if data are given with ``dtype='float32'`` and storage order 'C'. Otherwise copies will be needed. """ variant, variant_in = str(variant).lower(), variant if variant not in ('forward', 'backward'): raise ValueError('`variant` {!r} not understood' ''.format(variant_in)) if variant == 'forward': reco_name = 'domain' proj_name = 'range' else: reco_name = 'range' proj_name = 'domain' if not isinstance(reco_space, DiscreteLp): raise TypeError('`{}` must be a `DiscreteLp` instance, got ' '{!r}'.format(reco_name, reco_space)) if not isinstance(geometry, Geometry): raise TypeError('`geometry` must be a `Geometry` instance, got ' '{!r}'.format(geometry)) # Handle backend choice if not _AVAILABLE_IMPLS: raise RuntimeError('no ray transform back-end available; ' 'this requires 3rd party packages, please ' 'check the install docs') impl = kwargs.pop('impl', None) if impl is None: # Select fastest available if ASTRA_CUDA_AVAILABLE: impl = 'astra_cuda' elif ASTRA_AVAILABLE: impl = 'astra_cpu' elif SKIMAGE_AVAILABLE: impl = 'skimage' else: raise RuntimeError('bad impl') else: impl, impl_in = str(impl).lower(), impl if impl not in _SUPPORTED_IMPL: raise ValueError('`impl` {!r} not understood'.format(impl_in)) if impl not in _AVAILABLE_IMPLS: raise ValueError('{!r} back-end not available'.format(impl)) # Cache for input/output arrays of transforms self.use_cache = kwargs.pop('use_cache', True) # Sanity checks if impl.startswith('astra'): if geometry.ndim > 2 and impl.endswith('cpu'): raise ValueError('`impl` {!r} only works for 2d' ''.format(impl_in)) # Print a warning if the detector midpoint normal vector at any # angle is perpendicular to the geometry axis in parallel 3d # single-axis geometry -- this is broken in some ASTRA versions if (isinstance(geometry, Parallel3dAxisGeometry) and not astra_supports('par3d_det_mid_pt_perp_to_axis')): axis = geometry.axis mid_pt = geometry.det_params.mid_pt for i, angle in enumerate(geometry.angles): if abs(np.dot(axis, geometry.det_to_src(angle, mid_pt))) < 1e-4: warnings.warn( 'angle {}: detector midpoint normal {} is ' 'perpendicular to the geometry axis {} in ' '`Parallel3dAxisGeometry`; this is broken in ' 'ASTRA v{}, please upgrade to v1.8 or later' ''.format(i, geometry.det_to_src(angle, mid_pt), axis, ASTRA_VERSION), RuntimeWarning) break elif impl == 'skimage': if not isinstance(geometry, Parallel2dGeometry): raise TypeError("{!r} backend only supports 2d parallel " 'geometries'.format(impl)) mid_pt = reco_space.domain.mid_pt if not np.allclose(mid_pt, [0, 0]): raise ValueError('`{}` must be centered at (0, 0), got ' 'midpoint {}'.format(reco_name, mid_pt)) shape = reco_space.shape if shape[0] != shape[1]: raise ValueError('`{}.shape` must have equal entries, ' 'got {}'.format(reco_name, shape)) extent = reco_space.domain.extent if extent[0] != extent[1]: raise ValueError('`{}.extent` must have equal entries, ' 'got {}'.format(reco_name, extent)) if reco_space.ndim != geometry.ndim: raise ValueError('`{}.ndim` not equal to `geometry.ndim`: ' '{} != {}'.format(reco_name, reco_space.ndim, geometry.ndim)) self.__geometry = geometry self.__impl = impl # Generate or check projection space proj_space = kwargs.pop('proj_space', None) if proj_space is None: dtype = reco_space.dtype proj_uspace = FunctionSpace(geometry.params, out_dtype=dtype) if isinstance(reco_space.weighting, NoWeighting): weighting = 1.0 elif (isinstance(reco_space.weighting, ConstWeighting) and np.isclose(reco_space.weighting.const, reco_space.cell_volume)): # Approximate cell volume # TODO: find a way to treat angles and detector differently # regarding weighting. While the detector should be uniformly # discretized, the angles do not have to and often are not. # The needed partition property is available since # commit a551190d, but weighting is not adapted yet. # See also issue #286 extent = float(geometry.partition.extent.prod()) size = float(geometry.partition.size) weighting = extent / size else: raise NotImplementedError('unknown weighting of domain') proj_dspace = reco_space.dspace_type(geometry.partition.size, weighting=weighting, dtype=dtype) if geometry.ndim == 2: axis_labels = ['$\\theta$', '$s$'] elif geometry.ndim == 3: axis_labels = ['$\\theta$', '$u$', '$v$'] else: # TODO Add this when we add nd ray transform axis_labels = None proj_interp = kwargs.get('interp', 'nearest') proj_space = DiscreteLp(proj_uspace, geometry.partition, proj_dspace, interp=proj_interp, order=reco_space.order, axis_labels=axis_labels) else: # proj_space was given, checking some stuff if not isinstance(proj_space, DiscreteLp): raise TypeError('`{}` must be a `DiscreteLp` instance, ' 'got {!r}'.format(proj_name, proj_space)) if proj_space.shape != geometry.partition.shape: raise ValueError('`{}.shape` not equal to `geometry.shape`: ' '{} != {}'.format(proj_name, proj_space.shape, geometry.partition.shape)) if proj_space.dtype != reco_space.dtype: raise ValueError('`{}.dtype` not equal to `{}.dtype`: ' '{} != {}'.format(proj_name, reco_name, proj_space.dtype, reco_space.dtype)) # Reserve name for cached properties (used for efficiency reasons) self._adjoint = None self._astra_wrapper = None # Extra kwargs that can be reused for adjoint etc. These must # be retrieved with `get` instead of `pop` above. self._extra_kwargs = kwargs # Finally, initialize the Operator structure if variant == 'forward': super().__init__(domain=reco_space, range=proj_space, linear=True) elif variant == 'backward': super().__init__(domain=proj_space, range=reco_space, linear=True)
def __init__(self, discr_domain, geometry, **kwargs): """Initialize a new instance. Parameters ---------- discr_domain : `DiscreteLp` Discretized space, the domain of the forward projector geometry : `Geometry` Geometry of the transform, containing information about the operator range Other Parameters ---------------- impl : {`None`, 'astra_cuda', 'astra_cpu', 'scikit'}, optional Implementation back-end for the transform. Supported back-ends: * ``'astra_cuda'``: ASTRA toolbox, using CUDA, 2D or 3D * ``'astra_cpu'``: ASTRA toolbox using CPU, only 2D * ``'scikit'``: scikit-image, only 2D parallel with square domain If ``None`` is given, the fastest available back-end is used. interp : {'nearest', 'linear'}, optional Interpolation type for the discretization of the operator range. Default: 'nearest' discr_range : `DiscreteLp`, optional Discretized space, the range of the forward projector. Default: Infered from parameters. use_cache : bool, optional If ``True``, data is cached. Note that this causes notable memory overhead, both on the GPU and on the CPU since a full volume and projection is stored. In the 3D case, some users may want to disable this. Default: True Notes ----- The ASTRA backend is faster if data is given with ``dtype`` 'float32' and storage order 'C'. Otherwise copies will be needed. """ if not isinstance(discr_domain, DiscreteLp): raise TypeError('`discr_domain` {!r} is not a `DiscreteLp`' ' instance'.format(discr_domain)) if not isinstance(geometry, Geometry): raise TypeError('`geometry` {!r} is not a `Geometry` instance' ''.format(geometry)) impl = kwargs.pop('impl', None) if impl is None: # Select fastest available if ASTRA_CUDA_AVAILABLE: impl = 'astra_cuda' elif ASTRA_AVAILABLE: impl = 'astra_cpu' elif SCIKIT_IMAGE_AVAILABLE: impl = 'scikit' else: raise ValueError('no valid `impl` installed') impl, impl_in = str(impl).lower(), impl if impl not in _SUPPORTED_IMPL: raise ValueError('`impl` {!r} not supported' ''.format(impl_in)) self.use_cache = kwargs.pop('use_cache', True) # TODO: sanity checks between impl and discretization impl if impl.startswith('astra'): # TODO: these should be moved somewhere else if not ASTRA_AVAILABLE: raise ValueError("'astra' back-end not available") if impl == 'astra_cuda' and not ASTRA_CUDA_AVAILABLE: raise ValueError("'astra_cuda' back-end not available") if not np.allclose(discr_domain.partition.cell_sides[1:], discr_domain.partition.cell_sides[:-1]): raise ValueError('ASTRA does not support different voxel ' 'sizes per axis, got {}' ''.format(discr_domain.partition.cell_sides)) if geometry.ndim > 2 and impl.endswith('cpu'): raise ValueError('`impl` {}, only works for 2d geometries' ' got {}-d'.format(impl_in, geometry)) elif impl == 'scikit': if not isinstance(geometry, Parallel2dGeometry): raise TypeError("'scikit' backend only supports 2d parallel " 'geometries') mid_pt = discr_domain.domain.mid_pt if not all(mid_pt == [0, 0]): raise ValueError('`discr_domain.domain` needs to be ' 'centered on [0, 0], got {}'.format(mid_pt)) shape = discr_domain.shape if shape[0] != shape[1]: raise ValueError('`discr_domain.shape` needs to be square ' 'got {}'.format(shape)) extent = discr_domain.domain.extent if extent[0] != extent[1]: raise ValueError('`discr_domain.extent` needs to be square ' 'got {}'.format(extent)) # TODO: sanity checks between domain and geometry (ndim, ...) self.__geometry = geometry self.__impl = impl self.kwargs = kwargs discr_range = kwargs.pop('discr_range', None) if discr_range is None: dtype = discr_domain.dspace.dtype # Create a discretized space (operator range) with the same # data-space type as the domain. # TODO: use a ProductSpace structure or find a way to treat # different dimensions differently in DiscreteLp # (i.e. in partitions). range_uspace = FunctionSpace(geometry.params, out_dtype=dtype) # Approximate cell volume # TODO: angles and detector must be handled separately. While the # detector should be uniformly discretized, the angles do not have # to and often are not. extent = float(geometry.partition.extent.prod()) size = float(geometry.partition.size) weight = extent / size range_dspace = discr_domain.dspace_type(geometry.partition.size, weighting=weight, dtype=dtype) if geometry.ndim == 2: axis_labels = ['$\\theta$', '$s$'] elif geometry.ndim == 3: axis_labels = ['$\\theta$', '$u$', '$v$'] else: # TODO Add this when we add nd ray transform. axis_labels = None range_interp = kwargs.get('interp', 'nearest') discr_range = DiscreteLp(range_uspace, geometry.partition, range_dspace, interp=range_interp, order=discr_domain.order, axis_labels=axis_labels) self.backproj = None super().__init__(discr_domain, discr_range, linear=True)
def __init__(self, discr_range, geometry, **kwargs): """Initialize a new instance. Parameters ---------- discr_range : `DiscreteLp` Reconstruction space, the range of the back-projector geometry : `Geometry` The geometry of the transform, contains information about the operator domain Other Parameters ---------------- impl : {'astra_cpu', 'astra_cuda', 'scikit'}, optional Implementation back-end for the transform. Supported back-ends: * ``'astra_cuda'``: ASTRA toolbox, using CUDA, 2D or 3D * ``'astra_cpu'``: ASTRA toolbox using CPU, only 2D * ``'scikit'``: scikit-image, only 2D parallel with square domain If ``None`` is given, the fastest available back-end is used. interp : {'nearest', 'linear'}, optional Interpolation type for the discretization of the operator range. Default: 'nearest' discr_domain : `DiscreteLp`, optional Discretized space, the range of the forward projector. Default: Infered from parameters. use_cache : bool, optional If ``True``, data is cached. Note that this causes notable memory overhead, both on the GPU and on the CPU since a full volume and projection is stored. In the 3D case, some users may want to disable this. """ if not isinstance(discr_range, DiscreteLp): raise TypeError('`discr_range` {!r} is not a `DiscreteLp`' ' instance'.format(discr_range)) if not isinstance(geometry, Geometry): raise TypeError('`geometry` {!r} is not a `Geometry` instance' ''.format(geometry)) impl = kwargs.pop('impl', None) if impl is None: # Select fastest available if ASTRA_CUDA_AVAILABLE: impl = 'astra_cuda' elif ASTRA_AVAILABLE: impl = 'astra_cpu' elif SCIKIT_IMAGE_AVAILABLE: impl = 'scikit' else: raise ValueError('no valid `impl` installed') impl, impl_in = str(impl).lower(), impl if impl not in _SUPPORTED_IMPL: raise ValueError("`impl` '{}' not supported" ''.format(impl_in)) if impl.startswith('astra'): if not ASTRA_AVAILABLE: raise ValueError("'astra' backend not available") if impl == 'astra_cuda' and not ASTRA_CUDA_AVAILABLE: raise ValueError("'astra_cuda' backend not available") if not np.allclose(discr_range.partition.cell_sides[1:], discr_range.partition.cell_sides[:-1], atol=0, rtol=1e-5): raise ValueError('ASTRA does not support different voxel ' 'sizes per axis, got {}' ''.format(discr_range.partition.cell_sides)) self.use_cache = kwargs.pop('use_cache', True) self.__geometry = geometry self.__impl = impl self.kwargs = kwargs discr_domain = kwargs.pop('discr_domain', None) if discr_domain is None: dtype = discr_range.dspace.dtype # Create a discretized space (operator domain) with the same # data-space type as the range. domain_uspace = FunctionSpace(geometry.params, out_dtype=dtype) # Approximate cell volume extent = float(geometry.partition.extent.prod()) size = float(geometry.partition.size) weight = extent / size domain_dspace = discr_range.dspace_type(geometry.partition.size, weighting=weight, dtype=dtype) if geometry.ndim == 2: axis_labels = ['$\\theta$', '$s$'] elif geometry.ndim == 3: axis_labels = ['$\\theta$', '$u$', '$v$'] else: # TODO Add this when we add nd ray transform. axis_labels = None domain_interp = kwargs.get('interp', 'nearest') discr_domain = DiscreteLp(domain_uspace, geometry.partition, domain_dspace, interp=domain_interp, order=discr_range.order, axis_labels=axis_labels) self.ray_trafo = None super().__init__(discr_domain, discr_range, linear=True)