Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Display 3D trajectories, with a grid interface #193

Open
wants to merge 66 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
8ca012c
Added
Apr 26, 2024
3a980c2
Merge branch 'mind-inria:master' into master
chaithyagr Apr 26, 2024
093edfb
Fix
Apr 26, 2024
f8a6a4a
Merge branch 'master' of github.com:chaithyagr/mri-nufft
Apr 26, 2024
74c1ecd
Remove bymistake add
Apr 26, 2024
0250aa8
Fix
Apr 26, 2024
060a8bd
Fixed lint
Apr 26, 2024
aecb844
Lint
Apr 26, 2024
3130bc1
Added refbackend
Apr 26, 2024
bc014b8
Fix NDFT
Apr 26, 2024
0cc73c4
feat: use finufft as ref backend.
paquiteau Apr 29, 2024
21e090f
feat(tests): move ndft vs nufft tests to own file.
paquiteau Apr 29, 2024
6869a4a
Merge branch 'master' of github.com:mind-inria/mri-nufft
Apr 29, 2024
f8364d4
Merge branch 'master' of github.com:mind-inria/mri-nufft
Apr 30, 2024
23a63da
Merge branch 'master' of github.com:mind-inria/mri-nufft
Jun 17, 2024
3709e74
Merge branch 'master' of github.com:mind-inria/mri-nufft
Jul 1, 2024
8d6b9b4
Merge branch 'master' of github.com:chaithyagr/mri-nufft
chaithyagr Aug 1, 2024
96a8287
fix typoes (off_resonnance -> off_resonance)
mcencini Aug 8, 2024
f8d5dc8
feat: add field interpolators calculation (#124)
mcencini Aug 8, 2024
855c99d
Merge branch 'mind-inria:master' into off_resonance
mcencini Aug 8, 2024
6364186
feat: field interpolator estimation in MRIFourierCorrected constructor.
mcencini Aug 9, 2024
55eea10
Merge branch 'off_resonance' of github.com:mcencini/mri-nufft into of…
mcencini Aug 9, 2024
36563b5
feat: set autograd_available based on base FourierOperator
mcencini Aug 9, 2024
2b03a36
Merge branch 'mind-inria:master' into off_resonance
mcencini Aug 9, 2024
39105c6
Update test_offres_exp_approx.py
mcencini Aug 9, 2024
0b9666e
Merge branch 'mind-inria:master' into off_resonance
mcencini Aug 30, 2024
f3ad9ac
Update src/mrinufft/operators/off_resonance.py
mcencini Aug 30, 2024
f4b9108
address pr review
mcencini Aug 30, 2024
45ebb00
remove phantominator
mcencini Aug 30, 2024
308a27b
remove duplicated example and replacing subject in example_offres
mcencini Sep 2, 2024
f6a5cb7
address PR rev #2
mcencini Sep 2, 2024
f5b7230
Merge branch 'master' into off_resonance
chaithyagr Sep 3, 2024
ed44862
\!docs_build and fix on \!style
chaithyagr Sep 3, 2024
70c2cf8
\!docs_build
chaithyagr Sep 3, 2024
45a9364
skip field coefficient gpu test case if cupy not available
mcencini Sep 3, 2024
1bbe2c3
\!docs_build fix gpuNUFFT
chaithyagr Sep 3, 2024
398fb28
Merge branch 'master' of github.com:mind-inria/mri-nufft
chaithyagr Sep 5, 2024
216b7ad
Update src/mrinufft/operators/off_resonance.py
mcencini Sep 5, 2024
0ac0aa0
Update src/mrinufft/operators/off_resonance.py
mcencini Sep 5, 2024
9b04b51
\!docs_build gpunufft
chaithyagr Sep 5, 2024
60ed09c
Merge branch 'off_resonance' of github.com:mcencini/mri-nufft into pr…
chaithyagr Sep 5, 2024
a35fc35
Update src/mrinufft/operators/off_resonance.py
mcencini Sep 5, 2024
327cb03
Merge branch 'off_resonance' of github.com:mcencini/mri-nufft into pr…
chaithyagr Sep 5, 2024
f3ed9c1
Update src/mrinufft/operators/off_resonance.py
mcencini Sep 5, 2024
c57f3b8
address PR rev 3
mcencini Sep 5, 2024
e46095d
Update pyproject.toml
chaithyagr Sep 6, 2024
c944e78
Merge branch 'master' into off_resonance
chaithyagr Sep 9, 2024
5f32210
Update src/mrinufft/operators/off_resonance.py
mcencini Sep 12, 2024
211a6aa
\![docs_build]
chaithyagr Sep 13, 2024
fe05e31
Merge branch 'master' into display_dens
chaithyagr Sep 13, 2024
abd5970
Add 3D trajectory viewer
chaithyagr Sep 13, 2024
a97be05
Add supoport for grads and slew
chaithyagr Sep 16, 2024
cb076bd
Merge branch 'master' into display_dens
chaithyagr Sep 17, 2024
165043f
Merge branch 'master' into display_dens
chaithyagr Sep 27, 2024
2aff0f4
Fix ruff
chaithyagr Sep 27, 2024
0280cb0
Merge branch 'master' into display_dens
chaithyagr Oct 7, 2024
5ca6aec
Merge branch 'master' into display_dens
chaithyagr Nov 8, 2024
b6598a3
added example
chaithyagr Nov 8, 2024
dec680b
\!docs_build finalizing
chaithyagr Nov 8, 2024
2754158
\!docs_build added new example
chaithyagr Nov 13, 2024
96d9850
Apply suggestions from code review
chaithyagr Nov 25, 2024
2594b41
Handle a bunch of comments
chaithyagr Nov 26, 2024
4a07125
Examples
chaithyagr Nov 26, 2024
dad26ce
PEP
chaithyagr Nov 26, 2024
98cb154
PEP
chaithyagr Nov 26, 2024
34555cf
Merge branch 'master' into display_dens
chaithyagr Dec 3, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 111 additions & 0 deletions examples/GPU/example_3d_trajectory_display.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
"""
======================
3D Trajectory display
======================

In this example, we show some tools available to display 3D trajectories.
It can be used to understand the k-space sampling patterns, visualize the trajectories, see the sampling times, gradient strengths, slew rates etc.
Another key feature is to display the sampling density in k-space, for example to check for k-space holes or irregularities in the learning-based trajectories that would lead to artifacts in the images.
"""

# %%
# Imports
from mrinufft.trajectories.display3D import get_gridded_trajectory
import mrinufft.trajectories.trajectory3D as mtt
from mrinufft.trajectories.utils import Gammas
import matplotlib.pyplot as plt
from matplotlib import gridspec
import numpy as np


# %%
# Utility function to plot mid-plane slices for 3D volumes
def plot_slices(axs, volume, title=""):
def set_labels(ax, axis_num=None):
ax.set_xticks([0, 32, 64])
ax.set_yticks([0, 32, 64])
ax.set_xticklabels([r"$-\pi$", 0, r"$\pi$"])
ax.set_yticklabels([r"$-\pi$", 0, r"$\pi$"])
if axis_num is not None:
ax.set_xlabel(r"$k_" + "zxy"[axis_num] + r"$")
ax.set_ylabel(r"$k_" + "yzx"[axis_num] + r"$")

for i in range(3):
volume = np.rollaxis(volume, i, 0)
axs[i].imshow(volume[volume.shape[0] // 2])
axs[i].set_title(
((title + f"\n") if i == 0 else "") + r"$k_{" + "xyz"[i] + r"}=0$"
)
set_labels(axs[i], i)


def create_grid(grid_type, trajectories, **kwargs):
fig, axs = plt.subplots(3, 3, figsize=(10, 10))
for i, (name, traj) in enumerate(trajectories.items()):
grid = get_gridded_trajectory(
traj,
traj_params["img_size"],
grid_type=grid_type,
traj_params=traj_params,
**kwargs,
)
plot_slices(axs[:, i], grid, title=name)
plt.tight_layout()


# %%
# Create a bunch of sample trajectories
trajectories = {
"Radial": mtt.initialize_3D_phyllotaxis_radial(64 * 8, 64),
"FLORETs": mtt.initialize_3D_floret(64 * 8, 64, nb_revolutions=2),
"Seiffert Spirals": mtt.initialize_3D_seiffert_spiral(64 * 8, 64),
}
traj_params = {
"FOV": (0.23, 0.23, 0.23),
"img_size": (64, 64, 64),
"gamma": Gammas.HYDROGEN,
}

# %%
# Display the density of the trajectories, along the 3 mid-planes. For this, make `grid_type="density"`.
create_grid("density", trajectories)
plt.suptitle("Sampling Density")
plt.show()


# %%
# Display the sampling times over the trajectories. For this, make `grid_type="time"`.
# It helps to check the sampling times over the k-space trajectories, which can be responsible for excessive off-resonance artifacts.
create_grid("time", trajectories)
plt.suptitle("Sampling Time")
plt.show()

# %%
# Display the inversion time of the trajectories. For this, make `grid_type="inversion"`.
# This helps in obtaining the inversion time when particular region of k-space is sampled, assuming the trajectories are time ordered.
# This helps understand any issues for imaging involving inversion recovery.
# The argument `turbo_factor` can be used to tell what is the number of echoes between 2 inversion pulses.
create_grid("inversion", trajectories, turbo_factor=64)
plt.suptitle("Inversion Time")
plt.show()
# %%
# Display the k-space holes in the trajectories. For this, make `grid_type="holes"`.
# This helps in understanding the k-space holes, and can help debug artifacts in reconstructions.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can help debug artifacts in reconstructions.

The reconstruction process is unrelated. As suggested above, I would just talk of the images directly instead of "reconstructions" to designate them

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This helps in understanding the k-space holes, and can help debug artifacts in reconstructions.

This sentence alone is quite cryptic for people not familiar with that problem, which is mostly specific to optimization-based trajectories. Could you tell a bit more about the use case ?

create_grid("holes", trajectories, threshold=1e-2)
plt.suptitle("K-space Holes")
plt.show()
# %%
# Display the gradient strength of the trajectories. For this, make `grid_type="gradients"`.
# This helps in understanding the gradient strength applied at specific k-space region.
# This can also be used as a surrogate to k-space "velocity", i.e. how fast does trajectory pass through a given region in k-space
create_grid("gradients", trajectories)
plt.suptitle("Gradient Strength")
plt.show()

# %%
# Display the slew rates of the trajectories. For this, make `grid_type="slew"`.
# This helps in understanding the slew rates applied at specific k-space region.
# This can also be used as a surrogate to k-space "acceleration", i.e. how fast does trajectory change in a given region in k-space
create_grid("slew", trajectories)
plt.suptitle("Slew Rates")
plt.show()
140 changes: 140 additions & 0 deletions src/mrinufft/trajectories/display3D.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
"""Utils for displaying 3D trajectories."""

from mrinufft import get_operator, get_density
from mrinufft.trajectories.utils import (
convert_trajectory_to_gradients,
convert_gradients_to_slew_rates,
KMAX,
DEFAULT_RASTER_TIME,
)
import numpy as np
from typing import Tuple
from mrinufft.io import read_trajectory


def get_gridded_trajectory(
shots: np.ndarray,
shape: Tuple,
grid_type: str = "density",
osf: int = 1,
backend: str = "gpunufft",
traj_params: dict = None,
turbo_factor: int = 176,
elliptical_samp: bool = True,
threshold: float = 1e-3,
):
Comment on lines +15 to +25
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall, why have all of those features gathered into only one function ? Some of them are not related to grids.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also please add more comments to your code

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think, all the features are related to gridding and how you want to "color" the trajectories on the grid.

"""
Compute the gridded trajectory for MRI reconstruction.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Compute the gridded trajectory for MRI reconstruction.

The use case it up to the user. Could you instead detail a bit what gridding means here ?


This function helps in gridding a k-space sampling trajectory to a desired shape.
The gridding process can be carried out to reflect the sampling density,
sampling time, inversion time, k-space holes, gradient strengths, or slew rates.
Please check `grid_type` parameter to know the benefits of each type of gridding.

Parameters
----------
shots : ndarray
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The shots name isn't conventional in that part of the package

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What would you recommend? traj ?

The input array of shape (N, M, D), where N is the number of shots and M is the
number of samples per shot and D is the dimension of the trajectory (usually 3)
shape : tuple
The desired shape of the gridded trajectory.
grid_type : str, optional
The type of gridded trajectory to compute. Default is "density".
It can be one of the following:
"density" : Get the sampling density in closest number of samples per voxel.
Helps understand suboptimal sampling, by showcasing regions with strong
oversampling.
"time" : Showcases when the k-space data is acquired in time.
This is helpful to view and understand off-resonance effects.
Generally, lower off-resonance effects occur when the sampling trajectory
has smoother k-space sampling time over the k-space.
"inversion" : Relative inversion time at the sampling location. Needs
turbo_factor to be set. This is useful for analyzing the exact inversion
time when the k-space is acquired, for sequences like MP(2)RAGE.
"holes": Show the k-space holes within a ellipsoid of the k-space.
"gradients": Show the gradient strengths of the k-space trajectory.
"slew": Show the slew rate of the k-space trajectory.
osf : int, optional
The oversampling factor for the gridded trajectory. Default is 1.
backend : str, optional
The backend to use for gridding. Default is "gpunufft".
Note that "gpunufft" is anyway used to get the `pipe` density internally.
traj_params : dict, optional
The trajectory parameters. Default is None.
This is only needed when `grid_type` is "gradients" or "slew".
Comment on lines +62 to +64
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just explicit the parameters ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the problem is we need to give img_size, FOV and gamma. I will add these parameters are in header.

The parameters needed include `img_size`, `FOV`, and `gamma` of the sequence.
Generally these values are stored in the header of the trajectory file.
turbo_factor : int, optional
The turbo factor when sampling is with inversion. Default is 176, which is
the default turbo factor for MPRAGE acquisitions at 1mm whole
brain acquisitions.
elliptical_samp : bool, optional
Whether to use elliptical sampling. Default is True.
This is useful while analyzing the k-space holes, especially if the k-space
trajectory is expected to be elliptical sampling of k-space
(i.e. ellipsoid over cuboid).
threshold: float, optional
The threshold for the k-space holes. Default is 1e-3.
Comment on lines +76 to +77
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please explicit the threshold unit, and what the value corresponds to/what it should be compared to

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sadly, theres no unit, its set heuristically :P

This value is set heuristically to visualize the k-space hole.

Returns
-------
ndarray
The gridded trajectory of shape `shape`.
"""
samples = shots.reshape(-1, shots.shape[-1])
dcomp = get_density("pipe")(samples, shape)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why assume the density type ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we currently have only one way to get density, which is fast.

grid_op = get_operator(backend)(
samples, [sh * osf for sh in shape], density=dcomp, upsampfac=1
)
gridded_ones = grid_op.raw_op.adj_op(np.ones(samples.shape[0]), None, True)
if grid_type == "density":
return np.abs(gridded_ones).squeeze()
elif grid_type == "time":
data = grid_op.raw_op.adj_op(
np.tile(np.linspace(1, 10, shots.shape[1]), (shots.shape[0],)),
None,
True,
)
Comment on lines +94 to +98
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't we want to provide actual time values ? It should be more explicit in the documentation

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also the output shape doesn't correspond to shape as expected when reading the documentation. I don't understand why this is part of that function since it doesn't relate to a grid

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why 1 to 10 ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is just relative to get an understanding. Given that adjoint is not inverse, even if we give exact values, we cant expect it to be reflected the same way on image domain.
The shape would still be same as expected as we use grid_op which is set based on shape, right? Am I missing something?

elif grid_type == "inversion":
data = grid_op.raw_op.adj_op(
np.repeat(
np.linspace(1, 10, turbo_factor), samples.shape[0] // turbo_factor + 1
)[: samples.shape[0]],
None,
True,
)
Comment on lines +100 to +106
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same problem here, the output shape doesn't seem to match the documented output

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above, isnt data.shape same as shape we give as input?

elif grid_type == "holes":
data = np.abs(gridded_ones).squeeze() < threshold
if elliptical_samp:
# If the trajectory uses elliptical sampling, ignore the k-space holes
# outside the ellipsoid.
data[
np.linalg.norm(
np.meshgrid(
*[np.linspace(-1, 1, sh) for sh in shape], indexing="ij"
),
axis=0,
)
> 1
] = 0
elif grid_type in ["gradients", "slew"]:
gradients, initial_position = convert_trajectory_to_gradients(
shots,
norm_factor=KMAX,
resolution=np.asarray(traj_params["FOV"])
/ np.asarray(traj_params["img_size"]),
raster_time=DEFAULT_RASTER_TIME,
gamma=traj_params["gamma"],
)
if grid_type == "gradients":
data = np.hstack(
[gradients, np.zeros((gradients.shape[0], 1, gradients.shape[2]))]
)
else:
slews, _ = convert_gradients_to_slew_rates(gradients, DEFAULT_RASTER_TIME)
data = np.hstack([slews, np.zeros((slews.shape[0], 2, slews.shape[2]))])
data = grid_op.raw_op.adj_op(
np.linalg.norm(data, axis=-1).flatten(), None, True
)
return np.squeeze(np.abs(data))
Loading