Source code for mumott.pipelines.optical_flow_alignment

"""
Functions for subpixel alignment algorithm.
"""
import sys

import numpy as np
import scipy as sp
from scipy import ndimage
from tqdm import tqdm

from typing import Any

from mumott.data_handling import DataContainer
from mumott.data_handling.utilities import get_absorbances
from mumott.pipelines import run_mitra
from mumott.methods.projectors import SAXSProjectorCUDA, SAXSProjector
from mumott.pipelines.utilities import image_processing as imp
from mumott.pipelines.utilities.alignment_geometry import get_alignment_geometry


def _define_axis_index(rotation_axis_index: int) -> tuple[int, int]:
    """ Defines the indices of the geometrical x and y axes relative to the axes of the array,
    using the index of the main rotation axis, which by definition is the geometrical y axis.

    Parameters
    ----------
    rotation_axis_index
        The index of the main rotation axis in the projections.

    Returns
    -------
        A tuple comprising the indices of the geometrical x and y axes for the array.

    """

    # define the x and y axis, y being the rotation axis
    if rotation_axis_index == 0:
        x_axis_index = 1
        y_axis_index = 0
    else:
        x_axis_index = 0
        y_axis_index = 1
    return x_axis_index, y_axis_index


[docs]def run_optical_flow_alignment( data_container: DataContainer, **kwargs, ) -> tuple[np.ndarray[float], np.ndarray[float], np.ndarray[float]]: """This pipeline implements the alignment algorithm described in [Odstrcil2019]_ The latter allows one to compute the shifts that are need to align the data according to the tomographical problem defined by the given geometry and projector. The alignment also relies on the related definition of the reconstruction volume and main axis of rotation ('y' axis). The procedure can be customized via the keyword arguments described under below (see Notes). Parameters ---------- data_container Input data. volume : tuple[int, int, int] **Geometry parameter:** The size of the volume of the wanted tomogram. If not specified, deduced from information in :attr:`data_container`. main_rot_axis : int **Geometry parameter:** The index of the main rotation axis (``y`` axis on the real geometry) on the array. If not specified, deduced from information in :attr:`data_container`. smooth_data : bool **Data filtering:** If ``True`` apply Gaussian filter to smoothen the data; default: ``False``. sigma_smooth : float **Data filtering:** The smoothing kernel related to the overall data smoothing; default: ``0``. high_pass_filter : float **Data filtering:** The kernel for high pass filter in the gradient computation to avoid phase artifact; default: ``0.01``. optimal_shift : np.ndarray[float] **Shifts:** The original shift; default: ``np.zeros((nb_projections, 2))``. rec_iteration : int **Projector parameters:** The number of iteration used to solve the tomogram from the projections; default: ``20``. use_gpu : bool **Projector parameters:** Use GPU (``True``) or CPU (``False``) for the tomographic computation; default: ``False``. optimizer_kwargs : dict[str, Any] **Projector parameters:** Keyword arguments to pass on to the optimizer; default: ``dict(nestorov_weight = 0.6)``. stop_max_iteration : int **Alignment parameters:** The maximum iteration allowed to find the shifts for the alignment; default: ``20``. stop_min_correction : float **Alignment parameters:** The optimization is terminated if the correction (in pixel) drops below this value; default ``0.01``. align_horizontal : bool **Alignment parameters:** Apply the horizontal alignment procedure; default: ``True``. align_vertical : bool **Alignment parameters:** Apply the vertical alignment procedure; default: ``True``. center_reconstruction : bool **Alignment parameters:** Shift the reconstructed tomogram to the center of the volume to avoid drift in the alignment procedure; default: ``True``. Returns ------- A tuple comprising the shifts used for aligning the, the resulting (and aligned) projections obtained by projecting the reconstructed tomogram based on the aligned data, and the resulting tomogram reconstructed using the aligned data. Example ------- The alignment procedure is simple to use. >>> import numpy as np >>> from mumott.data_handling import DataContainer >>> from mumott.pipelines import optical_flow_alignment as ofa >>> data_container = DataContainer('tests/test_fbp_data.h5') We introduce some spurious offsets to this already-aligned data. >>> length = len(data_container.geometry) >>> data_container.geometry.j_offsets = np.arange(0., length) - length * 0.5 >>> data_container.geometry.k_offsets = \ np.cos(np.arange(0., length) * np.pi / length) We then perform the alignment with default parameters. >>> shifts, sinogram_corr, tomogram_corr = ofa.run_optical_flow_alignment(data_container) ... To use the alignment shifts, we have to translate them from the reconstruction ``(x, y, z)``-coordinates into the projection ``(p, j, k)`` coordinates. The function :func:`shifts_to_vector` transforms the shifts in projection space into shifts in 3D space based on the detector geometry. The function :func:`shifts_to_geometry` transforms the shifts vector from the detector reference frame to the sample reference frame. >>> j_offsets, k_offsets = ofa.shifts_to_geometry(data_container, shifts) >>> data_container.geometry.j_offsets = j_offsets >>> data_container.geometry.k_offsets = k_offsets We can configure a variety of parameters and pass those to the alignment pipeline. For example, we can choose whether to align in the horizontal or vertical directions, whether to center the reconstruction, initial guesses for the shifts, the number of iterations for the reconstruction, the number of iterations for the alignment procedure, and so on. A quick way to align very misaligned data is to use the :func:`line_vertical_alignment` function to obtain an initial guess. It can as well be used between alignment steps. Note that their is two version for the vertical It can also be used between alignment steps. There are two version for the vertical alignment. :func:`line_vertical_alignment` uses the :class:`DataContainer` geometry and data, while :func:`sinogram_line_vertical_alignment` is based on a sinogram and the geometry. Note that the more strongly the object deviates from axial symmetry, the worse the vertical line alignment will be. >>> initial_shift = ofa.line_vertical_alignment(data_container) >>> alignment_param = dict( ... optimal_shift=initial_shift, ... rec_iteration=10, ... stop_max_iteration=3, ... align_horizontal=True, ... align_vertical=True, ... center_reconstruction=True, ... optimizer_kwargs=dict(nestorov_weight=0.6)) >>> shifts, sinogram_corr, tomogram_corr = ofa.run_optical_flow_alignment(data_container, ... **alignment_param) >>> vertical_shift, index = ofa.sinogram_line_vertical_alignment( ... sinogram_corr, ofa.get_alignment_geometry(data_container)[0]) >>> shifts[:,index] += vertical_shift # to update the shifts with vertical line alignment. """ # deduce main rotation axis and associated volume from information in data container main_rot_axis_deduced, volume_deduced = get_alignment_geometry(data_container) # -- options nb_projections = np.size(data_container.diode, 0) # ========= geometry ================ # volume to reconstruct volume = kwargs.get('volume', volume_deduced) # main rotation axis main_rot_axis = kwargs.get('main_rot_axis', main_rot_axis_deduced) # ========= data filtering ================ # overall filtering smooth_data = kwargs.get('smooth_data', False) sigma_smooth = kwargs.get('sigma_smooth', 0) # phase artifact removal filtering high_pass_filter = kwargs.get('high_pass_filter', 0.01) # ========= original shift ================ optimal_shift = kwargs.get('optimal_shift', np.zeros((nb_projections, 2))) # ========= projector parameters ================ # number of iteration for the tomogram reconstruction rec_iteration = kwargs.get('rec_iteration', 20) # ========= alignment parameters ================ # stoppage criteria # maximum iteration possible max_iteration = kwargs.get('stop_max_iteration', 20) # stop iterate if the position is not corrected enough anymore min_correction = kwargs.get('stop_min_correction', 0.01) # alignment condition # horizontal and vertical algwinment align_horizontal = kwargs.get('align_horizontal', True) align_vertical = kwargs.get('align_vertical', True) # to avoid a full volume shift center_reconstruction = kwargs.get('center_reconstruction', True) # use the GPU for tomographic computation use_gpu = kwargs.get('use_gpu', False) # optimizer kwargs optimizer_kwargs = kwargs.get('optimizer_kwargs', dict(nestorov_weight=0.6)) # add the max iter optimizer_kwargs.update({'maxiter': rec_iteration}) # call the real function return _optical_flow_alignment_full_param( data_container, volume, main_rot_axis, smooth_data, sigma_smooth, high_pass_filter, optimal_shift, rec_iteration, max_iteration, min_correction, align_horizontal, align_vertical, center_reconstruction, use_gpu, optimizer_kwargs, )
def _optical_flow_alignment_full_param( data_container: DataContainer, volume: tuple[int, int, int], main_rot_axis: int, smooth_data: bool = False, sigma_smooth: float = 0.0, high_pass_filter: float = 0.01, optimal_shift: np.ndarray[float] = 0.0, rec_iteration: int = 20, max_iteration: int = 100, min_correction: float = 0.01, align_horizontal: bool = True, align_vertical: bool = True, center_reconstruction: bool = True, use_gpu: bool = False, optimizer_kwargs: dict[str, Any] = None, ): """ To compute the shifts nescessary to align the datas according to the tomographical problem defined by the geom and the projector given in arg. This will also rely on the related definition of the reconstruction volume and main axis of rotation ('y' axis), and a lot of alignment parameters. This function should not be called on itself, but through the wrapper optical_flow_alignment. Complete re-use and reformating of the code of Michal Odstrcil, 2016. Based on [Odstrcil2019]_ Parameters ---------- data_container Input data. volume The size of the volume of the wanted tomogram. main_rot_axis The index of the main rotation axis ('y' axis on the real geometry) on the array. smooth_data To smooth the data by gaussian filter. sigma_smooth The smoothing kernel related to the overall data smoothing. high_pass_filter The kernel for high pass filter in the gradient computation, to avoid phase artifact. optimal_shift The original shift. rec_iteration The number of iteration used to solve the tomogram from the projections. max_iteration The maximum iteration allowed to find the shifts for the alignment. min_correction The minimum of correction (in pixel) needed for each iteration to not stop the iterative procedure. align_horizontal Apply the horizontal alignment procedure. align_vertical Apply the horizontal vertical procedure. center_reconstruction Recenter the reconstructed tomogram at the center of the volume to avoid drift in the alignment procedure. use_gpu Use GPU or CPU for the tomographic computation. optimizer_kwargs kwargs for the optimizer, for the run pipeline. Returns ------- shift np.ndarray[float]. The shifts nescessary to align the data. sinogram_corr A tuple comprising the shifts used for aligning the, the resulting (and aligned) projections obtained by projecting the reconstructed tomogram based on the aligned data, and the resulting tomogram reconstructed using the aligned data. """ # define the x and y axis, y being the rotation axis x_axis_index, y_axis_index = _define_axis_index(main_rot_axis) # get absorbance for the sino abs_dict = get_absorbances(data_container.diode, normalize_per_projection=True) absorbances = abs_dict['absorbances'] sinogram_0 = np.moveaxis(np.squeeze(absorbances), 0, -1) # reference diodes diodes_ref = np.moveaxis(np.squeeze(data_container.diode), 0, -1) # define sinogram sizes nb_projections = np.size(sinogram_0, -1) # the volume is a parallepipede with square base nb_layers = data_container.diode.shape[main_rot_axis + 1] width = data_container.diode.shape[np.mod(main_rot_axis + 1, 2) + 1] # -- pre processing # if data is smoothened, smooth sinogram_0 via Gaussian filter if smooth_data: sinogram_0 = sp.ndimage.gaussian_filter(sinogram_0, sigma_smooth) sinogram_0 = imp.smooth_edges(sinogram_0) # Tukey window, necessary for the grad computation, to avoid edges issues W = imp.compute_tukey_window(width, nb_layers) if x_axis_index == 0: W = W.transpose(1, 0, 2) # -- configuration for the loop iteration = 0 max_step = 1 + min_correction shifts = np.zeros((max_iteration + 1, nb_projections, 2)) # shift initial tomogram with a guessed shift shifts[0, :, :] = optimal_shift # shift in geometry to 0, since it is done by sinogram shift # data_container.geometry.j_offsets = optimal_shift[:, 0] * 0 # data_container.geometry.k_offsets = optimal_shift[:, 1] * 0 # visual feedback of the progression pbar = tqdm(total=max_iteration, desc='Alignment iterations', file=sys.stdout) while True: # compute the shifts and the related tomogram and corrected sinograms max_step, sinogram_corr, tomogram, shifts = compute_shifts( sinogram_0, diodes_ref, shifts, data_container, iteration, x_axis_index, y_axis_index, rec_iteration, high_pass_filter, W, align_horizontal, align_vertical, center_reconstruction, use_gpu, optimizer_kwargs, ) pbar.update(1) # if the step size is too small stop optimization if max_step <= min_correction: print( 'The largest change ({max_step})' ' has dropped below the stopping criterion ({min_correction})' ' The alignment is complete.', ) break if iteration + 1 >= max_iteration: break else: iteration = iteration + 1 pbar.close() shift = shifts[iteration + 1, :, :] return shift, np.moveaxis(sinogram_corr, 0, -1), tomogram def recenter_tomogram( tomogram: np.ndarray[float], step: float = 0.2, **kwargs: dict[str, Any], ): """ Recenter a tomogram in frame. Parameters ---------- tomogram The tomogram to shift. step The step for the recentering, should be smaller than one. Default is 0.2 Returns ------- The shifted tomogram, recentered in frame. """ # remove the extra dimension for this calculation tomo_2 = np.squeeze(np.copy(tomogram)) # the volume is a parallelepiped with square base axis = 0 volume = tomo_2.shape if volume[0] == volume[1]: axis = 2 elif volume[0] == volume[2]: axis = 1 axis = kwargs.get('axis ', axis) # the 2 first dimensions must be the transverse slices of the tomogram, i.e., have the same dimension tomo_2 = np.moveaxis(tomo_2, axis, -1) # try to keep reconstruction in center # enforce positivity pos_tomo_2 = np.copy(tomo_2) pos_tomo_2[pos_tomo_2 < 0] = 0 x, y, mass = imp.center(np.sqrt(pos_tomo_2)) # x is here first axis, y second axis # more robust estimation of the center # remove nan ind_x = np.argwhere(~np.isnan(x)) ind_y = np.argwhere(~np.isnan(y)) x = ( np.mean(x[ind_x] * mass[ind_x] ** 2) / np.mean(mass[ind_x] ** 2 + np.finfo(float).eps) * np.ones(x.shape) ) y = ( np.mean(y[ind_y] * mass[ind_y] ** 2) / np.mean(mass[ind_y] ** 2 + np.finfo(float).eps) * np.ones(y.shape) ) # shift (slowly) the tomogram to the new center # here, x is the first axis, y the second axis, same reference for imshift_fft function tomo_2 = imp.imshift_fft( tomo_2, -x * step, -y * step ) # go slowly, using only one fifth of the shift # put back the right order tomo_2 = np.moveaxis(tomo_2, -1, axis) tomogram[:, :, :, 0] = tomo_2 return tomogram def compute_shifts( sinogram_0: np.ndarray[float], diodes_ref: np.ndarray[float], shifts, data_container: DataContainer, iteration: int, x_axis_index: int, y_axis_index: int, rec_iteration: int, high_pass_filter: float, W: np.ndarray[float], align_horizontal: bool, align_vertical: bool, center_reconstruction: bool, use_gpu: bool, optimizer_kwargs: dict[str, Any], ) -> tuple[float, np.ndarray[float], np.ndarray[float], np.ndarray[float]]: """ Compute the shifts to align the reference sinogram and the synthetized sinogram. Parameters ---------- sinogram_0 The absorbance of the data to align. diodes_ref The data to align. data_container The data container. iteration The current iteration. shifts Current shifts. x_axis_index The index in the array of the geometrical X axis (tilt axis). y_axis_index The index in the array of the geometrical Y axis (main rotation axis). rec_iteration The number of iteration used to solve the tomogram from the projections. high_pass_filter The kernel for high pass filter in the gradient computation applied to avoid phase artifact. W The Tukey window associated with our computation. align_horizontal Apply the horizontal alignment procedure. align_vertical Apply the vertical alignment procedure. center_reconstruction Shift the reconstructed tomogram to the center of the volume to avoid drift in the alignment procedure. use_gpu Use GPU (``True``) or CPU (``False``) for the tomographic computation. Returns ------- Tuple comprising the maximum update in this iteration, the resulting aligned projections found by projecting the reconstructed tomogram using the aligned data, the resulting tomogram reconstructed by the aligned data, and the updated shifts. """ nb_projections = np.size(sinogram_0, -1) # shift the original sinogram_0 by the new value sinogram_shifted = imp.imshift_fft( np.copy(sinogram_0), shifts[iteration, :, 0], shifts[iteration, :, 1]) # shift the original diodes diodes_shifted = imp.imshift_fft(diodes_ref, shifts[iteration, :, 0], shifts[iteration, :, 1]) # update the shifts for SIRT / MITRA for ii in range(data_container.projections.diode.shape[0]): data_container.projections[ii].diode = diodes_shifted[..., ii] # reconstruct the tomogram with the given geometry, shifts, and data tomogram = run_mitra( data_container, use_gpu=use_gpu, maxiter=rec_iteration, ftol=None, enforce_non_negativity=True, optimizer_kwargs=optimizer_kwargs, )['result']['x'] if center_reconstruction and (iteration < 20): tomogram = recenter_tomogram(tomogram) # positivity constraint tomogram[tomogram < 0] = 0 # compute the projected data from the reconstructed tomogram: sinogram_corr if use_gpu: projector = SAXSProjectorCUDA(data_container.geometry) else: projector = SAXSProjector(data_container.geometry) sinogram_corr = projector.forward(tomogram) # remove extra dimension of the sinogram sinogram_corr = np.squeeze(sinogram_corr) # find the optimal shift shift_hor, shift_vect = compute_shift_through_sinogram( sinogram_shifted, np.moveaxis(sinogram_corr, 0, -1), x_axis_index, y_axis_index, high_pass_filter, W, align_horizontal, align_vertical, ) # store the values on the right axis shift_vector = np.zeros((1, nb_projections, 2)) shift_vector[:, :, x_axis_index] = shift_hor shift_vector[:, :, y_axis_index] = shift_vect # apply the optical flow correction method step_relaxation = 0.5 # small relaxation is needed to avoid oscilations shifts[iteration + 1, :, :] = shifts[iteration, :, :] + shift_vector * step_relaxation # remove degree of freedom in the vertical dimension (avoid drifts) shifts[iteration + 1, :, y_axis_index] = shifts[iteration + 1, :, y_axis_index] - np.median( shifts[iteration + 1, :, y_axis_index] ) max_step = np.maximum(np.max(np.abs(shift_hor)), np.max(np.abs(shift_vect))) # put back original data for ii in range(data_container.projections.diode.shape[0]): data_container.projections[ii].diode = diodes_ref[..., ii] return max_step, sinogram_corr, tomogram, shifts def compute_shift_through_sinogram( sinogram_shifted: np.ndarray[float], sinogram_corr: np.ndarray[float], x_axis_index: int, y_axis_index: int, high_pass_filter: float, W: np.ndarray[float], align_horizontal: bool, align_vertical: bool, ) -> tuple[np.ndarray[float], np.ndarray[float]]: """ Compute the shift needed to obtain a better sinogram, based on actual shifted sinogram and the sythetic sinogram. Parameters ---------- sinogram_shifted The sinogram, aka data, to compute the shift correction on. sinogram_corr The sythetic sinogram obtain after reconstruction on the tomogram obtain from the sinogram_shifted and reprojecting it. W The Tukey window associated with our computation. x_axis_index The index in the array of the geometrical X axis (tilt axis). y_axis_index The index in the array of the geometrical Y axis (main rotation axis). high_pass_filter The kernel for high pass filter in the gradient computation, to avoid phase artifact. align_horizontal Compute the horizontal alignment shift. align_vertical Compute the horizontal vertical shift. Returns ------- A tuple comprising the horizontal and vertical shifts. """ nb_projections = np.size(sinogram_shifted, -1) # find the optimal shift d_vect = np.zeros(sinogram_corr.shape) d_hor = np.zeros(sinogram_corr.shape) for index in range(nb_projections): d_hor[..., index], d_vect[..., index] = imp.get_img_grad( imp.smooth_edges(sinogram_corr)[..., index], x_axis_index, y_axis_index ) DS = sinogram_corr - sinogram_shifted # apply high pass filter => get rid of phase artefacts DS = imp.imfilter_high_pass_1d(DS, x_axis_index, high_pass_filter) # align horizontal if align_horizontal: # calculate optimal shift of the 2D projections in horiz direction d_hor = imp.imfilter_high_pass_1d(d_hor, x_axis_index, high_pass_filter).real shift_hor = -( np.sum(W * d_hor * DS, axis=(0, 1)) / np.sum(W * d_hor ** 2 + np.finfo(float).eps, axis=(0, 1)) ) # do not allow more than 1px shift per iteration ! shift_hor = np.minimum(np.ones(shift_hor.shape), np.abs(shift_hor)) * np.sign(shift_hor) else: # if disable shift_hor = np.zeros((1, nb_projections)) # align vertical if align_vertical: # calculate optimal shift of the 2D projections in vert direction d_vect = imp.imfilter_high_pass_1d(d_vect, x_axis_index, high_pass_filter).real shift_vect = -( np.sum(W * d_vect * DS, axis=(0, 1)) / np.sum(W * d_vect ** 2 + np.finfo(float).eps, axis=(0, 1)) ) shift_vect = shift_vect - np.mean(shift_vect) # do not allow more than 1px shift per iteration ! shift_vect = np.minimum(np.ones(shift_vect.shape), np.abs(shift_vect)) * np.sign(shift_vect) else: # if disable shift_vect = np.zeros((1, nb_projections)) return shift_hor, shift_vect def sinogram_line_vertical_alignment( sinogram: np.ndarray[float], main_rot_axis: int, threshold: float = 0 ) -> tuple[np.ndarray[float], float]: """ Compute the vertical, i.e., transverse, weight on a given sinogram and align it by aligning the center of mass. This yields only a very rough alignment but it is very efficient if the misalignment is large. It can be used as the initial step in an alignment procedure or between different steps of the alignment. Parameters ---------- sinogram The sinogram to align, as a stack of 2D arrays. main_rot_axis The index of the vertical axis on the given sinogram. It is the axis perpendicular to the rotation axis ``// y_axis_index``. threshold Maximum value to take into account for the weighting. Returns ------- Computed shifts in the vertical direction. """ horizontal_axis = main_rot_axis # defining the horizontal axis as the one not vertical vertical_axis = 0 if horizontal_axis == 1 else 1 im = np.copy(sinogram) # select the center of the sinogram to have only a 2D array left if vertical_axis == 0: im = im[:, round(sinogram.shape[1] / 2), :] else: im = im[round(sinogram.shape[0] / 2), :, :] center_vect = np.zeros(im.shape[-1]) # threshold value to avoid outliers to influence the alignment too much if threshold > 0: im[im > threshold] = threshold # compute the weights and positions of each vertical line of the sinogram for ii in range(im.shape[-1]): center_vect[ii] = ndimage.center_of_mass(im[:, ii])[0] # we want all vertical center of mass to be aligned with the global center of mass center_vect = -(center_vect - np.mean(center_vect)) return center_vect, vertical_axis def line_vertical_alignment( data_container: DataContainer, threshold: float = 0, **kwargs ) -> np.ndarray[float]: """ Compute the vertical, i.e. transverse weight on the sinogram of a given DataContainer and align it by aligning the center of mass. It is a very rough alignment but very efficient for large misalignment. It can be used as initialisation of an alignment procedure or between different steps of the alignment. Parameters ---------- data_container The DataContainer with the absorption data to align. threshold Maximum value to take into account for the weighting. main_rot_axis : int Geometry parameter. The index of the main rotation axis (``y`` axis on the real geometry) of the array. If not specified, deduced from information in :attr:`data_container`. Returns ------- Computed shifts in the vertical direction. """ # deduce main rotation axis and associated volume from information in data container main_rot_axis_deduced, _ = get_alignment_geometry(data_container) # main rotation axis main_rot_axis = kwargs.get('main_rot_axis', main_rot_axis_deduced) # get absorbance for the sinogram abs_dict = get_absorbances(data_container.diode, normalize_per_projection=True) absorbances = abs_dict['absorbances'] sinogram_0 = np.moveaxis(np.squeeze(absorbances), 0, -1) print(sinogram_0.shape) vertical_shift, vertical_axis = sinogram_line_vertical_alignment(sinogram_0, main_rot_axis) # realign sinogram that was shifted originally shift_all = np.zeros((sinogram_0.shape[-1], 2)) shift_all[:, vertical_axis] = vertical_shift return shift_all def shifts_to_vector( shifts: np.ndarray[float], data_container: DataContainer, **kwargs, ) -> np.ndarray[float]: """ This function expresses the detector-referenced shifts in a 3D Cartesian coordinate system based on the detector frame and the inner and outer axes of the geometry. Parameters ---------- shifts The shifts expressed in the detector frame. data_container The data container containing the geometry information. main_rot_axis : int Geometry parameter. The index of the main rotation axis (``y`` axis on the real geometry) of the array. If not specified, deduced from information in :attr:`data_container`. Returns ------- vector_shifts in 3D coordinates system based on the detector given geometry. """ # deduce main rotation axis and associated volume from information in data container main_rot_axis_deduced, _ = get_alignment_geometry(data_container) # main rotation axis main_rot_axis = kwargs.get('main_rot_axis', main_rot_axis_deduced) other_axis = 0 if main_rot_axis == 1 else 1 vector_shifts = np.array( [( shifts[i, main_rot_axis] * g.inner_axis + shifts[i, other_axis] * g.outer_axis ) for i, g in enumerate(data_container.geometry)] ) return vector_shifts def shifts_to_geometry( data_container: DataContainer, shifts: np.ndarray[float], ) -> tuple[np.ndarray[float], np.ndarray[float]]: """ This function expresses the detector-referenced shifts in a 3D Cartesian coordinate system based on the object coordinate system, i.e., the ``j`` and ``k``-directions. Parameters ---------- data_container The data container containing the geometry information connected to the data. shifts The shifts expressed in the detector frame. Returns ------- Two tuples for the shifts in the j and k directions. """ # computing the shifts in detector space from the projection shifts vector_shifts = shifts_to_vector(shifts, data_container) j_offsets = np.sum(vector_shifts * data_container.geometry.j_direction_0[np.newaxis], axis=1) k_offsets = np.sum(vector_shifts * data_container.geometry.k_direction_0[np.newaxis], axis=1) return j_offsets, k_offsets