Source code for mri.reconstructors.utils.extract_sensitivity_maps

# -*- 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 module contains tools to extract sensitivity maps from undersampled MR
acquisition with high density in the k space center.
"""
import warnings

# Package import
from mri.operators import NonCartesianFFT
from mri.operators.utils import gridded_inverse_fourier_transform_nd, \
    convert_locations_to_mask

# Third party import
from joblib import Parallel, delayed
import numpy as np
import scipy.fftpack as pfft


[docs]def extract_k_space_center_and_locations(data_values, samples_locations, thr=None, img_shape=None, window_fun=None, is_fft=False, density_comp=None): r""" This class extract the k space center for a given threshold and extracts the corresponding sampling locations Parameters ---------- data_values: numpy.ndarray The value of the samples samples_locations: numpy.ndarray The samples location in the k-sapec domain (between [-0.5, 0.5[) thr: tuple or float The threshold used to extract the k_space center img_shape: tuple The image shape to estimate the cartesian density is_fft: bool default False Checks if the incoming data is from FFT, in which case, masking can be done more directly density_comp: numpy.ndarray default None The density compensation for kspace data in case it exists and we use density compensated adjoint for Smap estimation window_fun: "Hann", "Hanning", "Hamming", or a callable, default None. The window function to apply to the selected data. It is computed with the center locations selected. Only works with circular mask. If window_fun is a callable, it takes as input the array (n_samples x n_dims) of sample positions and returns an array of n_samples weights to be applied to the selected k-space values, before the smaps estimation. Returns ------- The extracted center of the k-space, i.e. both the kspace locations and kspace values. If the density compensators are passed, the corresponding compensators for the center of k-space data will also be returned. The return stypes for density compensation and kspace data is same as input Notes ----- The Hann (or Hanning) and Hamming windows of width :math:`2\theta` are defined as: .. math:: w(x,y) = a_0 - (1-a_0) * \cos(\pi * \sqrt{x^2+y^2}/\theta), \sqrt{x^2+y^2} \le \theta In the case of Hann window :math:`a_0=0.5`. For Hamming window we consider the optimal value in the equiripple sense: :math:`a_0=0.53836`. .. Wikipedia:: https://en.wikipedia.org/wiki/Window_function#Hann_and_Hamming_windows """ if thr is None: if img_shape is None: raise ValueError('target image cartesian image shape must be fill') raise NotImplementedError if data_values.ndim > 2: warnings.warn('Data Values seem to have rank ' + str(data_values.ndim) + ' (>2). Using is_fft for now.') is_fft = True if is_fft: img_shape = np.asarray(data_values[0].shape) mask = convert_locations_to_mask(samples_locations, img_shape) indices = np.where(np.reshape(mask, mask.size))[0] data_ordered = np.asarray([ np.reshape(data_values[channel], mask.size)[indices] for channel in range(data_values.shape[0])]) else: data_ordered = np.copy(data_values) if window_fun is None: if isinstance(thr, float): thr = (thr,) * samples_locations.shape[1] condition = np.logical_and.reduce( tuple(np.abs(samples_locations[:, i]) <= thr[i] for i in range(len(thr)))) elif isinstance(thr, float): condition = np.sum(np.square(samples_locations), axis=1) <= thr**2 else: raise ValueError("threshold type is not supported with select window") index = np.linspace(0, samples_locations.shape[0] - 1, samples_locations.shape[0], dtype=np.int64) index = np.extract(condition, index) center_locations = samples_locations[index, :] data_thresholded = data_ordered[:, index] if window_fun is not None: if callable(window_fun): window = window_fun(center_locations) else: if window_fun == "Hann" or window_fun == "Hanning": a_0 = 0.5 elif window_fun == "Hamming": a_0 = 0.53836 else: raise ValueError("Unsupported window function.") radius = np.linalg.norm(center_locations, axis=1) window = a_0 + (1 - a_0) * np.cos(np.pi * radius / thr) data_thresholded = window * data_thresholded if density_comp is not None: density_comp = density_comp[index] return data_thresholded, center_locations, density_comp else: return data_thresholded, center_locations
[docs]def get_Smaps(k_space, img_shape, samples, thresh, min_samples, max_samples, mode='NFFT', method='linear', window_fun=None, density_comp=None, n_cpu=1, fourier_op_kwargs={}): r""" Get Smaps for from pMRI sample data. Estimate the sensitivity maps information from parallel mri acquisition and for variable density sampling scheme where the k-space center had been heavily sampled. Reference : Self-Calibrating Nonlinear Reconstruction Algorithms for Variable Density Sampling and Parallel Reception MRI https://ieeexplore.ieee.org/abstract/document/8448776 Parameters ---------- k_space: numpy.ndarray The acquired kspace of shape (M,L), where M is the number of samples acquired and L is the number of coils used img_shape: tuple The final output shape of Sensitivity Maps. samples: numpy.ndarray The sample locations where the above k_space data was acquired thresh: tuple The value of threshold in kspace for thresholding k-space center min_samples: tuple The minimum values in k-space where gridding must be done max_samples: tuple The maximum values in k-space where gridding must be done mode: string 'FFT' | 'NFFT' | 'gridding' , default='NFFT' Defines the mode in which we would want to interpolate, NOTE: FFT should be considered only if the input has been sampled on the grid method: string 'linear' | 'cubic' | 'nearest', default='linear' For gridding mode, it defines the way interpolation must be done window_fun: "Hann", "Hanning", "Hamming", or a callable, default None. The window function to apply to the selected data. It is computed with the center locations selected. Only works with circular mask. If window_fun is a callable, it takes as input the n_samples x n_dims of samples position and would return an array of n_samples weight to be applied to the selected k-space values, before the smaps estimation. density_comp: numpy.ndarray default None The density compensation for kspace data in case it exists and we use density compensated adjoint for Smap estimation n_cpu: int default=1 Number of parallel jobs in case of parallel MRI fourier_op_kwargs: dict, default {} The keyword arguments given to fourier_op initialization if mode == 'NFFT'. If None, we choose implementation of fourier op to 'gpuNUFFT' if library is installed. Returns ------- Smaps: numpy.ndarray the estimated sensitivity maps of shape (img_shape, L) with L the number of channels SOS: numpy.ndarray The sum of Square used to extract the sensitivity maps Notes ----- The Hann (or Hanning) and Hamming window of width :math:`2\theta` are defined as: .. math:: w(x,y) = a_0 - (1-a_0) * \cos(\pi * \sqrt{x^2+y^2}/\theta), \sqrt{x^2+y^2} \le \theta In the case of Hann window :math:`a_0=0.5`. For Hamming window we consider the optimal value in the equiripple sens: :math:`a_0=0.53836`. .. Wikipedia:: https://en.wikipedia.org/wiki/Window_function#Hann_and_Hamming_windows """ if len(min_samples) != len(img_shape) \ or len(max_samples) != len(img_shape): raise NameError('The img_shape, max_samples, and ' 'min_samples must be of same length') k_space, samples, *density_comp = \ extract_k_space_center_and_locations( data_values=k_space, samples_locations=samples, thr=thresh, img_shape=img_shape, is_fft=mode == 'FFT', window_fun=window_fun, density_comp=density_comp, ) if density_comp: density_comp = density_comp[0] else: density_comp = None if samples is None: mode = 'FFT' L, M = k_space.shape Smaps_shape = (L, *img_shape) Smaps = np.zeros(Smaps_shape).astype('complex128') if mode == 'FFT': if not M == img_shape[0]*img_shape[1]: raise ValueError(['The number of samples in the k-space must be', 'equal to the (image size, the number of coils)' ]) k_space = k_space.reshape(Smaps_shape) for l in range(Smaps_shape[2]): Smaps[l] = pfft.ifftshift(pfft.ifft2(pfft.fftshift(k_space[l]))) elif mode == 'NFFT': fourier_op = NonCartesianFFT( samples=samples, shape=img_shape, density_comp=density_comp, n_coils=L, **fourier_op_kwargs, ) Smaps = fourier_op.adj_op(np.ascontiguousarray(k_space)) elif mode == 'gridding': grid_space = [np.linspace(min_samples[i], max_samples[i], num=img_shape[i], endpoint=False) for i in np.arange(np.size(img_shape))] grid = np.meshgrid(*grid_space) Smaps = \ Parallel(n_jobs=n_cpu, verbose=1, mmap_mode='r+')( delayed(gridded_inverse_fourier_transform_nd)( kspace_loc=samples, kspace_data=k_space[l], grid=tuple(grid), method=method ) for l in range(L) ) Smaps = np.asarray(Smaps) else: raise ValueError('Bad smap_extract_mode chosen! ' 'Please choose between : ' '`FFT` | `NFFT` | `gridding` | `Stack`') SOS = np.sqrt(np.sum(np.abs(Smaps)**2, axis=0)) for r in range(L): Smaps[r] /= SOS return Smaps, SOS