8. Non-Cartesian 3D under-sampling

8. Non-Cartesian 3D under-sampling#

In this notebook, you can play with the design parameters to regenerate different 3D sampling in-out patterns (so, we draw as many spiral arches as the number of shots). You can play with the number of shots by changing the under-sampling factor.

  • Author: Philippe Ciuciu (philippe.ciuciu@cea.fr) & Pierre-Antoine Comby (pierre-antoine.comby@ens-paris-saclay.fr)

  • Date: 01/30/2024 (dependence on MRI-NUFFT for 3D trajectories and NUFFT operators with density compensation)

  • Date: 04/02/2025 support GPUNUFFT with Pipe’s method for density compensation

  • Target: ATSI MSc students at Paris-Saclay University

import brainweb_dl as bwdl
import numpy as np
import matplotlib.pyplot as plt

import mrinufft
from mrinufft import display_2D_trajectory, display_3D_trajectory
from mrinufft import get_density, get_operator, check_backend
/volatile/github-ci-mind-inria/gpu_mind_runner/_work/mri-acq-recon-book/mri-acq-recon-book/venv/lib/python3.10/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html
  from .autonotebook import tqdm as notebook_tqdm
def _fit_grid(n_tiles):
    """Give the number of row and columns to optimally fit n_tiles."""
    n_rows = int(np.sqrt(n_tiles))
    n_cols = n_rows
    while n_rows * n_cols < n_tiles:
        if n_rows < n_cols:
            n_rows += 1
        else:
            n_cols += 1
    return n_rows, n_cols

def tile_view(
    array,
    axis=-1,
    samples=-1,
    n_rows=-1,
    n_cols=-1,
    img_w=3,
    fig=None,
    cmap="gray",
    axis_label=None,
):
    """Plot a 3D array as a tiled grid of 2D images.

    Parameters
    ----------
    array: numpy.ndarray
        A 3D array
    axis: int
        the axis on which to performs the sampling
    samples: int or float, default: -1
        If int, represent the number of equaly sample to take along axis.
        If float, must be in (0, 1], and represent the sampling rate.
    n_rows int, default=-1
    n_cols int, default=-1
    img_w: int
        size in inches of a sample tile.
    fig: Figure handle, default None
        If None, a new figure is created.
    cmap: matplotlib colormap.
        color map to use.

    Returns
    -------
    matplotlib.pyplot.figure
        The figure handle

    Raises
    ------
    ValueError
        If array is not 3D
    ValueError
        If both n_rows and n_cols are specified and cannot fit the samples.
    """
    if array.ndim != 3:
        raise ValueError("Only 3D array are supported.")
    if axis < 0:
        axis = 3 + axis

    slicer = [slice(None), slice(None), slice(None)]
    axis_label = axis_label or ["t", "x", "y", "z"][axis]

    if samples == -1:
        samples_loc = np.arange(array.shape[axis])
        step = 1
    elif isinstance(samples, int):
        step = array.shape[axis] // (samples + 1)

        samples_loc = np.arange(1, samples + 1) * step
    elif isinstance(samples, float) and (0 < samples <= 1):
        n_samples = int(array.shape[axis] * samples)
        step = array.shape[axis] // n_samples
        samples_loc = np.arange(0, n_samples) * step
    else:
        raise ValueError("Unsupported value for sample")
    n_samples = len(samples_loc)
    array_list = [array[(*slicer[:axis], s, *slicer[axis + 1 :])] for s in samples_loc]

    if n_rows == -1 and n_cols != -1:
        while n_rows * n_cols < n_samples:
            n_rows += 1
    elif n_rows != -1 and n_cols == -1:
        while n_rows * n_cols < n_samples:
            n_cols += 1
    elif n_rows == -1 and n_cols == -1:
        n_rows, n_cols = _fit_grid(n_samples)
    elif n_rows != -1 and n_cols != -1 and n_rows * n_cols < n_samples:
        raise ValueError("The grid is not big enough to fit all samples.")

    aspect_ratio = array_list[0].shape[0] / array_list[0].shape[1]

    fig = plt.figure(num=fig, figsize=(n_cols * img_w, n_rows * img_w * aspect_ratio))
    gs = fig.add_gridspec(
        n_rows,
        n_cols,
        hspace=0.01,
        wspace=0.01,
    )
    axs_2d = gs.subplots(squeeze=False)

    axs = axs_2d.flatten()
    for i, img in enumerate(array_list):
        ax = axs[i]
        ax.axis("off")
        if np.any(np.iscomplex(img)):
            ax.imshow(abs(img), cmap=cmap, origin="lower")
        else:
            ax.imshow(img, cmap=cmap, origin="lower")
        ax.text(
            0.05,
            0.95,
            f"{axis_label}={step*i}",
            ha="left",
            va="top",
            transform=ax.transAxes,
            bbox=dict(boxstyle="square, pad=0", fc=fig.get_facecolor(), ec="none"),
        )
    # remove unused axis
    for ax in axs[len(array_list) :]:
        ax.remove()
    return fig
