-
Notifications
You must be signed in to change notification settings - Fork 13
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
Dispatch density methods in operators #68
Changes from all commits
1deff3b
0d18a8b
a468f39
a665083
3eacdbd
9340174
630c83b
7ca7bfe
74e0bcf
66dd07f
ddf26d2
0e5d902
7c083bb
63e5d1a
ffe7a70
3c6fb18
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,150 @@ | ||
# %% | ||
""" | ||
============================= | ||
Density Compensation Routines | ||
============================= | ||
|
||
Examples of differents density compensation methods. | ||
|
||
Density compensation depends on the sampling trajectory,and is apply before the | ||
adjoint operation to act as preconditioner, and should make the lipschitz constant | ||
of the operator roughly equal to 1. | ||
|
||
""" | ||
import brainweb_dl as bwdl | ||
|
||
import matplotlib.pyplot as plt | ||
import numpy as np | ||
from mrinufft import get_density, get_operator, check_backend | ||
from mrinufft.trajectories import initialize_2D_radial | ||
from mrinufft.trajectories.display import display_2D_trajectory | ||
|
||
|
||
# %% | ||
# Create sample data | ||
# ------------------ | ||
|
||
mri_2D = bwdl.get_mri(4, "T1")[80, ...].astype(np.float32) | ||
|
||
print(mri_2D.shape) | ||
|
||
traj = initialize_2D_radial(192, 192).astype(np.float32) | ||
|
||
nufft = get_operator("finufft")(traj, mri_2D.shape, density=False) | ||
kspace = nufft.op(mri_2D) | ||
adjoint = nufft.adj_op(kspace) | ||
|
||
fig, axs = plt.subplots(1, 3, figsize=(15, 5)) | ||
axs[0].imshow(abs(mri_2D)) | ||
display_2D_trajectory(traj, subfigure=axs[1]) | ||
axs[2].imshow(abs(adjoint)) | ||
|
||
# %% | ||
# As you can see, the radial sampling pattern as a strong concentration of sampling point in the center, resulting in a low-frequency biased adjoint reconstruction. | ||
|
||
# %% | ||
# Geometry based methods | ||
# ====================== | ||
# | ||
# Voronoi | ||
# ------- | ||
# | ||
# Voronoi Parcellation attribute a weights to each k-space coordinate, inversely | ||
# proportional to its voronoi cell area. | ||
|
||
|
||
# .. warning:: | ||
# The current implementation of voronoi parcellation is CPU only, and is thus | ||
# **very** slow in 3D ( > 1h). | ||
|
||
# %% | ||
voronoi_weights = get_density("voronoi", traj) | ||
|
||
nufft_voronoi = get_operator("finufft")( | ||
traj, shape=mri_2D.shape, density=voronoi_weights | ||
) | ||
adjoint_voronoi = nufft_voronoi.adj_op(kspace) | ||
fig, axs = plt.subplots(1, 3, figsize=(15, 5)) | ||
axs[0].imshow(abs(mri_2D)) | ||
axs[0].set_title("Ground Truth") | ||
axs[1].imshow(abs(adjoint)) | ||
axs[1].set_title("no density compensation") | ||
axs[2].imshow(abs(adjoint_voronoi)) | ||
axs[2].set_title("Voronoi density compensation") | ||
|
||
|
||
# %% | ||
# Cell Counting | ||
# ------------- | ||
# | ||
# Cell Counting attributes weights based on the number of trajectory point lying in a same k-space nyquist voxel. | ||
# This can be viewed as an approximation to the voronoi neth | ||
|
||
# .. note:: | ||
# Cell counting is faster than voronoi (especially in 3D), but is less precise. | ||
|
||
# The size of the niquist voxel can be tweak by using the osf parameter. Typically as the NUFFT (and by default in MRI-NUFFT) is performed at an OSF of 2 | ||
|
||
|
||
# %% | ||
cell_count_weights = get_density("cell_count", traj, shape=mri_2D.shape, osf=2.0) | ||
|
||
nufft_cell_count = get_operator("finufft")( | ||
traj, shape=mri_2D.shape, density=cell_count_weights, upsampfac=2.0 | ||
) | ||
adjoint_cell_count = nufft_cell_count.adj_op(kspace) | ||
fig, axs = plt.subplots(1, 3, figsize=(15, 5)) | ||
axs[0].imshow(abs(mri_2D)) | ||
axs[0].set_title("Ground Truth") | ||
axs[1].imshow(abs(adjoint)) | ||
axs[1].set_title("no density compensation") | ||
axs[2].imshow(abs(adjoint_cell_count)) | ||
axs[2].set_title("cell_count density compensation") | ||
|
||
# %% | ||
# Manual Density Estimation | ||
# ------------------------- | ||
# | ||
# For some analytical trajectory it is also possible to determine the density compensation vector directly. | ||
# In radial trajectory for instance, a sample's weight can be determined from its distance to the center. | ||
|
||
|
||
# %% | ||
flat_traj = traj.reshape(-1, 2) | ||
weights = np.sqrt(np.sum(flat_traj**2, axis=1)) | ||
nufft = get_operator("finufft")(traj, shape=mri_2D.shape, density=weights) | ||
adjoint_manual = nufft.adj_op(kspace) | ||
fig, axs = plt.subplots(1, 3, figsize=(15, 5)) | ||
axs[0].imshow(abs(mri_2D)) | ||
axs[0].set_title("Ground Truth") | ||
axs[1].imshow(abs(adjoint)) | ||
axs[1].set_title("no density compensation") | ||
axs[2].imshow(abs(adjoint_manual)) | ||
axs[2].set_title("manual density compensation") | ||
|
||
# %% | ||
# Operator-based method | ||
# ===================== | ||
# | ||
# Pipe's Method | ||
# ------------- | ||
# Pipe's method is an iterative scheme, that use the interpolation and spreading kernel operator for computing the density compensation. | ||
# | ||
# .. warning:: | ||
# If this method is widely used in the literature, there exists no convergence guarantees for it. | ||
|
||
# .. note:: | ||
# The Pipe method is currently only implemented for gpuNUFFT. | ||
|
||
# %% | ||
if check_backend("gpunufft"): | ||
flat_traj = traj.reshape(-1, 2) | ||
nufft = get_operator("gpunufft")(traj, shape=mri_2D.shape, density=False) | ||
adjoint_manual = nufft.adj_op(kspace) | ||
fig, axs = plt.subplots(1, 3, figsize=(15, 5)) | ||
axs[0].imshow(abs(mri_2D)) | ||
axs[0].set_title("Ground Truth") | ||
axs[1].imshow(abs(adjoint)) | ||
axs[1].set_title("no density compensation") | ||
axs[2].imshow(abs(adjoint_manual)) | ||
axs[2].set_title("manual density compensation") |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
"""General utility functions for MRI-NUFFT.""" | ||
|
||
import warnings | ||
from collections import defaultdict | ||
from functools import wraps | ||
import numpy as np | ||
|
||
|
||
def proper_trajectory(trajectory, normalize="pi"): | ||
"""Normalize the trajectory to be used by NUFFT operators. | ||
|
||
Parameters | ||
---------- | ||
trajectory: np.ndarray | ||
The trajectory to normalize, it might be of shape (Nc, Ns, dim) of (Ns, dim) | ||
|
||
normalize: str | ||
if "pi" trajectory will be rescaled in [-pi, pi], if it was in [-0.5, 0.5] | ||
if "unit" trajectory will be rescaled in [-0.5, 0.5] if it was not [-0.5, 0.5] | ||
|
||
Returns | ||
------- | ||
new_traj: np.ndarray | ||
The normalized trajectory of shape (Nc * Ns, dim) or (Ns, dim) in -pi, pi | ||
""" | ||
# flatten to a list of point | ||
try: | ||
new_traj = np.asarray(trajectory).copy() | ||
except Exception as e: | ||
raise ValueError( | ||
"trajectory should be array_like, with the last dimension being coordinates" | ||
) from e | ||
new_traj = new_traj.reshape(-1, trajectory.shape[-1]) | ||
|
||
if normalize == "pi" and np.max(abs(new_traj)) - 1e-4 < 0.5: | ||
warnings.warn( | ||
"Samples will be rescaled to [-pi, pi), assuming they were in [-0.5, 0.5)" | ||
) | ||
new_traj *= 2 * np.pi | ||
elif normalize == "unit" and np.max(abs(new_traj)) - 1e-4 > 0.5: | ||
warnings.warn( | ||
"Samples will be rescaled to [-0.5, 0.5), assuming they were in [-pi, pi)" | ||
) | ||
new_traj /= 2 * np.pi | ||
if normalize == "unit" and np.max(new_traj) >= 0.5: | ||
new_traj = (new_traj + 0.5) % 1 - 0.5 | ||
return new_traj | ||
|
||
|
||
class MethodRegister: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Was this abstraction really needed and helpful? Especially when we have only 3 density methods and also in which only 2 are used. Although, i understand you are trying to be future proof. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
""" | ||
A Decorator to register methods of the same type in dictionnaries. | ||
|
||
Parameters | ||
---------- | ||
name: str | ||
The register | ||
""" | ||
|
||
registry = defaultdict(dict) | ||
|
||
def __init__(self, register_name): | ||
self.register_name = register_name | ||
|
||
def __call__(self, method_name=None): | ||
"""Register the function in the registry.""" | ||
|
||
def decorator(func): | ||
self.registry[self.register_name][method_name] = func | ||
|
||
@wraps(func) | ||
def wrapper(*args, **kwargs): | ||
return func(*args, **kwargs) | ||
|
||
return wrapper | ||
|
||
if callable(method_name): | ||
func = method_name | ||
method_name = func.__name__ | ||
return decorator(func) | ||
else: | ||
return decorator |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,13 @@ | ||
"""Density compensation methods.""" | ||
from .geometry_based import voronoi, voronoi_unique, cell_count | ||
from .nufft_based import pipe | ||
from .utils import get_density | ||
|
||
|
||
__all__ = ["voronoi", "voronoi_unique", "pipe", "cell_count"] | ||
__all__ = [ | ||
"voronoi", | ||
"voronoi_unique", | ||
"pipe", | ||
"cell_count", | ||
"get_density", | ||
] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In documentation I recommend mentioning a bit about each including computation times