Source code for snake.core.handlers.motion.image

"""Motion in the image domain."""

from copy import deepcopy

import numpy as np
from numpy.typing import NDArray

from ...phantom import DynamicData, Phantom
from ...simulation import SimConfig
from ..base import AbstractHandler
from .utils import add_motion, motion_generator


[docs] class RandomMotionImageHandler(AbstractHandler): """Add Random Motion in Image. Parameters ---------- ts_std_mm Translation standard deviation, in mm/s. rs_std_mm Rotation standard deviation, in radians/s. motion_file: str If provided, the motion file is loaded and resampled to match the number of frames in the simulation. The motion is then added to the data. motion_file_tr: float Original TR of the motion file, in seconds. Notes ----- The motion is generated by drawing from a normal distribution with standard deviation for the 6 motion parameters (3 translations and 3 rotations, in this order). Then the cumulative motion is computed by summing the motion at each frame. The handlers is parametrized with speed in mm/s and rad/s, as these values provides an independent control of the motion amplitude regardless of the time resolution for the simulation. """ __handler_name__ = "motion-image" ts_std_mms: tuple[float, float, float] | None = None rs_std_degs: tuple[float, float, float] | None = None motion_file: str | None = None motion_file_tr_ms: float | None = None def __post_init__(self): if (self.ts_std_mms is None or self.rs_std_degs is None) and ( self.motion_file is None or self.motion_file_tr is None ): raise ValueError( "At least one of ts_std_mm, rs_std_mm or motion_file must be provided." ) self._motion_data = None if self.motion_file is not None: # load the motion file self._motion_data = np.loadtxt(self.motion_file)
[docs] def get_dynamic(self, phantom: Phantom, sim_conf: SimConfig) -> DynamicData: """Get dynamic information.""" n_frames = sim_conf.max_n_shots if self._motion_data is not None and self.motion_file_tr_ms is not None: # resample the motion data to match the simulation framerate. motion = np.interp( np.arange(n_frames) * sim_conf.sim_tr_ms, np.arange(len(self._motion_data)) * self.motion_file_tr_ms, self._motion_data, ) elif self.rs_std_degs is not None and self.ts_std_mms is not None: ts_std_pix = np.array(self.ts_std_mms) / np.array(sim_conf.res_mm) motion = motion_generator( n_frames, ts_std_pix, self.rs_std_degs, sim_conf.sim_tr_ms / 1000, sim_conf.rng, ) return DynamicData( name=self.__handler_name__, data=motion.T, func=apply_motion_to_phantom, )
[docs] def apply_motion_to_phantom( phantom: Phantom, motions: NDArray, time_idx: int ) -> Phantom: """Apply motion to the phantom.""" new_phantom = deepcopy(phantom) for i, tissue_mask in enumerate(new_phantom.masks): # TODO Parallel ? new_phantom.masks[i] = add_motion(tissue_mask, motions[:, time_idx]) return new_phantom