| """ |
| Copyright (c) Facebook, Inc. and its affiliates. |
| |
| This source code is licensed under the MIT license found in the |
| LICENSE file in the root directory of this source tree. |
| """ |
|
|
| import contextlib |
|
|
| import numpy as np |
| import torch |
|
|
|
|
| @contextlib.contextmanager |
| def temp_seed(rng, seed): |
| state = rng.get_state() |
| rng.seed(seed) |
| try: |
| yield |
| finally: |
| rng.set_state(state) |
|
|
|
|
| def create_mask_for_mask_type(mask_type_str, center_fractions, accelerations): |
| if mask_type_str == "random": |
| return RandomMaskFunc(center_fractions, accelerations) |
| elif mask_type_str == "equispaced": |
| return EquispacedMaskFunc(center_fractions, accelerations) |
| else: |
| raise Exception(f"{mask_type_str} not supported") |
|
|
|
|
| class MaskFunc(object): |
| """ |
| An object for GRAPPA-style sampling masks. |
| |
| This crates a sampling mask that densely samples the center while |
| subsampling outer k-space regions based on the undersampling factor. |
| """ |
|
|
| def __init__(self, center_fractions, accelerations): |
| """ |
| Args: |
| center_fractions (List[float]): Fraction of low-frequency columns to be |
| retained. If multiple values are provided, then one of these |
| numbers is chosen uniformly each time. |
| accelerations (List[int]): Amount of under-sampling. This should have |
| the same length as center_fractions. If multiple values are |
| provided, then one of these is chosen uniformly each time. |
| """ |
| if len(center_fractions) != len(accelerations): |
| raise ValueError( |
| "Number of center fractions should match number of accelerations" |
| ) |
|
|
| self.center_fractions = center_fractions |
| self.accelerations = accelerations |
| self.rng = np.random |
|
|
| def choose_acceleration(self): |
| """Choose acceleration based on class parameters.""" |
| choice = self.rng.randint(0, len(self.accelerations)) |
| center_fraction = self.center_fractions[choice] |
| acceleration = self.accelerations[choice] |
|
|
| return center_fraction, acceleration |
|
|
|
|
| class RandomMaskFunc(MaskFunc): |
| """ |
| RandomMaskFunc creates a sub-sampling mask of a given shape. |
| |
| The mask selects a subset of columns from the input k-space data. If the |
| k-space data has N columns, the mask picks out: |
| 1. N_low_freqs = (N * center_fraction) columns in the center |
| corresponding to low-frequencies. |
| 2. The other columns are selected uniformly at random with a |
| probability equal to: prob = (N / acceleration - N_low_freqs) / |
| (N - N_low_freqs). This ensures that the expected number of columns |
| selected is equal to (N / acceleration). |
| |
| It is possible to use multiple center_fractions and accelerations, in which |
| case one possible (center_fraction, acceleration) is chosen uniformly at |
| random each time the RandomMaskFunc object is called. |
| |
| For example, if accelerations = [4, 8] and center_fractions = [0.08, 0.04], |
| then there is a 50% probability that 4-fold acceleration with 8% center |
| fraction is selected and a 50% probability that 8-fold acceleration with 4% |
| center fraction is selected. |
| """ |
|
|
| def __call__(self, shape, seed=None): |
| """ |
| Create the mask. |
| |
| Args: |
| shape (iterable[int]): The shape of the mask to be created. The |
| shape should have at least 3 dimensions. Samples are drawn |
| along the second last dimension. |
| seed (int, optional): Seed for the random number generator. Setting |
| the seed ensures the same mask is generated each time for the |
| same shape. The random state is reset afterwards. |
| |
| Returns: |
| torch.Tensor: A mask of the specified shape. |
| """ |
| if len(shape) < 3: |
| raise ValueError("Shape should have 3 or more dimensions") |
|
|
| with temp_seed(self.rng, seed): |
| num_cols = shape[-2] |
| center_fraction, acceleration = self.choose_acceleration() |
|
|
| |
| num_low_freqs = int(round(num_cols * center_fraction)) |
| prob = (num_cols / acceleration - num_low_freqs) / ( |
| num_cols - num_low_freqs |
| ) |
| mask = self.rng.uniform(size=num_cols) < prob |
| pad = (num_cols - num_low_freqs + 1) // 2 |
| mask[pad : pad + num_low_freqs] = True |
|
|
| |
| mask_shape = [1 for _ in shape] |
| mask_shape[-2] = num_cols |
| mask = torch.from_numpy(mask.reshape(*mask_shape).astype(np.float32)) |
|
|
| return mask |
|
|
|
|
| class EquispacedMaskFunc(MaskFunc): |
| """ |
| EquispacedMaskFunc creates a sub-sampling mask of a given shape. |
| |
| The mask selects a subset of columns from the input k-space data. If the |
| k-space data has N columns, the mask picks out: |
| 1. N_low_freqs = (N * center_fraction) columns in the center |
| corresponding tovlow-frequencies. |
| 2. The other columns are selected with equal spacing at a proportion |
| that reaches the desired acceleration rate taking into consideration |
| the number of low frequencies. This ensures that the expected number |
| of columns selected is equal to (N / acceleration) |
| |
| It is possible to use multiple center_fractions and accelerations, in which |
| case one possible (center_fraction, acceleration) is chosen uniformly at |
| random each time the EquispacedMaskFunc object is called. |
| |
| Note that this function may not give equispaced samples (documented in |
| https://github.com/facebookresearch/fastMRI/issues/54), which will require |
| modifications to standard GRAPPA approaches. Nonetheless, this aspect of |
| the function has been preserved to match the public multicoil data. |
| """ |
|
|
| def __call__(self, shape, seed): |
| """ |
| Args: |
| shape (iterable[int]): The shape of the mask to be created. The |
| shape should have at least 3 dimensions. Samples are drawn |
| along the second last dimension. |
| seed (int, optional): Seed for the random number generator. Setting |
| the seed ensures the same mask is generated each time for the |
| same shape. The random state is reset afterwards. |
| |
| Returns: |
| torch.Tensor: A mask of the specified shape. |
| """ |
| if len(shape) < 3: |
| raise ValueError("Shape should have 3 or more dimensions") |
|
|
| with temp_seed(self.rng, seed): |
| center_fraction, acceleration = self.choose_acceleration() |
| num_cols = shape[-2] |
| num_low_freqs = int(round(num_cols * center_fraction)) |
|
|
| |
| mask = np.zeros(num_cols, dtype=np.float32) |
| pad = (num_cols - num_low_freqs + 1) // 2 |
| mask[pad : pad + num_low_freqs] = True |
|
|
| |
| adjusted_accel = (acceleration * (num_low_freqs - num_cols)) / ( |
| num_low_freqs * acceleration - num_cols |
| ) |
| offset = self.rng.randint(0, round(adjusted_accel)) |
|
|
| accel_samples = np.arange(offset, num_cols - 1, adjusted_accel) |
| accel_samples = np.around(accel_samples).astype(np.uint) |
| mask[accel_samples] = True |
|
|
| |
| mask_shape = [1 for _ in shape] |
| mask_shape[-2] = num_cols |
| mask = torch.from_numpy(mask.reshape(*mask_shape).astype(np.float32)) |
|
|
| return mask |
|
|