# -*- coding: utf-8 -*-
"""
Tomographic projector backends.
@author: Nicola VIGANĂ’, Computational Imaging group, CWI, The Netherlands,
and ESRF - The European Synchrotron, Grenoble, France
"""
import numpy as np
import skimage
import skimage.transform as skt
from . import filters
from .models import ProjectionGeometry, VolumeGeometry
from typing import Optional, Sequence, Union, Mapping, List
from numpy.typing import ArrayLike, NDArray
from abc import ABC, abstractmethod
try:
import astra
has_astra = True
has_cuda = astra.astra.use_cuda()
if not has_cuda:
print(
"WARNING: CUDA is not available. Only 2D operations on CPU are available (scikit-image will be used as default)."
)
except ImportError:
has_astra = False
has_cuda = False
print("WARNING: ASTRA is not available. Only 2D operations on CPU are available (scikit-image will be used).")
if has_cuda:
try:
import astra.experimental
has_astra_direct = True
except ImportError:
has_astra_direct = False
print("WARNING: the experimental ASTRA direct interface is not available. The traditional interface will be used.")
[docs]
def compute_attenuation(vol: NDArray, angle_rad: float, invert: bool = False) -> NDArray:
"""
Compute the attenuation volume for the given local attenuation, and angle.
Parameters
----------
vol : NDArray
The local attenuation volume.
angle_rad : float
The angle along which to compute the attenuation.
invert : bool, optional
Whether to invert propagation direction. The default is False.
Returns
-------
NDArray
The attenuation volume.
"""
def pad_vol(vol: NDArray, edges: Sequence[int]):
paddings = np.zeros((len(vol.shape), 1), dtype=int)
paddings[-2], paddings[-1] = edges[0], edges[1]
return np.pad(vol, paddings, mode="constant")
def compute_cumsum(vol: NDArray, rot_angle_deg: float):
vol = skt.rotate(vol, -rot_angle_deg, order=1, clip=False)
vol += np.roll(vol, 1, axis=-2)
vol = np.cumsum(vol / 2, axis=-2)
return skt.rotate(vol, rot_angle_deg, order=1, clip=False)
size_lims = np.array(vol.shape[-2:])
min_size = np.ceil(np.sqrt(np.sum(size_lims**2)))
edges = np.ceil((min_size - size_lims) / 2).astype(int)
if invert:
angle_rad += np.pi
rot_angle_deg = np.rad2deg(angle_rad)
cum_arr = pad_vol(vol, edges)
if cum_arr.ndim > 2:
prev_shape = np.array(cum_arr.shape, ndmin=1)
num_slices = np.prod(prev_shape[:-2])
cum_arr = cum_arr.reshape([num_slices, *prev_shape[-2:]])
for ii in range(num_slices):
cum_arr[ii] = compute_cumsum(cum_arr[ii], rot_angle_deg)
cum_arr = cum_arr.reshape(prev_shape)
else:
cum_arr = compute_cumsum(cum_arr, rot_angle_deg)
cum_arr = cum_arr[..., edges[0] : -edges[0], edges[1] : -edges[1]]
cum_arr = np.exp(-cum_arr)
return cum_arr
[docs]
class ProjectorBackend(ABC):
"""Base abstract projector backend class. All backends should inherit from this class."""
vol_geom: VolumeGeometry
vol_shape_zxy: NDArray[np.integer]
angles_w_rad: NDArray[np.floating]
prj_shape_vwu: NDArray[np.integer]
prj_shape_vu: NDArray[np.integer]
is_initialized: bool
is_ready: bool
def __init__(self):
"""Initialize base abstract projector backend class."""
self.is_initialized = False
self.is_ready = False
[docs]
def initialize_geometry(
self,
vol_geom: VolumeGeometry,
angles_rot_rad: Union[ArrayLike, NDArray],
rot_axis_shift_pix: Union[ArrayLike, NDArray, None] = None,
prj_geom: Optional[ProjectionGeometry] = None,
create_single_projs: bool = False,
):
"""Initialize the projector geometry.
Parameters
----------
vol_geom : VolumeGeometry
The volume geometry.
angles_rot_rad : ArrayLike | NDArray
The projection angles.
rot_axis_shift_pix : float | NDArray | None, optional
Relative position of the rotation center with respect to the volume center. The default is None.
prj_geom : ProjectionGeometry, optional
The fully specified projection geometry.
When active, the rotation axis shift is ignored. The default is None.
create_single_projs : bool, optional
Whether to create projectors for single projections. Used for corrections and SART. The default is False.
"""
self.vol_geom = vol_geom
self.vol_shape_zxy = self.vol_geom.shape_zxy
self.angles_w_rad = np.array(angles_rot_rad, ndmin=1, dtype=np.float32)
self.vol_eff_shape_zxy = np.floor(self.vol_shape_zxy * self.vol_geom.vox_size).astype(int)
# Basic sizes, unless overridden
self.prj_shape_vwu = np.array(
[*self.vol_eff_shape_zxy[:-2], len(self.angles_w_rad), self.vol_eff_shape_zxy[-1]], dtype=int
)
self.prj_shape_vu = np.array([*self.vol_eff_shape_zxy[:-2], 1, self.vol_eff_shape_zxy[-1]], dtype=int)
self.has_individual_projs = create_single_projs
self.is_initialized = True
[docs]
def get_vol_shape(self) -> NDArray:
"""Return the expected and produced volume shape (in ZXY coordinates).
Returns
-------
NDArray
The volume shape.
"""
return self.vol_shape_zxy
[docs]
def get_prj_shape(self) -> NDArray:
"""Return the expected and produced projection shape (in VWU coordinates).
Returns
-------
NDArray
The projection shape.
"""
return self.prj_shape_vwu
[docs]
def make_ready(self) -> None:
"""Initialize the projector.
It should make sure that all the resources have been allocated.
"""
self.is_ready = True
[docs]
def dispose(self) -> None:
"""De-initialize the projector.
It should make sure that all the resources have been de-allocated.
"""
self.is_ready = False
[docs]
def __del__(self):
"""De-initialize projector on deletion."""
if self.is_ready:
self.dispose()
[docs]
def __repr__(self) -> str:
"""Build a string representation of the projector backend.
Returns
-------
str
The representation of the projector.
"""
class_name = f"{self.__class__.__name__}: "
if self.is_initialized:
return (
class_name
+ "{\n"
+ f" Shape vol ZXY: {self.get_vol_shape()}\n"
+ f" Shape prj VWU: {self.get_prj_shape()}\n"
+ f" Angles (deg): {np.rad2deg(self.angles_w_rad)}\n"
+ "}"
)
else:
return class_name + "{ Not initialized! }"
[docs]
@abstractmethod
def fp(self, vol: NDArray, angle_ind: Optional[int] = None) -> NDArray:
"""Forward-project volume.
Forward-projection interface. Derived backends need to implement this method.
Parameters
----------
vol : NDArray
The volume to forward-project.
angle_ind : int, optional
The angle index to foward project. The default is None.
"""
[docs]
@abstractmethod
def bp(self, prj: NDArray, angle_ind: Optional[int] = None) -> NDArray:
"""Back-project data.
Back-projection interface. Derived backends need to implement this method.
Parameters
----------
prj : NDArray
The sinogram to back-project or a single line.
angle_ind : int, optional
The angle index to foward project. The default is None.
"""
[docs]
class ProjectorBackendSKimage(ProjectorBackend):
"""Projector backend based on scikit-image."""
def __init__(self) -> None:
"""Initialize scikit-image projector backend class."""
super().__init__()
self.is_ready = True
[docs]
def initialize_geometry(
self,
vol_geom: VolumeGeometry,
angles_rot_rad: Union[ArrayLike, NDArray],
rot_axis_shift_pix: Union[ArrayLike, NDArray, None] = None,
prj_geom: Optional[ProjectionGeometry] = None,
create_single_projs: bool = False,
):
"""Initialize projector backend based on scikit-image.
Parameters
----------
vol_geom : VolumeGeometry
The volume shape.
angles_rot_rad : ArrayLike
The projection angles.
rot_axis_shift_pix : float | NDArray | None, optional
Relative position of the rotation center with respect to the volume center. The default is None.
NOT SUPPORTED: if anything else than None is passed, it will throw an error!
prj_geom : ProjectionGeometry, optional
The fully specified projection geometry.
When active, the rotation axis shift is ignored. The default is None.
NOT SUPPORTED: if anything else than None is passed, it will throw an error!
create_single_projs : bool, optional
Whether to create projectors for single projections. Used for corrections and SART. The default is False.
Raises
------
ValueError
In case the volume dimensionality is larger than 2D, and if a rotation axis shift is passed.
"""
if vol_geom.is_3D():
raise ValueError("With the scikit-image backend, only 2D volumes are allowed!")
if rot_axis_shift_pix is not None:
raise ValueError("With the scikit-image backend, rotation axis shifts are not supported!")
if prj_geom is not None:
raise ValueError("With the scikit-image backend, `ProjectionGeometry` is not supported!")
super().initialize_geometry(vol_geom, angles_rot_rad, create_single_projs=create_single_projs)
self.angles_w_deg = np.rad2deg(self.angles_w_rad)
[docs]
@staticmethod
def _set_filter_name(filt):
if skimage.__version__ >= "0.18":
return dict(filter_name=filt)
else:
return dict(filter=filt)
[docs]
@staticmethod
def _set_bpj_size(output_size):
return dict(circle=False, output_size=output_size)
[docs]
def fp(self, vol: NDArray, angle_ind: Optional[int] = None) -> NDArray:
"""Forward-projection of the volume to the sinogram or a single sinogram line.
Parameters
----------
vol : NDArray
The volume to forward-project.
angle_ind : int, optional
The angle index to foward project. The default is None.
Returns
-------
NDArray
The forward-projected sinogram or sinogram line.
"""
if angle_ind is None:
prj = np.empty(self.prj_shape_vwu, dtype=vol.dtype)
for ii_a, a in enumerate(self.angles_w_deg):
prj[ii_a, :] = np.squeeze(skt.radon(vol, [a]))
return prj
else:
return np.squeeze(skt.radon(vol, self.angles_w_deg[angle_ind : angle_ind + 1 :]))
[docs]
def bp(self, prj: NDArray, angle_ind: Optional[int] = None) -> NDArray:
"""Back-projection of a single sinogram line to the volume.
Parameters
----------
prj : NDArray
The sinogram to back-project or a single line.
angle_ind : int, optional
The angle index to foward project. The default is None.
Returns
-------
NDArray
The back-projected volume.
"""
filter_name = self._set_filter_name(None)
bpj_size = self._set_bpj_size(self.vol_shape_zxy[-1])
if angle_ind is None:
vol = np.empty([self.prj_shape_vwu[-2], *self.vol_shape_zxy], dtype=prj.dtype)
for ii_a, a in enumerate(self.angles_w_deg):
vol[ii_a, ...] = skt.iradon(prj[ii_a, :, np.newaxis], [a], **bpj_size, **filter_name)
vol = vol.sum(axis=0)
else:
vol = skt.iradon(prj[:, np.newaxis], self.angles_w_deg[angle_ind : angle_ind + 1 :], **bpj_size, **filter_name)
return vol * 2 / np.pi
[docs]
class ProjectorBackendASTRA(ProjectorBackend):
"""Projector backend based on astra-toolbox."""
proj_id: List
astra_vol_geom: Mapping
proj_geom_ind: Sequence[Mapping]
proj_geom_all: Mapping
def __init__(self, super_sampling: int = 1):
"""Initialize the ASTRA projector backend.
Parameters
----------
super_sampling : int, optional
Super sampling factor for the pixels and voxels, by default 1.
"""
super().__init__()
self.super_sampling = super_sampling
[docs]
def initialize_geometry(
self,
vol_geom: VolumeGeometry,
angles_rot_rad: Union[ArrayLike, NDArray],
rot_axis_shift_pix: Union[ArrayLike, NDArray, None] = None,
prj_geom: Optional[ProjectionGeometry] = None,
create_single_projs: bool = False,
):
"""Initialize geometry of projector backend based on astra-toolbox.
Parameters
----------
vol_geom : VolumeGeometry
The volume shape.
angles_rot_rad : ArrayLike
The projection angles.
rot_axis_shift_pix : float | NDArray | None, optional
Relative position of the rotation center with respect to the volume center. The default is None.
prj_geom : ProjectionGeometry, optional
The fully specified projection geometry.
When active, the rotation axis shift is ignored. The default is None.
create_single_projs : bool, optional
Whether to create projectors for single projections. Used for corrections and SART. The default is False.
Raises
------
ValueError
In case the volume dimensionality is larger than 2D and CUDA is not available.
"""
if vol_geom.is_3D() and not has_cuda:
raise ValueError("CUDA is not available: only 2D volumes are allowed!")
if not (rot_axis_shift_pix is None or isinstance(rot_axis_shift_pix, (int, float, Sequence, np.ndarray))):
raise ValueError(
"Rotation axis shift should either be None or one of the following: int, a float or a sequence of floats"
+ f" ({type(rot_axis_shift_pix)} given instead)."
)
super().initialize_geometry(vol_geom, angles_rot_rad, create_single_projs=create_single_projs)
self.proj_id = []
self.dispose()
num_angles = self.angles_w_rad.size
if self.vol_geom.is_3D():
self.astra_vol_geom = astra.create_vol_geom(*vol_geom.shape_xyz[list([1, 0, 2])], *self.vol_geom.extent)
if prj_geom is None:
prj_geom = ProjectionGeometry.get_default_parallel(geom_type="3d", rot_axis_shift_pix=rot_axis_shift_pix)
if prj_geom.det_shape_vu is None:
prj_geom.det_shape_vu = np.array(self.prj_shape_vwu[list([-3, -1])], dtype=int)
else:
# Here the projections are supposed to be larger or smaller than the sample size
self.prj_shape_vwu = np.array([*prj_geom.det_shape_vu[:-1], num_angles, prj_geom.det_shape_vu[-1]])
self.prj_shape_vu = np.array([*prj_geom.det_shape_vu[:-1], 1, prj_geom.det_shape_vu[-1]])
rot_geom = prj_geom.rotate(self.angles_w_rad)
vectors = np.empty([num_angles, 12])
# source / beam direction
vectors[:, 0:3] = rot_geom.get_field_scaled("src_pos_xyz")
# center of detector
vectors[:, 3:6] = rot_geom.get_field_scaled("det_pos_xyz")
# vector from detector pixel (0, 0) to (0, 1)
vectors[:, 6:9] = rot_geom.get_field_scaled("det_u_xyz")
# vector from detector pixel (0, 0) to (1, 0)
vectors[:, 9:12] = rot_geom.get_field_scaled("det_v_xyz")
geom_type_str = prj_geom.geom_type
else:
self.astra_vol_geom = astra.create_vol_geom(*vol_geom.shape_xyz[list([1, 0])], *self.vol_geom.extent)
if prj_geom is None:
prj_geom = ProjectionGeometry.get_default_parallel(geom_type="2d", rot_axis_shift_pix=rot_axis_shift_pix)
if prj_geom.det_shape_vu is None:
prj_geom.det_shape_vu = np.array(self.prj_shape_vwu[list([-1])], dtype=int)
else:
# Here the projections are supposed to be larger or smaller than the sample size
self.prj_shape_vwu = np.array([num_angles, prj_geom.det_shape_vu[-1]])
self.prj_shape_vu = np.array([1, prj_geom.det_shape_vu[-1]])
rot_geom = prj_geom.rotate(self.angles_w_rad, patch_astra_2d=True)
vectors = np.empty([num_angles, 6])
# source / beam direction
vectors[:, 0:2] = rot_geom.get_field_scaled("src_pos_xyz")
# center of detector
vectors[:, 2:4] = rot_geom.get_field_scaled("det_pos_xyz")
# vector from detector pixel 0 to 1
vectors[:, 4:6] = rot_geom.get_field_scaled("det_u_xyz")
geom_type_str = prj_geom.geom_type[:-2]
vectors /= vol_geom.vox_size
if self.has_individual_projs:
self.proj_geom_ind = [
astra.create_proj_geom(geom_type_str + "_vec", *prj_geom.det_shape_vu, vectors[ii : ii + 1 :, :])
for ii in range(num_angles)
]
self.proj_geom_all = astra.create_proj_geom(geom_type_str + "_vec", *prj_geom.det_shape_vu, vectors)
# def get_vol_shape(self) -> NDArray:
# """Return the expected and produced volume shape (in ZYX coordinates).
# Returns
# -------
# NDArray
# The volume shape.
# """
# return astra.functions.geom_size(self.astra_vol_geom)
# def get_prj_shape(self) -> NDArray:
# """Return the expected and produced projection shape (in VWU coordinates).
# Returns
# -------
# NDArray
# The projection shape.
# """
# return astra.functions.geom_size(self.proj_geom_all)
[docs]
def make_ready(self) -> None:
"""Initialize the ASTRA projectors."""
if not self.is_ready:
if self.vol_geom.is_3D():
projector_type = "cuda3d"
self.algo_type = "3D_CUDA"
self.data_mod = astra.data3d
else:
if has_cuda:
projector_type = "cuda"
self.algo_type = "_CUDA"
else:
projector_type = "linear"
self.algo_type = ""
self.data_mod = astra.data2d
voxel_sampling = int(self.super_sampling * np.fmax(1, self.vol_geom.vox_size))
pixel_sampling = int(self.super_sampling / np.fmin(1, self.vol_geom.vox_size))
opts = {"VoxelSuperSampling": voxel_sampling, "DetectorSuperSampling": pixel_sampling}
if self.has_individual_projs:
self.proj_id = [
astra.create_projector(projector_type, pg, self.astra_vol_geom, opts) for pg in self.proj_geom_ind
]
self.proj_id.append(astra.create_projector(projector_type, self.proj_geom_all, self.astra_vol_geom, opts))
super().make_ready()
[docs]
def _check_data(self, x: NDArray, expected_shape: Union[Sequence[int], NDArray[np.integer]]) -> NDArray:
if x.dtype != np.float32:
x = x.astype(np.float32)
if not x.flags["C_CONTIGUOUS"]:
x = np.ascontiguousarray(x)
try:
return x.reshape(np.array(expected_shape, ndmin=1))
except ValueError:
print(f"Could not reshape input data of shape={x.shape} into expected shape={expected_shape}")
raise
[docs]
def _check_prj_shape(self, prj: NDArray) -> None:
inds_vu = [ind for ind in range(-len(self.prj_shape_vu), 0) if self.prj_shape_vu[ind] > 1]
expected_prj_shape_vu = self.prj_shape_vu[inds_vu]
given_prj_shape_vu = np.array(prj.shape)[inds_vu]
if np.any(given_prj_shape_vu != expected_prj_shape_vu):
raise ValueError(
f"Expected prj shape (VU: {expected_prj_shape_vu} - likely inferred from `vol_geom`),"
f" and passed prj shape (VU: {given_prj_shape_vu}) from the projection data, do not match.\n"
"If you intend to use projection shapes different from the volume shape, please adjust"
" the `prj_geom` object's `det_shape_vu` accordingly."
)
[docs]
def dispose(self) -> None:
"""De-initialize the ASTRA projectors."""
for p in self.proj_id:
astra.projector.delete(p)
self.proj_id = []
super().dispose()
[docs]
def fp(self, vol: NDArray, angle_ind: Optional[int] = None) -> NDArray:
"""Apply forward-projection of the volume to the sinogram or a single sinogram line.
Parameters
----------
vol : NDArray
The volume to forward-project.
angle_ind : int | None, optional
The angle index to forward project. The default is None.
Returns
-------
NDArray
The forward-projected sinogram or sinogram line.
"""
self.make_ready()
vol = self._check_data(vol, self.vol_shape_zxy)
if angle_ind is None:
prj = np.empty(self.prj_shape_vwu, dtype=np.float32)
prj_geom = self.proj_geom_all
proj_id = self.proj_id[-1]
else:
if not self.has_individual_projs:
raise ValueError("Individual projectors not available!")
prj = np.empty(self.prj_shape_vu, dtype=np.float32)
prj_geom = self.proj_geom_ind[angle_ind]
proj_id = self.proj_id[angle_ind]
vid = self.data_mod.link("-vol", self.astra_vol_geom, vol)
try:
sid = self.data_mod.link("-sino", prj_geom, prj)
try:
cfg = astra.creators.astra_dict("FP" + self.algo_type)
cfg["ProjectionDataId"] = sid
cfg["VolumeDataId"] = vid
cfg["ProjectorId"] = proj_id
fp_id = astra.algorithm.create(cfg)
try:
astra.algorithm.run(fp_id)
finally:
astra.algorithm.delete(fp_id)
finally:
self.data_mod.delete([sid])
finally:
self.data_mod.delete([vid])
return np.squeeze(prj)
[docs]
def bp(self, prj: NDArray, angle_ind: Optional[int] = None) -> NDArray:
"""Apply back-projection of a single sinogram line to the volume.
Parameters
----------
prj : NDArray
The sinogram to back-project or a single line.
angle_ind : int | None, optional
The angle index to forward project. The default is None.
Returns
-------
NDArray
The back-projected volume.
"""
self.make_ready()
self._check_prj_shape(prj)
vol = np.empty(self.vol_shape_zxy, dtype=np.float32)
if angle_ind is None:
prj = self._check_data(prj, self.prj_shape_vwu)
prj_geom = self.proj_geom_all
proj_id = self.proj_id[-1]
else:
if not self.has_individual_projs:
raise ValueError("Individual projectors not available!")
prj = self._check_data(prj, self.prj_shape_vu)
prj_geom = self.proj_geom_ind[angle_ind]
proj_id = self.proj_id[angle_ind]
vid = self.data_mod.link("-vol", self.astra_vol_geom, vol)
try:
sid = self.data_mod.link("-sino", prj_geom, prj)
try:
cfg = astra.creators.astra_dict("BP" + self.algo_type)
cfg["ProjectionDataId"] = sid
cfg["ReconstructionDataId"] = vid
cfg["ProjectorId"] = proj_id
bp_id = astra.algorithm.create(cfg)
try:
astra.algorithm.run(bp_id)
finally:
astra.algorithm.delete(bp_id)
finally:
self.data_mod.delete([sid])
finally:
self.data_mod.delete([vid])
return vol
[docs]
class ProjectorBackendDirectASTRA(ProjectorBackendASTRA):
"""Experimental astra-toolbox functions projector."""
astra_vol_shape: Sequence
astra_prj_shape: Sequence
astra_angle_prj_shape: Sequence
angle_prj_shape: Sequence
[docs]
def initialize_geometry(
self,
vol_geom: VolumeGeometry,
angles_rot_rad: Union[ArrayLike, NDArray],
rot_axis_shift_pix: Union[ArrayLike, NDArray, None] = None,
prj_geom: Optional[ProjectionGeometry] = None,
create_single_projs: bool = False,
):
"""Initialize projector backend based on experimental astra-toolbox functions.
Parameters
----------
vol_geom : VolumeGeometry
The volume shape.
angles_rot_rad : ArrayLike
The projection angles.
rot_axis_shift_pix : float, optional
Relative position of the rotation center with respect to the volume center. The default is 0.0.
geom : ProjectionGeometry, optional
The fully specified projection geometry.
When active, the rotation axis shift is ignored. The default is None.
create_single_projs : bool, optional
Whether to create projectors for single projections. Used for corrections and SART. The default is False.
Raises
------
ValueError
In case CUDA is not available.
"""
if not has_cuda:
raise ValueError("CUDA is not available, but it is required for the direct functions!")
if not has_astra_direct:
raise ValueError("ASTRA direct is not available, but it is required!")
if not (rot_axis_shift_pix is None or isinstance(rot_axis_shift_pix, (int, float, Sequence, np.ndarray))):
raise ValueError(
"Rotation axis shift should either be None or one of the following: int, a float or a sequence of floats"
+ f" ({type(rot_axis_shift_pix)} given instead)."
)
ProjectorBackend.initialize_geometry(self, vol_geom, angles_rot_rad, create_single_projs=create_single_projs)
self.proj_id = []
self.dispose()
num_angles = self.angles_w_rad.size
vol_geom_tmp3d = self.vol_geom.get_3d()
self.astra_vol_geom = astra.create_vol_geom(*vol_geom_tmp3d.shape_xyz[list([1, 0, 2])], *vol_geom_tmp3d.extent)
if prj_geom is None:
prj_geom = ProjectionGeometry.get_default_parallel(geom_type="3d", rot_axis_shift_pix=rot_axis_shift_pix)
else:
if prj_geom.det_shape_vu is not None:
# Here the projections are supposed to be larger or smaller than the sample size
# We use the original version because it makes sure that 2D is respected.
self.prj_shape_vwu = np.array([*prj_geom.det_shape_vu[:-1], num_angles, prj_geom.det_shape_vu[-1]])
self.prj_shape_vu = np.array([*prj_geom.det_shape_vu[:-1], 1, prj_geom.det_shape_vu[-1]])
prj_geom = prj_geom.get_3d()
if prj_geom.det_shape_vu is None:
prj_shape_vu = np.delete(self.prj_shape_vwu, obj=-2)
prj_geom.det_shape_vu = np.ones(2, dtype=int)
prj_geom.det_shape_vu[-len(prj_shape_vu) :] = prj_shape_vu
rot_geom = prj_geom.rotate(self.angles_w_rad)
vectors = np.empty([num_angles, 12])
# source / beam direction
vectors[:, 0:3] = rot_geom.get_field_scaled("src_pos_xyz")
# center of detector
vectors[:, 3:6] = rot_geom.get_field_scaled("det_pos_xyz")
# vector from detector pixel (0, 0) to (0, 1)
vectors[:, 6:9] = rot_geom.get_field_scaled("det_u_xyz")
# vector from detector pixel (0, 0) to (1, 0)
vectors[:, 9:12] = rot_geom.get_field_scaled("det_v_xyz")
geom_type_str = prj_geom.geom_type
if self.has_individual_projs:
self.proj_geom_ind = [
astra.create_proj_geom(geom_type_str + "_vec", *prj_geom.det_shape_vu, vectors[ii : ii + 1 :, :])
for ii in range(num_angles)
]
self.proj_geom_all = astra.create_proj_geom(geom_type_str + "_vec", *prj_geom.det_shape_vu, vectors)
self.astra_vol_shape = tuple(vol_geom_tmp3d.shape_zxy)
self.astra_prj_shape = (prj_geom.det_shape_vu[-2], num_angles, prj_geom.det_shape_vu[-1])
self.astra_angle_prj_shape = (prj_geom.det_shape_vu[-2], 1, prj_geom.det_shape_vu[-1])
self.angle_prj_shape = (prj_geom.det_shape_vu[-2], prj_geom.det_shape_vu[-1])
[docs]
def make_ready(self):
"""Initialize the ASTRA projectors."""
if not self.is_ready:
voxel_sampling = int(self.super_sampling * np.fmax(1, self.vol_geom.vox_size))
pixel_sampling = int(self.super_sampling / np.fmin(1, self.vol_geom.vox_size))
opts = {"VoxelSuperSampling": voxel_sampling, "DetectorSuperSampling": pixel_sampling}
if self.has_individual_projs:
self.proj_id = [astra.create_projector("cuda3d", pg, self.astra_vol_geom, opts) for pg in self.proj_geom_ind]
self.proj_id.append(astra.create_projector("cuda3d", self.proj_geom_all, self.astra_vol_geom, opts))
ProjectorBackend.make_ready(self)
[docs]
def fp(self, vol: NDArray, angle_ind: Optional[int] = None):
"""Apply forward-projection of the volume to the sinogram or a single sinogram line.
Parameters
----------
vol : NDArray
The volume to forward-project.
angle_ind : int | None, optional
The angle index to forward project. The default is None.
Returns
-------
NDArray
The forward-projected sinogram or sinogram line.
"""
self.make_ready()
if angle_ind is None:
prj = np.zeros(self.astra_prj_shape, dtype=np.float32)
proj_id = self.proj_id[-1]
out_shape = self.prj_shape_vwu
else:
if not self.has_individual_projs:
raise ValueError("Individual projectors not available!")
prj = np.zeros(self.astra_angle_prj_shape, dtype=np.float32)
proj_id = self.proj_id[angle_ind]
out_shape = self.angle_prj_shape
vol = self._check_data(vol, self.astra_vol_shape)
astra.experimental.direct_FP3D(proj_id, vol, prj)
return prj.reshape(out_shape)
[docs]
def bp(self, prj: NDArray, angle_ind: Optional[int] = None):
"""Apply back-projection of a single sinogram line to the volume.
Parameters
----------
prj : NDArray
The sinogram to back-project or a single line.
angle_ind : int | None, optional
The angle index to forward project. The default is None.
Returns
-------
NDArray
The back-projected volume.
"""
self.make_ready()
self._check_prj_shape(prj)
if angle_ind is None:
prj = self._check_data(prj, self.astra_prj_shape)
proj_id = self.proj_id[-1]
else:
if not self.has_individual_projs:
raise ValueError("Individual projectors not available!")
prj = self._check_data(prj, self.astra_angle_prj_shape)
proj_id = self.proj_id[angle_ind]
vol = np.zeros(self.astra_vol_shape, dtype=np.float32)
astra.experimental.direct_BP3D(proj_id, vol, prj)
return vol.reshape(self.vol_shape_zxy)