mri_3D = bwdl.get_mri(4, "T1").astype(np.float32)
print(mri_3D.shape)

# Axial views
#tile_view(mri_3D, samples=0.1, axis=0)
# Coronal views
#tile_view(mri_3D, samples=0.1, axis=1)
# Sagittal views
tile_view(mri_3D, samples=0.05, axis=-1)
(181, 256, 256)
_images/18c1b3fc2bda03969ee249edf488c50d9b9a2968c4fe6a2a4ef03c2e27089223.png _images/18c1b3fc2bda03969ee249edf488c50d9b9a2968c4fe6a2a4ef03c2e27089223.png
def show_trajectory(trajectory, one_shot, figure_size):
    ax = display_3D_trajectory(
        trajectory, size=figure_size, one_shot=one_shot, per_plane=False
    )
    plt.tight_layout()
    plt.subplots_adjust(bottom=0.1)
    plt.show()
from mrinufft.trajectories import initialize_3D_cones

traj = initialize_3D_cones(256, 256).astype(np.float32)

show_trajectory(traj, 20, (15, 5))

# call to the ninufft operator without density compensation
#nufft = get_operator("finufft")(traj, mri_3D.shape, density=False)
# call to the gpunufft backend with density compensatioon based on Pipe's method
nufft = get_operator("gpunufft")(traj, mri_3D.shape, density=True)

kspace = nufft.op(mri_3D)
adjoint_3Dcones = nufft.adj_op(kspace)
tile_view(adjoint_3Dcones, samples=0.05, axis=-1)

#cell_weights = get_density("cell_count", traj, shape=mri_3D.shape, osf=2.0)
#nufft_cell_counting = get_operator("finufft")(
#    traj, shape=mri_3D.shape, density=cell_weights, upsampfac=2.0
#)
#dc_adjoint_3Dcones = nufft_cell_counting.adj_op(kspace)
#tile_view(dc_adjoint_3Dcones, samples=0.05, axis=-1)
_images/daef08e12aac675aef63e8bb023f724e985b370ffec31badc291efc1fe4eedd5.png
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[5], line 10
      5 show_trajectory(traj, 20, (15, 5))
      7 # call to the ninufft operator without density compensation
      8 #nufft = get_operator("finufft")(traj, mri_3D.shape, density=False)
      9 # call to the gpunufft backend with density compensatioon based on Pipe's method
---> 10 nufft = get_operator("gpunufft")(traj, mri_3D.shape, density=True)
     12 kspace = nufft.op(mri_3D)
     13 adjoint_3Dcones = nufft.adj_op(kspace)

File ~/gpu_mind_runner/_work/mri-acq-recon-book/mri-acq-recon-book/venv/lib/python3.10/site-packages/mrinufft/operators/interfaces/gpunufft.py:422, in MRIGpuNUFFT.__init__(self, samples, shape, n_coils, n_batchs, n_trans, density, smaps, squeeze_dims, eps, **kwargs)
    420 self.n_batchs = n_batchs
    421 self.squeeze_dims = squeeze_dims
