import logging
import numpy as np
from mumott import Geometry
from mumott.data_handling.utilities import get_absorbances
from mumott.core.projection_stack import ProjectionStack
logger = logging.getLogger(__name__)
def _get_unique_indices(angles: np.ndarray[float],
indices: np.ndarray[int],
tolerance: float = 1e-4) -> np.ndarray[float]:
"""Filters indices based on angles."""
angles = np.mod(angles, np.pi)
out_indices = [indices[0]]
out_angles = [angles[0]]
for angle, index in zip(angles, indices):
angle_delta = np.abs(angle - np.array(out_angles))
if np.all((angle_delta > tolerance) & (angle_delta < np.pi - tolerance)):
out_indices.append(index)
out_angles.append(angle)
return np.array(out_indices)
def _get_orthogonal_axis(geometry: Geometry,
projection_index: int = 0,
axis_string='inner'):
"""Retrieves index of axis orthogonal to inner or outer axis in geometry."""
if axis_string == 'inner':
axis = geometry.inner_axes[projection_index]
elif axis_string == 'outer':
axis = geometry.outer_axes[projection_index]
else:
raise ValueError('axis_string must be either "inner" or "outer", '
f'but a value of "{axis_string}" was specified.')
j_projection = np.dot(axis, geometry.j_direction_0)
k_projection = np.dot(axis, geometry.k_direction_0)
if not np.isclose(j_projection + k_projection, 1):
raise ValueError('Rotation axis must be orthogonal to the j or k-axis.')
if np.isclose(j_projection, 0):
return 1
else:
return 2
def _get_filter(length: int,
filter_type: str) -> np.ndarray[float]:
"""Retrieves a high-pass filter for FBP based on the string."""
u = np.fft.fftfreq(length) / (4 * np.pi)
filter = abs(u)
filter[abs(u) > 1 / (16 * np.pi)] = 0.0
if filter_type.lower() == 'ram-lak':
return filter
elif filter_type.lower() == 'hann':
filter *= 0.5 + 0.5 * np.cos(2 * np.pi * u)
elif filter_type.lower() == 'hamming':
filter *= 0.54 + 0.46 * np.cos(2 * np.pi * u)
elif filter_type.lower() == 'shepp-logan':
filter *= np.sinc(u)
elif filter_type.lower() == 'cosine':
filter *= np.cos(u * np.pi)
else:
raise ValueError(f'Unknown filter type: "{filter_type}"!'
' Permitted values are: "Ram-Lak", "Hamming", "Hann",'
' "cosine", and "Shepp-Logan".')
return filter
[docs]def get_filtered_projections(projections: ProjectionStack,
axis_string: str = 'inner',
filter_type: str = 'Ram-Lak',
normalization_percentile: float = 99.9,
transmittivity_cutoff: tuple[float, float] = (None, None)) \
-> tuple[np.ndarray, np.ndarray]:
"""
Applies a high-pass filter to a selected subset of the
absorbances for filtered back projection.
Parameters
----------
projections
The :class:`ProjectionStack <mumott.data_handling.ProjectionStack>` to calculate the
filtered projections from.
axis_string
Default is ``'inner'``, the value depends on how the sample is mounted to the holder. Typically,
the inner axis is the rotation axis while the ``'outer'`` axis refers to the tilt axis.
filter_string
Default is ``'ram-lak'``, a high-pass filter. Other options are ``'Hamming'`` and ``'Hanning'``.
normalization_percentile
The normalization percentile to use for the transmittivity calculation. See
:func:`get_transmittivities <mumott.data_handling.utilities.get_transmittivities>` for details.
transmittivity_cutoff
The cutoffs to use for the transmittivity calculation. See
:func:`get_transmittivities <mumott.data_handling.utilities.get_transmittivities>` for details.
Returns
-------
A tuple containing the filtered subset of the absorbances
and the index of the axis orthogonal to the inner or outer axis.
"""
geometry = projections.geometry
if axis_string == 'inner':
tilt_angles = geometry.outer_angles_as_array
rotation_angles = geometry.inner_angles_as_array
elif axis_string == 'outer':
tilt_angles = geometry.inner_angles_as_array
rotation_angles = geometry.outer_angles_as_array
else:
raise ValueError(f'Unknown axis: {axis_string}, '
'please specify "inner" or "outer".')
# Check if we have any transmittivity cutoff values to consider
cutoff_values = (1e-4, 1) # default
for k in range(2):
if transmittivity_cutoff[k] is not None:
cutoff_values[k] = transmittivity_cutoff[k]
abs_dict = get_absorbances(
projections.diode,
normalize_per_projection=True,
normalization_percentile=normalization_percentile,
cutoff_values=cutoff_values)
absorbances = abs_dict['absorbances']
# Find projections without any tilt and the rotation of each such projection
no_tilt_indices = np.where(np.isclose(tilt_angles, 0))[0]
no_tilt_angles = rotation_angles[no_tilt_indices]
# Remove 'duplicate' projections with equivalent or very similar rotation angles
no_tilt_indices = _get_unique_indices(no_tilt_angles, no_tilt_indices)
absorbances = absorbances[no_tilt_indices]
filter_axis = _get_orthogonal_axis(geometry, 0, axis_string)
filter = _get_filter(length=absorbances.shape[filter_axis],
filter_type=filter_type)
if filter_axis == 1:
filter = filter.reshape(1, -1, 1, 1)
else:
filter = filter.reshape(1, 1, -1, 1)
# reduce weights over last index
abs_mask = np.all(abs_dict['cutoff_mask'][no_tilt_indices] *
projections.weights[no_tilt_indices] > 0., axis=-1).astype(float)
absorbances *= abs_mask[..., None] # redundant axis needed for consistency reasons
abs_fft = np.fft.fft(absorbances, axis=filter_axis)
abs_fft *= filter
filtered_absorbances = np.fft.ifft(abs_fft, axis=filter_axis).real
return np.ascontiguousarray(filtered_absorbances), no_tilt_indices