Source code for mri.reconstructors.self_calibrating

# -*- coding: utf-8 -*-
##########################################################################
# pySAP - Copyright (C) CEA, 2017 - 2018
# Distributed under the terms of the CeCILL-B license, as published by
# the CEA-CNRS-INRIA. Refer to the LICENSE file or to
# http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.html
# for details.
##########################################################################

"""
This implements the self-calibrating reconstruction for the multi-channel case.
"""
# System import
import warnings

# Module import
from .base import ReconstructorBase
from ..operators import GradSelfCalibrationSynthesis, \
    GradSelfCalibrationAnalysis, GradAnalysis, GradSynthesis, WaveletN
from ..operators.fourier.utils import check_if_fourier_op_uses_sense
from .utils.extract_sensitivity_maps import get_Smaps


[docs]class SelfCalibrationReconstructor(ReconstructorBase): """Self Calibrating reconstruction for multi-channel acquisition. The coil sensitivity is estimated from a small portion of the k-space center and used to reconstruct the complex image. Notes ----- For the Analysis case, finds the solution for x of: ..math:: (1/2) * sum(||F Sl x - yl||^2_2, n_coils) + mu * H( W x ) For the Synthesis case, finds the solution of: ..math:: (1/2) * sum(||F Sl Wt alpha - yl||^2_2, n_coils) + mu * H (alpha) with ..math:: alpha = W x and x = Wt alpha The sensitivity information (Sl) is taken to be the low-resolution of the image extracts from the k-space portion given in the parameter Parameters ---------- fourier_op: object of class FFT, NonCartesianFFT or Stacked3DNFFT in mri.operators Defines the fourier operator F in the above equation. linear_op: object, (optional, default None) Defines the linear sparsifying operator denoted :math:`W` in the equation above. This must operate on x and have 2 functions, op(x) and adj_op(coeff) which implements the operator and adjoint operator. For wavelets, this can be object of class WaveletN or WaveletUD2 from `mri.operators.linear` . If None, sym8 wavelet with nb_scale=3 is chosen. gradient_formulation: str between 'analysis' or 'synthesis', default 'synthesis' defines the formulation of the image model which defines the gradient. kspace_portion: int or tuple (default is 0.1 in all dimension) int or tuple indicating the k-space central portion used to estimate the coil sensitivity information, denoted :math:`Sl` in the above equation. if int, will be evaluated to (0.1,)*nb_dim of the image Smaps: numpy.ndarray (optional, default None) for gradient initialization: Please refer to mri.operators.gradient.base for information. Sensivity maps used to initialize the gradient operator. If set to None, the maps will have to be recomputed once when calling the reconstruct method. The shape should correspond to the shape of the expected volume. smaps_extraction_mode: string 'FFT' | 'NFFT' | 'Stack' | 'gridding' (default) Defines the mode in which we would want to interpolate to extract the sensitivity information when recomputing the sensitivity maps. NOTE: FFT should be considered only if the input has been sampled on the grid smaps_gridding_method: string 'linear' (default) | 'cubic' | 'nearest' For gridding mode, it defines the way interpolation must be done used by the sensitivity extraction method when recomputing the sensitivity maps. n_jobs: int, default 1 The number of CPUs used to accelerate the reconstruction. verbose: int, optional default 0 Verbosity levels 1 => Print basic debug information 5 => Print all initialization information 20 => Calculate cost at the end of each iteration. 30 => Print the debug information of operators if defined by class NOTE - High verbosity (>20) levels are computationally intensive. **kwargs: Extra keyword arguments for gradient initialization: Please refer to mri.operators.gradient.base for information. regularizer_op: operator, (optional default None) Defines the regularization operator for the regularization function H. If None, the regularization chosen is Identity and the optimization turns to gradient descent. See Also -------- ReconstructorBase : parent class """ def __init__(self, fourier_op, linear_op=None, gradient_formulation="synthesis", kspace_portion=0.1, Smaps=None, smaps_extraction_mode='gridding', smaps_gridding_method='linear', n_jobs=1, verbose=0, **kwargs): if linear_op is None: linear_op = WaveletN( # TODO change nb_scales to max_nb_scale - 1 wavelet_name="sym8", nb_scale=3, dim=len(fourier_op.shape), n_coils=1, verbose=bool(verbose >= 30), ) # Add Smaps to kwargs if necessary for gradient initialization if Smaps is not None: kwargs["Smaps"] = Smaps # Ensure that we are in right multichannel config if linear_op.n_coils != 1: raise ValueError("The value of n_coils for linear operation must " "be 1 for Self-Calibrating reconstruction!") if gradient_formulation == 'analysis': if check_if_fourier_op_uses_sense(fourier_op): grad_class = GradAnalysis else: grad_class = GradSelfCalibrationAnalysis elif gradient_formulation == 'synthesis': if check_if_fourier_op_uses_sense(fourier_op): grad_class = GradSynthesis else: grad_class = GradSelfCalibrationSynthesis super().__init__( fourier_op=fourier_op, linear_op=linear_op, gradient_formulation=gradient_formulation, grad_class=grad_class, init_gradient_op=(Smaps is not None), verbose=verbose, **kwargs, ) self.smaps_gridding_method = smaps_gridding_method self.smaps_extraction_mode = smaps_extraction_mode if isinstance(kspace_portion, float): self.kspace_portion = (kspace_portion,) * self.fourier_op.samples.shape[-1] else: self.kspace_portion = kspace_portion if len(self.kspace_portion) != self.fourier_op.samples.shape[-1]: raise ValueError("The k-space portion size used to estimate the" + "sensitivity information is not aligned with" + " the input dimension") self.n_jobs = n_jobs if check_if_fourier_op_uses_sense(fourier_op): self.initialize_gradient_op(**self.extra_grad_args)
[docs] def get_smaps(self): """Get the sensitivity maps. Returns ------- np.ndarray, None The sensitivity maps when given or already computed, or None. """ return self.extra_grad_args.get("Smaps")
[docs] def set_smaps(self, smaps): """Set the sensitivity maps and re-initializes the gradient accordingly. Parameters ---------- smaps: numpy.ndarray for gradient initialization: Please refer to mri.operators.gradient.base for information """ self.extra_grad_args["Smaps"] = smaps self.initialize_gradient_op(**self.extra_grad_args)
[docs] def reconstruct(self, kspace_data, optimization_alg='pogm', x_init=None, num_iterations=100, recompute_smaps=True, **kwargs): """Perform the self-calibrating reconstruction. Parameters ---------- kspace_data: numpy.ndarray the acquired value in the Fourier domain. this is y in above equation. optimization_alg: str (optional, default 'pogm') Type of optimization algorithm to use, 'pogm' | 'fista' | 'condatvu' x_init: numpy.ndarray (optional, default None) input initial guess image for reconstruction. If None, the initialization will be an ndarray of zeros num_iterations: int (optional, default 100) number of iterations of algorithm recompute_smaps: bool (optional, default False) A boolean value to check if the Smaps must be recalculated. Note that this would recompute the lipchitz constant. This must be set to True if you want the Smaps to be updated in this reconstruction. The first reconstruction would need this to be True. """ if self.fourier_op.n_coils != kspace_data.shape[0]: raise ValueError("The provided number of coil (n_coils) do not " "match the data itself") if recompute_smaps is False and 'Smaps' not in self.extra_grad_args: warnings.warn("recompute_smaps was set to False and Smaps was " "not found, re-calculating Smaps and " "initializing gradient anyway!") recompute_smaps = True if recompute_smaps and not check_if_fourier_op_uses_sense(self.fourier_op): # Extract Sensitivity maps and initialize gradient smaps, _ = get_Smaps( k_space=kspace_data, img_shape=self.fourier_op.shape, samples=self.fourier_op.samples, thresh=self.kspace_portion, min_samples=self.fourier_op.samples.min(axis=0), max_samples=self.fourier_op.samples.max(axis=0), mode=self.smaps_extraction_mode, method=self.smaps_gridding_method, n_cpu=self.n_jobs, ) self.set_smaps(smaps) # Start Reconstruction return super().reconstruct( kspace_data, optimization_alg, x_init, num_iterations, **kwargs, )