def sum_of_squares(arr, coil_axis=-1, verbose=D_VERB_LVL): """ Sum-of-Squares coil combination method. Note: this function returns the same array used for input except for the normalization, therefore the `coil_axis` parameter is left unused. Args: arr (np.ndarray): The input array. coil_axis (int): The coil dimension. The dimension of `arr` along which single coil elements are stored. verbose (int): Set level of verbosity. Returns: result (tuple): The tuple contains: - combined (np.ndarray): The combined data. - sens (np.ndarray): The coil sensitivity. References: - Roemer, P.B., Edelstein, W.A., Hayes, C.E., Souza, S.P., Mueller, O.M., 1990. The NMR phased array. Magn Reson Med 16, 192–225. doi:10.1002/mrm.1910160203 """ # combined = np.sqrt(np.abs(np.sum(arr * arr.conj(), axis=coil_axis))) coil_axis = fc.valid_index(coil_axis, arr.ndim) broadcast_shape = [ d if i != coil_axis else 1 for i, d in enumerate(arr.shape) ] combined = np.sum(np.abs(arr), axis=coil_axis) return combined, np.abs(arr) / combined.reshape(broadcast_shape)
def msense_1d(arr, acceleration=2, autocalib=16, acceleration_axis=0, coil_axis=-1): """ Perform modified SENSE reconstruction with 1D-accelerated cartesian k-data. The coil sensitivity is estimated from the autocalibration lines. Args: arr (np.ndarray): The input array. Data is in k-space and missing k-space lines are zero-filled. acceleration (int): The acceleration factor (along 1 dimension). autocalib (int): The number of central k-space lines acquired. acceleration_axis (int): The accelerated dimension. coil_axis (int): The coil dimension. The dimension of `arr` along which single coil elements are stored. Returns: arr (np.ndarray): The output array. See Also: - Pruessmann, Klaas P., Markus Weiger, Markus B. Scheidegger, and Peter Boesiger. 1999. “SENSE: Sensitivity Encoding for Fast MRI.” Magnetic Resonance in Medicine 42 (5): 952–62. https://doi.org/10.1002/ (SICI)1522-2594(199911)42:5<952::AID-MRM16>3.0.CO;2-S. - Griswold, Mark A., Felix Breuer, Martin Blaimer, Stephan Kannengiesser, Robin M. Heidemann, Matthias Mueller, Mathias Nittka, Vladimir Jellus, Berthold Kiefer, and Peter M. Jakob. 2006. “Autocalibrated Coil Sensitivity Estimation for Parallel Imaging.” NMR in Biomedicine 19 (3): 316–24. https://doi.org/10.1002/nbm.1048. """ # : ensure coil axis is the last assert (-arr.ndim <= coil_axis < arr.ndim) coil_axis = fc.valid_index(coil_axis, arr.ndim) last_axis = -1 % arr.ndim if coil_axis != last_axis: arr = np.swapaxes(arr, coil_axis, last_axis) # : prepare parameters acc_factors = tuple(acceleration if i == acceleration_axis else 1 if i != coil_axis else None for i in range(arr.ndim)) acc_slicing = acceleration_slices(arr.shape, acc_factors) autocalib_slicing = autocalib_slices(arr.shape, autocalib, acc_factors) acc_arr = arr[acc_slicing] calib_arr = arr[autocalib_slicing] if coil_axis != last_axis: result = np.swapaxes(result, last_axis, coil_axis) return result
def i_split(arr, axis=-1): """ Split an array along a specific axis into a list of arrays This is a generator version of `numpy.split()`. Args: arr (ndarray): The N-dim array to split. axis (int): Direction for the splitting of the array. Yields: arr (ndarray): The next (N-1)-dim array from the splitting. All yielded arrays have the same shape. """ assert (-arr.ndim <= axis < arr.ndim) axis = fc.valid_index(axis, arr.ndim) for i in range(arr.shape[axis]): slicing = tuple( slice(None) if j != axis else i for j, d in enumerate(arr.shape)) yield arr[slicing]
def i_stack(arrs, num, axis=-1): """ Stack an iterable of arrays of the same size along a specific axis. This is equivalent to `numpy.stack()` but fetches the arrays from an iterable instead. This is useful for reducing the memory footprint of stacking compared to `numpy.stack()` and similar functions, since it does not require a full sequence of the data to reside in memory. Args: arrs (Iterable[ndarray]): The (N-1)-dim arrays to stack. These must have the same shape. num (int): The number of arrays to stack. axis (int): Direction along which to stack the arrays. Supports also negative values. Returns: result (ndarray): The concatenated N-dim array. """ iter_arrs = iter(arrs) arr = next(iter_arrs) ndim = arr.ndim + 1 assert (-ndim <= axis < ndim) axis = fc.valid_index(axis, ndim) base_shape = arr.shape shape = base_shape[:axis] + (num, ) + base_shape[axis:] result = np.empty(shape, dtype=arr.dtype) slicing = tuple( slice(None) if j != axis else 0 for j, d in enumerate(shape)) result[slicing] = arr for i, arr in enumerate(iter_arrs, 1): slicing = tuple( slice(None) if j != axis else i for j, d in enumerate(shape)) result[slicing] = arr return result
def grappa_1d(arr, acceleration=2, autocalib=16, kernel_span=1, acceleration_axis=0, coil_axis=-1): """ Perform GRAPPA interpolation with 1D-accelerated cartesian k-data. Args: arr (np.ndarray): The input array. Data is in k-space and missing k-space lines are zero-filled. acceleration (int): The acceleration factor (along 1 dimension). autocalib (int): The number of central k-space lines acquired. kernel_span (int): The half-size of the kernel. The kernel window size in the non-accelerated dimension is given by: `kernel_size = kernel_span * 2 + 1`. Kernel span must be non-negative. The kernel window size in the accelerated dimension is equal to the `acceleration + 1`. acceleration_axis (int): The accelerated dimension. coil_axis (int): The coil dimension. The dimension of `arr` along which single coil elements are stored. Returns: arr (np.ndarray): The output array in k-space. See Also: - Griswold, Mark A., Peter M. Jakob, Robin M. Heidemann, Mathias Nittka, Vladimir Jellus, Jianmin Wang, Berthold Kiefer, and Axel Haase. “Generalized Autocalibrating Partially Parallel Acquisitions (GRAPPA).” Magnetic Resonance in Medicine 47, no. 6 (June 1, 2002): 1202–10. https://doi.org/10.1002/mrm.10171. """ # : ensure coil axis is the last assert (-arr.ndim <= coil_axis < arr.ndim) coil_axis = fc.valid_index(coil_axis, arr.ndim) last_axis = -1 % arr.ndim if coil_axis != last_axis: arr = np.swapaxes(arr, coil_axis, last_axis) # : prepare parameters acc_factors = tuple(acceleration if i == acceleration_axis else 1 if i != coil_axis else None for i in range(arr.ndim)) acc_slicing = acceleration_slices(arr.shape, acc_factors) autocalib_slicing = autocalib_slices(arr.shape, autocalib, acc_factors) acc_arr = arr[acc_slicing] calib_arr = arr[autocalib_slicing] kernel_size = kernel_span * 2 + 1 kernel_window = tuple( 1 if factor is None else kernel_size if factor == 1 else factor + 1 for factor in acc_factors) kernel_calib_size = 2 * kernel_size # number of target points within a kernel window n_targets = acceleration - 1 # : define target and calibration matrices calib_padded_arr = fcn.nd_windowing(calib_arr, kernel_window) target_slicing = \ tuple(slice(None) for _ in calib_arr.shape) \ + tuple((slice(None) if factor is None else slice(kernel_size // 2, kernel_size // 2 + 1) if factor == 1 else slice(1, factor)) for factor in acc_factors) target_arr = calib_padded_arr[target_slicing] \ .reshape(-1, calib_arr.shape[-1] * n_targets) calib_mat_slicing = \ tuple(slice(None) for _ in calib_arr.shape) \ + tuple( (slice(None) if factor is None or factor == 1 else (0, factor)) for factor in acc_factors) calib_mat_arr = calib_padded_arr[calib_mat_slicing] \ .reshape(-1, calib_arr.shape[-1] * kernel_calib_size) # : compute calibration weights weights_arr, _, _, _ = np.linalg.lstsq(calib_mat_arr, target_arr, rcond=None) # : use weights to compute missing k-space values # todo: avoid computing useless lines instead of selecting missing lines source_padded_arr = fcn.rolling_window_nd(arr, kernel_window, 1, out_mode='same') source_mat_arr = source_padded_arr[calib_mat_slicing] \ .reshape(-1, calib_arr.shape[-1] * kernel_calib_size) unknown_arr = np.dot(source_mat_arr, weights_arr) # : fill-in GRAPPA-reconstructed missing points result = np.zeros_like(arr) unknown_shape = source_padded_arr.shape[:arr.ndim] + (n_targets, ) unknown_arr = unknown_arr.reshape(unknown_shape) for i in range(n_targets): target_missing_slicing = tuple( slice(None) if factor is None else slice(kernel_span, -kernel_span) if factor == 1 else slice(i + 1, None, factor) for dim, factor in zip(arr.shape, acc_factors)) source_missing_slicing = tuple( slice(None) if factor is None else slice(kernel_span, -kernel_span) if factor == 1 else slice(n_targets, dim - factor + n_targets + 1, factor) for dim, factor in zip(arr.shape, acc_factors)) result[target_missing_slicing] = \ unknown_arr[..., i][source_missing_slicing] result[autocalib_slicing] = calib_arr result[acc_slicing] = acc_arr if coil_axis != last_axis: result = np.swapaxes(result, last_axis, coil_axis) return result
def combine(arr, method='block_adaptive_iter', method_kws=None, compression='compress_svd', compression_kws=None, coil_axis=-1, split_axis=None, verbose=D_VERB_LVL): """ Calculate the combination of multiple coil elements. An optional coil compression preprocessing step can be used to reduce both the computational complexity and (eventually) the noise. Note coil combination can be seen as a particular case of coil compression where the coils are compressed to a single one. If this is the desired behavior, `complex_sum` should be used as `method`. However, coil compression methods are typically not suitable for coil combination. Args: arr (np.ndarray): The input array. method (str): The combination method. If str, uses the specified method as found in this module. Some methods require `ref` and/or `multi_axis` to be set in `method_kws`. Accepted values not requiring `ref` or `multi_axis` are: - 'complex_sum': use `pymrt.recipes.coils.complex_sum()`; - 'sum_of_squares': use `pymrt.recipes.coils.sum_of_squares()`; - 'adaptive': use `pymrt.recipes.coils.adaptive()`; - 'block_adaptive': use `pymrt.recipes.coils.block_adaptive()`; - 'adaptive_iter': use `pymrt.recipes.coils.adaptive_iter()`; - 'block_adaptive_iter': use `pymrt.recipes.coils.block_adaptive_iter()`; Accepted values requiring `ref` but not `multi_axis` are: Not implemented yet. Accepted values requiring `multi_axis` but not `ref` are: - 'multi_svd': use `pymrt.recipes.coils.mult_svd()` Accepted values requiring both `ref` and `multi_axis` are: Not implemented yet. method_kws (Mappable|None): Keyword arguments to pass to `method`. If None, only `coil_axis`, `split_axis`, `verbose` are passed. compression (callable|str|None): The compression method. This is passed as `method` to `compress`. compression_kws (Mappable|None): Keyword arguments to pass to `compression`. This is passed as `method_kwd` to `compress`. coil_axis (int): The coil dimension. The dimension of `arr` along which single coil elements are stored. split_axis (int|None): The split dimension. If int, indicates the dimension of `arr` along which the algorithm is sequentially applied to reduce memory usage, but at the cost of accuracy. If None, the algorithm is applied to the whole `arr` at once. verbose (int): Set level of verbosity. Returns: arr (np.ndarray): The combined array. """ begin_time = datetime.datetime.now() sens_methods = ('complex_sum', 'sum_of_squares', 'adaptive', 'block_adaptive', 'adaptive_iter', 'block_adaptive_iter', 'multi_svd') methods = sens_methods + ('virtual_ref', 'multi_svd') if compression: arr = compress(arr, method=compression, method_kws=compression_kws, coil_axis=coil_axis, verbose=verbose) method = method.lower() msg('method={}'.format(method), verbose, VERB_LVL['medium']) method_kws = {} if method_kws is None else dict(method_kws) has_sens = method in sens_methods if method in methods: method = globals()[method] if not callable(method): text = ('Unknown method `{}` in `recipes.coils.combine(). ' + 'Using fallback `{}`.'.format(method, methods[0])) warnings.warn(text) method = globals()[methods[0]] has_sens = True if split_axis is not None: assert (-arr.ndim <= coil_axis < arr.ndim) assert (-arr.ndim <= split_axis < arr.ndim) coil_axis = fc.valid_index(coil_axis, arr.ndim) shape = arr.shape combined = np.zeros(tuple(d for i, d in enumerate(shape) if i != coil_axis), dtype=complex) split_axis = split_axis % arr.ndim combined = np.swapaxes(combined, split_axis, 0) arr = np.swapaxes(arr, split_axis, 0) msg(': split={}'.format(shape[split_axis]), verbose, VERB_LVL['medium'], end='\n', flush=True) for i in range(shape[split_axis]): msg('{}'.format(i + 1), verbose, VERB_LVL['high'], end=' ' if i + 1 < shape[split_axis] else '\n', flush=True) if has_sens: combined[i, ...], _ = method(arr[i, ...], coil_axis=coil_axis, verbose=verbose, **dict(method_kws)) del _ else: combined[i, ...] = method(arr[i, ...], coil_axis=coil_axis, verbose=verbose, **dict(method_kws)) combined = np.swapaxes(combined, 0, split_axis) arr = np.swapaxes(arr, 0, split_axis) else: if has_sens: combined, _ = method(arr, coil_axis=coil_axis, verbose=verbose, **dict(method_kws)) del _ else: combined = method(arr, coil_axis=coil_axis, verbose=verbose, **dict(method_kws)) if np.isclose(np.mean(np.abs(np.angle(combined))), 0.0, equal_nan=True): combined = combined.astype(complex) msg('Adding summed phase.', verbose, VERB_LVL['medium']) combined *= np.exp(1j * np.angle(np.sum(arr, axis=coil_axis))) end_time = datetime.datetime.now() msg('ExecTime({}): {}'.format('coils.combine', end_time - begin_time), verbose, D_VERB_LVL) return combined
def adaptive_iter(arr, filtering=None, filtering_kws=None, max_iter=16, threshold=1e-8, coil_axis=-1, verbose=D_VERB_LVL): """ Adaptive Iterative coil combination method. This is an iterative and faster implementation of the algorithm for computing 'adaptive' sensitivity, which allows for a simpler formulation for phase correction. Args: arr (np.ndarray): The input array. filtering (callable|None): The filtering function. If callable, it is used to separate the sensitivity from the input. Typically, a low-pass filter is used, under the assumption that the coil sensitivity is smooth compared to the sources. If None, no separation is performed. filtering_kws (Mappable|None): Keyword arguments to pass to `filtering`. max_iter (int): Maximum number of iterations. If `threshold` > 0, the algorithm may stop earlier. threshold (float): Threshold for next iteration. If the next iteration globally modifies the sensitivity by less than `threshold`, the algorithm stops. coil_axis (int): The coil dimension. The dimension of `arr` along which single coil elements are stored. verbose (int): Set level of verbosity. Returns: result (tuple): The tuple contains: - combined (np.ndarray): The combined data. - sens (np.ndarray): The coil sensitivity. References: - Walsh, D.O., Gmitro, A.F., Marcellin, M.W., 2000. Adaptive reconstruction of phased array MR imagery. Magn. Reson. Med. 43, 682–690. doi:10.1002/(SICI)1522-2594( 200005)43:5<682::AID-MRM10>3.0.CO;2-G - Inati, S.J., Hansen, M.S., Kellman, P., 2013. A Solution to the Phase Problem in Adaptive Coil Combination, in: Proceedings of the ISMRM 21st Annual Meeting & Exhibition. Presented at the 21st Annual Meeting & Exhibition of the International Society for Magnetic Resonance in Medicine, ISMRM, Salt Lake City, Utah, USA. - Inati, S.J., Hansen, M.S., Kellman, P., 2014. A Fast Optimal Method for Coil Sensitivity Estimation and Adaptive Coil Combination for Complex Images, in: Proceedings of the ISMRM 22nd Annual Meeting & Exhibition. Presented at the 22nd Annual Meeting & Exhibition of the International Society for Magnetic Resonance in Medicine, ISMRM, Milan, Italy. """ assert (-arr.ndim <= coil_axis < arr.ndim) coil_axis = fc.valid_index(coil_axis, arr.ndim) last_axis = -1 % arr.ndim if coil_axis != last_axis: arr = np.swapaxes(arr, coil_axis, last_axis) msg('arr.shape={}'.format(arr.shape), verbose, VERB_LVL['debug']) msg('threshold={}'.format(threshold), verbose, VERB_LVL['debug']) msg('max_iter={}'.format(max_iter), verbose, VERB_LVL['debug']) epsilon = np.spacing(1.0) other_axes = tuple(range(0, arr.ndim - 1)) with np.errstate(divide='ignore', invalid='ignore'): weights = np.sum(arr, other_axes) weights /= np.linalg.norm(weights) # combined == weighted combined = np.einsum('...i,i', arr, weights.conj()) sens = np.zeros_like(arr, dtype=complex) delta = 1.0 for i in range(max_iter): last_combined = combined.copy() if threshold > 0 else combined sens = arr * combined[..., None].conj() if filtering: sens = fcn.filter_cx(sens, filtering, (), filtering_kws) sens /= (np.sqrt(np.sum(sens * sens.conj(), -1)) + epsilon)[..., None] combined = np.sum(sens.conj() * arr, -1) # include the additional phase weights = np.sum(sens * combined[..., None], other_axes) weights /= np.linalg.norm(weights) weighted = np.einsum('...i,i', sens, weights.conj()) weighted /= (np.abs(weighted) + epsilon) combined *= weighted sens *= weighted[..., None].conj() msg('{}'.format(i + 1), verbose, VERB_LVL['debug'], end=' ' if threshold else ', ', flush=True) if threshold > 0: last_delta = delta delta = (np.linalg.norm(combined - last_combined) / np.linalg.norm(combined)) msg('delta={}'.format(delta), verbose, VERB_LVL['debug'], end=', ' if i + 1 < max_iter else '.\n', flush=True) if delta < threshold or last_delta < delta: break if coil_axis != last_axis: sens = np.swapaxes(sens, last_axis, coil_axis) return combined, sens
def adaptive(arr, filtering=None, filtering_kws=None, max_iter=16, threshold=1e-7, coil_axis=-1, verbose=D_VERB_LVL): """ Adaptive coil combination method. Args: arr (np.ndarray): The input array. filtering (callable|None): The filtering function. If callable, it is used to separate the sensitivity from the input. Typically, a low-pass filter is used, under the assumption that the coil sensitivity is smooth compared to the sources. If None, no separation is performed. filtering_kws (Mappable|None): Keyword arguments to pass to `filtering`. max_iter (int): Maximum iterations in power algorithm. This is the maximum number of iterations used for determining the principal component (eigenvalue/vector) using the power algorithm. threshold (float): Threshold in power algorithm. If the next iteration modifies the eigenvalue (in absolute terms) less than the threshold, the power algorithm stops. coil_axis (int): The coil dimension. The dimension of `arr` along which single coil elements are stored. verbose (int): Set level of verbosity. Returns: result (tuple): The tuple contains: - combined (np.ndarray): The combined data. - sens (np.ndarray): The coil sensitivity. References: - Walsh, D.O., Gmitro, A.F., Marcellin, M.W., 2000. Adaptive reconstruction of phased array MR imagery. Magn. Reson. Med. 43, 682–690. doi:10.1002/(SICI)1522-2594( 200005)43:5<682::AID-MRM10>3.0.CO;2-G - Inati, S.J., Hansen, M.S., Kellman, P., 2013. A Solution to the Phase Problem in Adaptive Coil Combination, in: Proceedings of the ISMRM 21st Annual Meeting & Exhibition. Presented at the 21st Annual Meeting & Exhibition of the International Society for Magnetic Resonance in Medicine, ISMRM, Salt Lake City, Utah, USA. """ assert (-arr.ndim <= coil_axis < arr.ndim) shape = arr.shape num_coils = shape[coil_axis] coil_axis = fc.valid_index(coil_axis, arr.ndim) last_axis = -1 % arr.ndim if coil_axis != last_axis: arr = np.swapaxes(arr, coil_axis, last_axis) base_shape = arr.shape[:-1] # calculate the coil covariance coil_cov = np.zeros(base_shape + (num_coils, ) * 2, dtype=complex) for i in range(num_coils): for j in range(num_coils): coil_cov[..., i, j] = arr[..., i] * arr[..., j].conj() if filtering: for i in range(num_coils): for j in range(num_coils): coil_cov[..., i, j] = fcn.filter_cx(coil_cov[..., i, j], filtering, (), filtering_kws) # calculate the principal eigenvector of the coil covariance # using the power method (pointwise through all spatial dimensions) sens = np.zeros(base_shape + (num_coils, ), dtype=complex) for i in itertools.product(*[range(k) for k in base_shape]): ii = tuple(j for j in i if i != slice(None)) + (slice(None), ) iii = tuple(j for j in i if i != slice(None)) + (slice(None), ) * 2 sensitivity_i = np.sum(coil_cov[iii], axis=-1) power_i = np.linalg.norm(sensitivity_i) if power_i: sensitivity_i = sensitivity_i / power_i else: sensitivity_i *= 0 continue for _ in range(max_iter): sensitivity_i = np.dot(coil_cov[iii], sensitivity_i) last_power_i = power_i power_i = np.linalg.norm(sensitivity_i) if power_i: sensitivity_i = sensitivity_i / power_i else: sensitivity_i *= 0 break if np.abs(last_power_i - power_i) < threshold: break sens[ii] = sensitivity_i if coil_axis != last_axis: sens = np.swapaxes(sens, last_axis, coil_axis) combined = combine_sens(arr, sens, coil_axis=coil_axis) return combined, sens
def compress_svd(arr, k_svd='quad_weight', orthonormal=False, coil_axis=-1, verbose=D_VERB_LVL): """ Compress the coil data to the SVD principal components. Rearranges (diagonalize) the acquired single-channel data into virtual single-channel data sorted by eigenvalue magnitude. If the number of SVD components `k_svd` is smaller than the number of coils, this is useful both as a denoise method and for reducing the complexity of the problem and the memory usage. Args: arr (np.ndarray): The input array. k_svd (int|float|str): The number of principal components. See `fc.optimal_num_components()` for more details. orthonormal: Uses the orthonormal approximation. Uses the pseudo-inverse, instead of the hermitian conjugate, to form the signal compression matrix. coil_axis (int): The coil dimension. The dimension of `arr` along which single coil elements are stored. verbose (int): Set level of verbosity. Returns: arr (np.ndarray): The compressed coil array. References: - Buehrer, M., Pruessmann, K.P., Boesiger, P., Kozerke, S., 2007. Array compression for MRI with large coil arrays. Magn. Reson. Med. 57, 1131–1139. doi:10.1002/mrm.21237 """ assert (-arr.ndim <= coil_axis < arr.ndim) shape = arr.shape num_coils = shape[coil_axis] coil_axis = fc.valid_index(coil_axis, arr.ndim) last_axis = -1 % arr.ndim if coil_axis != last_axis: arr = np.swapaxes(arr, coil_axis, last_axis) base_shape = arr.shape[:-1] arr = arr.reshape((-1, num_coils)) if orthonormal: inv_arr = np.linalg.pinv(arr) square_arr = np.zeros((num_coils, num_coils), dtype=complex) for i in range(num_coils): if orthonormal: square_arr[i, :] = np.dot(inv_arr[i, :], arr) else: square_arr[i, :] = np.dot(arr[:, i].conj(), arr) if orthonormal: del inv_arr eigvals, right_eigvects = sp.linalg.eig(square_arr) eig_sort = np.argsort(np.abs(eigvals))[::-1] k_svd = fcn.auto_num_components(k_svd, np.abs(eigvals[eig_sort]) / np.max(np.abs(eigvals)), verbose=verbose) arr = np.dot(arr, right_eigvects[:, eig_sort][:, :k_svd]) arr = arr.reshape(base_shape + (k_svd, )) if coil_axis != last_axis: arr = np.swapaxes(arr, last_axis, coil_axis) return arr