Source code for nabu.reconstruction.reconstructor

import numpy as np
from ..utils import convert_index


[docs] class Reconstructor: """ Abstract base class for reconstructors. A `Reconstructor` is a helper to reconstruct slices in arbitrary directions (not only usual "horizontal slices") in parallel-beam tomography. Current limitations: - Limitation to the three main axes - One instance of Reconstructor can only reconstruct successive slices Typical scenarios examples: - "I want to reconstruct several slices along 'z'", where `z` is the vertical axis. In this case, we reconstruct "horizontal slices" in planes perpendicular to the rotation axis. - "I want to reconstruct slices along 'y'". Here `y` is an axis perpendicular to `z`, i.e we reconstruct "vertical slices". A `Reconstructor` is tied to the set of slices to reconstruct (axis and orientation). Once defined, it cannot be changed ; i.e another class has to be instantiated to reconstruct slices in other axes/indices. The volume geometry conventions are defined below:: __________ / /| / / | z / / | ^ /_________/ | | | | | | y | | / | / | | / | / | | / | / |__________|/ |/ ---------- > x The axis `z` parallel to the rotation axis. The usual parallel-beam tomography setting reconstructs slices along `z`, i.e in planes parallel to (x, y). """ def __init__(self, shape, indices_range, axis="z", vol_type="sinograms", slices_roi=None): """ Initialize a reconstructor. Parameters ----------- shape: tuple Shape of the stack of sinograms or projections. indices_range: tuple Range of indices to reconstruct, in the form (start, end). As the standard Python behavior, the upper bound is not included. For example, to reconstruct 100 slices (numbered from 0 to 99), then you can provide (0, 100) or (0, None). Providing (0, 99) or (0, -1) will omit the last slice. axis: str Axis along which the slices are reconstructed. This axis is orthogonal to the slices planes. This parameter can be either "x", "y", or "z". Default is "z" (reconstruct slices perpendicular to the rotation axis). vol_type: str, optional Whether the parameter `shape` describes a volume of sinograms or projections. The two are the same except that axes 0 and 1 are swapped. Can be "sinograms" (default) or "projections". slices_roi: tuple, optional Define a Region Of Interest to reconstruct a part of each slice. By default, the whole slice is reconstructed for each slice index. This parameter is in the form `(start_u, end_u, start_v, end_v)`, where `u` and `v` are horizontal and vertical axes on the reconstructed slice respectively, regardless of its orientation. If one of the values is set to None, it will be replaced by the corresponding default value. Examples --------- To reconstruct the first two horizontal slices, i.e along `z`: `R = Reconstructor(vol_shape, [0, 1])` To reconstruct vertical slices 0-100 along the `y` axis: `R = Reconstructor(vol_shape, (0, 100), axis="y")` """ self._set_shape(shape, vol_type) self._set_axis(axis) self._set_indices(indices_range) self._configure_geometry(slices_roi) def _set_shape(self, shape, vol_type): if "sinogram" in vol_type.lower(): self.vol_type = "sinograms" elif "projection" in vol_type.lower(): self.vol_type = "projections" else: raise ValueError("vol_type can be either 'sinograms' or 'projections'") if len(shape) != 3: raise ValueError("Expected a 3D array description, but shape does not have 3 dims") self.shape = shape if self.vol_type == "sinograms": n_z, n_a, n_x = shape else: n_a, n_z, n_x = shape self.sinos_shape = (n_z, n_a, n_x) self.projs_shape = (n_a, n_z, n_x) self.data_shape = self.sinos_shape if self.vol_type == "sinograms" else self.projs_shape self.n_a = n_a self.n_x = n_x self.n_y = n_x # square slice by default self.n_z = n_z self._n = {"x": self.n_x, "y": self.n_y, "z": self.n_z} def _set_axis(self, axis): if axis.lower() not in ["x", "y", "z"]: raise ValueError("axis can be either 'x', 'y' or 'z' (got %s)" % axis) self.axis = axis def _set_indices(self, indices_range): start, end = indices_range npix = self._n[self.axis] start = convert_index(start, npix, 0) end = convert_index(end, npix, npix) self.indices_range = (start, end) self._idx_start = start self._idx_end = end self.indices = np.arange(start, end) def _configure_geometry(self, slices_roi): self.slices_roi = slices_roi or (None, None, None, None) start_u, end_u, start_v, end_v = self.slices_roi uv_to_xyz = { "z": ("x", "y"), # reconstruct along z: u = x, v = y "y": ("y", "z"), # reconstruct along y: u = y, v = z "x": ("y", "z"), # reconstruct along x: u = y, v = z } rotated_axes = uv_to_xyz[self.axis] u_max = self._n[rotated_axes[0]] v_max = self._n[rotated_axes[1]] start_u = convert_index(start_u, u_max, 0) end_u = convert_index(end_u, u_max, u_max) start_v = convert_index(start_v, v_max, 0) end_v = convert_index(end_v, v_max, v_max) self.slices_roi = (start_u, end_u, start_v, end_v) if self.axis == "z": self.backprojector_roi = self.slices_roi start_z, end_z = self._idx_start, self._idx_end if self.axis == "y": self.backprojector_roi = (start_u, end_u, self._idx_start, self._idx_end) start_z, end_z = start_v, end_v if self.axis == "x": self.backprojector_roi = (self._idx_start, self._idx_end, start_u, end_u) start_z, end_z = start_v, end_v self._z_indices = np.arange(start_z, end_z) self.output_shape = ( self._z_indices.size, self.backprojector_roi[3] - self.backprojector_roi[2], self.backprojector_roi[1] - self.backprojector_roi[0], ) def _check_data(self, data): if data.shape != self.data_shape: raise ValueError( "Invalid data shape: expected %s shape %s, but got %s" % (self.vol_type, self.data_shape, data.shape) ) if data.dtype != np.float32: raise ValueError("Expected float32 data type")
[docs] def reconstruct(self): raise ValueError("Base class")
__call__ = reconstruct