--> 422 self.compute_density(density)
    423 self.compute_smaps(smaps)
    424 self.raw_op = RawGpuNUFFT(
    425     samples=self.samples,
    426     shape=self.shape,
   (...)
    431     **kwargs,
    432 )

File ~/gpu_mind_runner/_work/mri-acq-recon-book/mri-acq-recon-book/venv/lib/python3.10/site-packages/mrinufft/operators/base.py:332, in FourierOperatorBase.compute_density(self, method)
    326 if self._density_method is None:
    327     self._density_method = lambda samples, shape: method(
    328         samples,
    329         shape,
    330         **kwargs,
    331     )
--> 332 self._density = method(self.samples, self.shape, **kwargs)

File ~/gpu_mind_runner/_work/mri-acq-recon-book/mri-acq-recon-book/venv/lib/python3.10/site-packages/mrinufft/density/utils.py:35, in flat_traj.<locals>.decorator.<locals>.wrapper(*args, **kwargs)
     33 args = list(args)
     34 args[0] = proper_trajectory(args[0], normalize=normalize)
---> 35 return func(*args, **kwargs)

File ~/gpu_mind_runner/_work/mri-acq-recon-book/mri-acq-recon-book/venv/lib/python3.10/site-packages/mrinufft/density/nufft_based.py:27, in pipe(traj, shape, backend, **kwargs)
     25 nufft_class = get_operator(backend)
     26 if hasattr(nufft_class, "pipe"):
---> 27     return nufft_class.pipe(traj, shape, **kwargs)
     28 raise ValueError("backend does not have pipe iterations method.")

File ~/gpu_mind_runner/_work/mri-acq-recon-book/mri-acq-recon-book/venv/lib/python3.10/site-packages/mrinufft/operators/interfaces/gpunufft.py:612, in MRIGpuNUFFT.pipe(cls, kspace_loc, volume_shape, num_iterations, osf, normalize, **kwargs)
    610     test_op = MRIGpuNUFFT(samples=kspace_loc, shape=original_shape, **kwargs)
    611     test_im = np.ones(original_shape, dtype=np.complex64)
--> 612     test_im_recon = test_op.adj_op(density_comp * test_op.op(test_im))
    613     density_comp /= np.mean(np.abs(test_im_recon))
    614 return density_comp.squeeze()

File ~/gpu_mind_runner/_work/mri-acq-recon-book/mri-acq-recon-book/venv/lib/python3.10/site-packages/mrinufft/_array_compat.py:103, in with_numpy_cupy.<locals>.wrapper(*args, **kwargs)
    100 xp = get_array_module(leading_arg)
    102 # convert all to cupy / numpy according to data arg device
--> 103 args, kwargs = _to_numpy_cupy(args, kwargs, leading_arg)
    105 # run function
    106 ret_ = fun(*args, **kwargs)

File ~/gpu_mind_runner/_work/mri-acq-recon-book/mri-acq-recon-book/venv/lib/python3.10/site-packages/mrinufft/_array_compat.py:251, in _to_numpy_cupy(args, kwargs, leading_argument)
    249 if is_cuda_array(leading_argument) and CUPY_AVAILABLE:
    250     return _to_cupy(*args, **kwargs)
--> 251 elif is_cuda_tensor(leading_argument) and CUPY_AVAILABLE:
    252     return _to_cupy(*args, **kwargs)
    253 else:

File ~/gpu_mind_runner/_work/mri-acq-recon-book/mri-acq-recon-book/venv/lib/python3.10/site-packages/mrinufft/operators/interfaces/utils/gpu_utils.py:39, in is_cuda_tensor(var)
     37 def is_cuda_tensor(var):
     38     """Check if var is a CUDA tensor."""
---> 39     return isinstance(var, torch.Tensor) and var.is_cuda

NameError: name 'torch' is not defined
from mrinufft.trajectories import initialize_3D_floret

traj = initialize_3D_floret(256, 256).astype(np.float32)
show_trajectory(traj, 10, (15, 5))

