def _call_real(self, x_real, out_real): """Real-space forward projection for the current set-up. This method also sets ``self._astra_projector`` for ``impl='astra_cuda'`` and enabled cache. """ if self.impl.startswith('astra'): backend, data_impl = self.impl.split('_') if data_impl == 'cpu': return astra_cpu_forward_projector(x_real, self.geometry, self.range.real_space, out_real) elif data_impl == 'cuda': if self._astra_wrapper is None: astra_wrapper = AstraCudaProjectorImpl( self.geometry, self.domain.real_space, self.range.real_space) if self.use_cache: self._astra_wrapper = astra_wrapper else: astra_wrapper = self._astra_wrapper return astra_wrapper.call_forward(x_real, out_real) else: # Should never happen raise RuntimeError('bad `impl` {!r}'.format(self.impl)) elif self.impl == 'skimage': return skimage_radon_forward(x_real, self.geometry, self.range.real_space, out_real) else: # Should never happen raise RuntimeError('bad `impl` {!r}'.format(self.impl))
def _call(self, x, out=None): """Forward project ``x`` and store the result in ``out`` if given.""" if self.impl.startswith('astra'): backend, data_impl = self.impl.split('_') if data_impl == 'cpu': return astra_cpu_forward_projector(x, self.geometry, self.range, out) elif data_impl == 'cuda': proj = getattr(self, 'astra_projector', None) if proj is None: self.astra_projector = AstraCudaProjectorImpl( self.geometry, self.domain, self.range, use_cache=self.use_cache) return self.astra_projector.call_forward(x, out) else: # Should never happen raise RuntimeError('implementation info is inconsistent') elif self.impl == 'scikit': return scikit_radon_forward(x, self.geometry, self.range, out) else: # Should never happen raise RuntimeError('implementation info is inconsistent')
def _call_real(self, x_real, angle_array, out_real): """Real-space forward projection for the current set-up.""" geometry = self.spacegeometry(angle_array) partialrange = self.range(geometry) partialdomain = self.domain(geometry) if self.impl.startswith('astra'): backend, data_impl = self.impl.split('_') if data_impl == 'cpu': return astra_cpu_forward_projector(x_real, geometry, partialrange.real_space, out_real) elif data_impl == 'cuda': # if self._astra_wrapper is None: # astra_wrapper = AstraCudaProjectorImpl( # geometry, partialdomain.real_space, # partialrange.real_space) # if self.use_cache: # self._astra_wrapper = astra_wrapper # else: # print("ever here?") # astra_wrapper = self._astra_wrapper astra_wrapper = AstraCudaProjectorImpl( geometry, partialdomain.real_space, partialrange.real_space) return astra_wrapper.call_forward(x_real, out_real) else: # Should never happen raise RuntimeError('bad `impl` {!r}'.format(self.impl)) else: # Should never happen raise RuntimeError('bad `impl` {!r}'.format(self.impl))
class RayTransform(Operator): """Discrete Ray transform between L^p spaces.""" 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) @property def impl(self): """Implementation back-end for evaluation of this operator.""" return self.__impl @property def geometry(self): """Geometry of this operator.""" return self.__geometry def _call(self, x, out=None): """Forward project ``x`` and store the result in ``out`` if given.""" if self.impl.startswith('astra'): backend, data_impl = self.impl.split('_') if data_impl == 'cpu': return astra_cpu_forward_projector(x, self.geometry, self.range, out) elif data_impl == 'cuda': proj = getattr(self, 'astra_projector', None) if proj is None: self.astra_projector = AstraCudaProjectorImpl( self.geometry, self.domain, self.range, use_cache=self.use_cache) return self.astra_projector.call_forward(x, out) else: # Should never happen raise RuntimeError('implementation info is inconsistent') elif self.impl == 'scikit': return scikit_radon_forward(x, self.geometry, self.range, out) else: # Should never happen raise RuntimeError('implementation info is inconsistent') @property def adjoint(self): """Adjoint of this operator. Returns ------- adjoint : `RayBackProjection` """ if self.backproj is not None: return self.backproj kwargs = self.kwargs.copy() kwargs['discr_domain'] = self.range self.backproj = RayBackProjection(self.domain, self.geometry, impl=self.impl, use_cache=self.use_cache, **kwargs) return self.backproj
class RayTransform(RayTransformBase): """Discrete Ray transform between L^p spaces.""" def __init__(self, domain, geometry, **kwargs): """Initialize a new instance. Parameters ---------- domain : `DiscreteLp` Discretized reconstruction space, the domain of the forward projector. geometry : `Geometry` Geometry of the transform, containing information about the operator range (projection/sinogram space). 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 operator range. This has no effect if ``range`` is given explicitly. Default: ``'nearest'`` range : `DiscreteLp`, optional Discretized projection (sinogram) space, the range of the forward projector. 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 is given with ``dtype`` 'float32' and storage order 'C'. Otherwise copies will be needed. """ range = kwargs.pop('range', None) super().__init__(reco_space=domain, proj_space=range, geometry=geometry, variant='forward', **kwargs) def _call_real(self, x_real, out_real): """Real-space forward projection for the current set-up. This method also sets ``self._astra_projector`` for ``impl='astra_cuda'`` and enabled cache. """ if self.impl.startswith('astra'): backend, data_impl = self.impl.split('_') if data_impl == 'cpu': return astra_cpu_forward_projector(x_real, self.geometry, self.range.real_space, out_real) elif data_impl == 'cuda': if self._astra_wrapper is None: self._astra_wrapper = AstraCudaProjectorImpl( self.geometry, self.domain.real_space, self.range.real_space, self.use_cache) return self._astra_wrapper.call_forward(x_real, out_real) else: # Should never happen raise RuntimeError('bad `impl` {!r}'.format(self.impl)) elif self.impl == 'skimage': return skimage_radon_forward(x_real, self.geometry, self.range.real_space, out_real) else: # Should never happen raise RuntimeError('bad `impl` {!r}'.format(self.impl)) @property def adjoint(self): """Adjoint of this operator. Returns ------- adjoint : `RayBackProjection` """ if self._adjoint is not None: return self._adjoint kwargs = self._extra_kwargs.copy() kwargs['domain'] = self.range self._adjoint = RayBackProjection(self.domain, self.geometry, impl=self.impl, use_cache=self.use_cache, **kwargs) return self._adjoint
class RayTransform(Operator): """Discrete Ray transform between L^p spaces.""" 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) @property def impl(self): """Implementation back-end for evaluation of this operator.""" return self.__impl @property def geometry(self): """Geometry of this operator.""" return self.__geometry def _call(self, x, out=None): """Forward project ``x`` and store the result in ``out`` if given.""" if self.impl.startswith('astra'): backend, data_impl = self.impl.split('_') if data_impl == 'cpu': return astra_cpu_forward_projector(x, self.geometry, self.range, out) elif data_impl == 'cuda': proj = getattr(self, 'astra_projector', None) if proj is None: self.astra_projector = AstraCudaProjectorImpl( self.geometry, self.domain, self.range, use_cache=self.use_cache) return self.astra_projector.call_forward(x, out) else: # Should never happen raise RuntimeError('implementation info is inconsistent') elif self.impl == 'scikit': return scikit_radon_forward(x, self.geometry, self.range, out) else: # Should never happen raise RuntimeError('implementation info is inconsistent') @property def adjoint(self): """Adjoint of this operator. Returns ------- adjoint : `RayBackProjection` """ if self.backproj is not None: return self.backproj kwargs = self.kwargs.copy() kwargs['discr_domain'] = self.range self.backproj = RayBackProjection(self.domain, self.geometry, impl=self.impl, use_cache=self.use_cache, **kwargs) return self.backproj