# call to the ninufft operator without density compensation
#nufft = get_operator("finufft")(traj, mri_3D.shape, density=False)
# call to the gpunufft backend with density compensatioon based on Pipe's method
nufft = get_operator("gpunufft")(traj, mri_3D.shape, density=True)

kspace = nufft.op(mri_3D)
adjoint_3Dfloret = nufft.adj_op(kspace)
tile_view(adjoint_3Dfloret, samples=0.05, axis=-1)

#cell_weights = get_density("cell_count", traj, shape=mri_3D.shape, osf=2.0)
#nufft_cell_counting = get_operator("finufft")(
#    traj, shape=mri_3D.shape, density=cell_weights, upsampfac=2.0
#)
#dc_adjoint_3Dfloret = nufft_cell_counting.adj_op(kspace)
#tile_view(dc_adjoint_3Dfloret, samples=0.05, axis=-1)
_images/d717202a480fab5b38ce7813196eb711bc01ce4c6f397807aa628a19d5dfb406.png _images/6c381d4521b6f888cf993aaab42aad541afcf0f090906811cff3e420518f2b73.png _images/6c381d4521b6f888cf993aaab42aad541afcf0f090906811cff3e420518f2b73.png
from mrinufft.trajectories import initialize_3D_wave_caipi

traj = initialize_3D_wave_caipi(256, 256).astype(np.float32)
show_trajectory(traj, 10, (15, 5))

# call to the ninufft operator without density compensation
#nufft = get_operator("finufft")(traj, mri_3D.shape, density=False)
# call to the gpunufft backend with density compensatioon based on Pipe's method
nufft = get_operator("gpunufft")(traj, mri_3D.shape, density=True)

kspace = nufft.op(mri_3D)
adjoint_3Dwave_caipi = nufft.adj_op(kspace)
tile_view(adjoint_3Dwave_caipi, samples=0.05, axis=-1)

#voronoi_weights = get_density("voronoi", traj)
#nufft3D_voronoi = get_operator("finufft")(
#    traj, shape=mri_3D.shape, density=voronoi_weights
#)
#dc_adjoint_3Dwave_caipi = nufft3D_voronoi.adj_op(kspace)

#cell_weights = get_density("cell_count", traj, shape=mri_3D.shape, osf=2.0)
##nufft_cell_counting = get_operator("finufft")(
#    traj, shape=mri_3D.shape, density=cell_weights, upsampfac=2.0
#)
#dc_adjoint_3Dwave_caipi = nufft_cell_counting.adj_op(kspace)
#tile_view(dc_adjoint_3Dwave_caipi, samples=0.05, axis=-1)
_images/2b479f42a45d0d48fb7b2497e7d8ff2e6e7c5bd9a5677b302084c4b21b991f3e.png _images/0bb8ae0f84fd6573fedbb7cf98cad46c0f7dce7593dd4a2f0078b77bdbd4bd29.png _images/0bb8ae0f84fd6573fedbb7cf98cad46c0f7dce7593dd4a2f0078b77bdbd4bd29.png
from mrinufft.trajectories import initialize_3D_seiffert_spiral

traj = initialize_3D_seiffert_spiral(256, 256).astype(np.float32)
show_trajectory(traj, 10, (15, 5))

# call to the ninufft operator without density compensation
#nufft = get_operator("finufft")(traj, mri_3D.shape, density=False)
# call to the gpunufft backend with density compensatioon based on Pipe's method
nufft = get_operator("gpunufft")(traj, mri_3D.shape, density=True)

kspace = nufft.op(mri_3D)
adjoint_3D_seiffert_spiral = nufft.adj_op(kspace)
tile_view(adjoint_3D_seiffert_spiral, samples=0.05, axis=-1)
_images/9b4b27b8f32c2afd7ec69137b75382401c5d1bb31afe2ba63c03edeb85ac7455.png _images/82d0cf26f86b2b9b4452e5036006da665983c6c03142788f2431c5dd896a604e.png _images/82d0cf26f86b2b9b4452e5036006da665983c6c03142788f2431c5dd896a604e.png