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

Density compensation #66

Merged
merged 14 commits into from
Dec 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 3 additions & 2 deletions .github/workflows/test-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ jobs:
strategy:
matrix:
backend: [gpunufft, cufinufft]

steps:
- uses: actions/checkout@v3

Expand All @@ -133,13 +134,13 @@ jobs:
pip install -e mri-nufft[test]
pip install finufft

- name: Install Cufinufft
- name: Install Experimental cufinufft
if: ${{ matrix.backend == 'cufinufft' }}
shell: bash
run: |
cd $RUNNER_WORKSPACE
rm -rf finufft
git clone https://github.com/chaithyagr/finufft --branch chaithyagr/issue306
git clone https://github.com/flatironinstitute/finufft
Copy link
Member

Choose a reason for hiding this comment

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

Is there no pip install?

Copy link
Member Author

Choose a reason for hiding this comment

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

As the development of cufinufft is very active, I prefer to track directly the repo.

Copy link
Member

Choose a reason for hiding this comment

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

We might also need to test against the latest release version as that's what users use.

cd finufft && mkdir build && cd build

export PATH=/usr/local/cuda-11.8/bin:$PATH
Expand Down
2 changes: 1 addition & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ Usage
import numpy as np
import mrinufft
from mrinufft.trajectories import display
from mrinufft.trajectories.density import voronoi
from mrinufft.density import voronoi

# Create a 2D Radial trajectory for demo
samples_loc = mrinufft.initialize_2D_radial(Nc=100, Ns=500)
Expand Down
2 changes: 1 addition & 1 deletion examples/example_readme.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import numpy as np
import mrinufft
from mrinufft.trajectories import display
from mrinufft.trajectories.density import voronoi
from mrinufft.density import voronoi

# Create a 2D Radial trajectory for demo
samples_loc = mrinufft.initialize_2D_radial(Nc=100, Ns=500)
Expand Down
2 changes: 1 addition & 1 deletion examples/example_stacked.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
# ----------------------------

from mrinufft import initialize_2D_spiral
from mrinufft.trajectories.density import voronoi
from mrinufft.density import voronoi

samples = initialize_2D_spiral(Nc=16, Ns=500, nb_revolutions=10)
density = voronoi(samples)
Expand Down
5 changes: 5 additions & 0 deletions src/mrinufft/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
display_3D_trajectory,
)

from .density import voronoi, cell_count, pipe

__all__ = [
"get_operator",
"check_backend",
Expand All @@ -60,6 +62,9 @@
"displayConfig",
"display_2D_trajectory",
"display_3D_trajectory",
"voronoi",
"cell_count",
"pipe",
]


Expand Down
6 changes: 6 additions & 0 deletions src/mrinufft/density/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""Density compensation methods."""
from .geometry_based import voronoi, voronoi_unique, cell_count
from .nufft_based import pipe


__all__ = ["voronoi", "voronoi_unique", "pipe", "cell_count"]
Original file line number Diff line number Diff line change
@@ -1,22 +1,11 @@
"""
Estimation of the density compensation array methods.

Those methods are agnostic of the NUFFT operator.
"""
"""Compute density compensation weights using geometry-based methods."""
import numpy as np
from scipy.spatial import Voronoi
from mrinufft.operators import proper_trajectory
from mrinufft.operators.interfaces.cufinufft import pipe as pipe_cufinufft
from mrinufft.operators.interfaces.tfnufft import pipe as pipe_tfnufft
from mrinufft.operators.interfaces.gpunufft import pipe as pipe_gpunufft


def compute_tetrahedron_volume(A, B, C, D):
"""Compute the volume of a tetrahedron."""
return np.abs(np.dot(np.cross(B - A, C - A), D - A)) / 6.0
from .utils import flat_traj, normalize_weights


def vol3d(points):
def _vol3d(points):
"""Compute the volume of a convex 3D polygon.

Parameters
Expand All @@ -35,7 +24,7 @@ def vol3d(points):
return np.sum(np.abs(np.dot(np.cross(B, C), A.T))) / 6.0


def vol2d(points):
def _vol2d(points):
"""Compute the area of a convex 2D polygon.

Parameters
Expand All @@ -57,7 +46,8 @@ def vol2d(points):
return abs(area) / 2.0


def _voronoi(kspace):
@flat_traj
def voronoi_unique(traj, *args, **kwargs):
"""Estimate density compensation weight using voronoi parcellation.

This assume unicity of the point in the kspace.
Expand All @@ -66,26 +56,28 @@ def _voronoi(kspace):
----------
kspace: array_like
array of shape (M, 2) or (M, 3) containing the coordinates of the points.
*args, **kwargs:
Dummy arguments to be compatible with other methods.

Returns
-------
wi: array_like
array of shape (M,) containing the density compensation weights.
"""
M = kspace.shape[0]
if kspace.shape[1] == 2:
vol = vol2d
M = traj.shape[0]
if traj.shape[1] == 2:
vol = _vol2d
else:
vol = vol3d
vol = _vol3d
wi = np.zeros(M)
v = Voronoi(kspace)
v = Voronoi(traj)
for mm in range(M):
idx_vertices = v.regions[v.point_region[mm]]
if np.all([i != -1 for i in idx_vertices]):
wi[mm] = vol(v.vertices[idx_vertices])
# For edge point (infinite voronoi cells) we extrapolate from neighbours
# Initial implementation in Jeff Fessler's MIRT
rho = np.sum(kspace**2, axis=1)
rho = np.sum(traj**2, axis=1)
igood = rho > 0.6 * np.max(rho)
if len(igood) < 10:
print("dubious extrapolation with", len(igood), "points")
Expand All @@ -94,50 +86,95 @@ def _voronoi(kspace):
return wi


def voronoi(kspace):
@flat_traj
def voronoi(traj, *args, **kwargs):
"""Estimate density compensation weight using voronoi parcellation.

In case of multiple point in the center of kspace, the weight is split evenly.

Parameters
----------
kspace: array_like
traj: array_like
array of shape (M, 2) or (M, 3) containing the coordinates of the points.

*args, **kwargs:
Dummy arguments to be compatible with other methods.

References
----------
Based on the MATLAB implementation in MIRT: https://github.com/JeffFessler/mirt/blob/main/mri/ir_mri_density_comp.m
"""
# deduplication only works for the 0,0 coordinate !!
kspace = proper_trajectory(kspace)
i0 = np.sum(np.abs(kspace), axis=1) == 0
i0 = np.sum(np.abs(traj), axis=1) == 0
if np.any(i0):
i0f = np.where(i0)
i0f = i0f[0]
i0[i0f] = False
wi = np.zeros(len(kspace))
wi[~i0] = _voronoi(kspace[~i0])
wi = np.zeros(len(traj))
wi[~i0] = voronoi_unique(traj[~i0])
i0[i0f] = True
wi[i0] = wi[i0f] / np.sum(i0)
else:
wi = _voronoi(kspace)
wi /= np.sum(wi)
return wi
wi = voronoi_unique(traj)
return normalize_weights(wi)


def pipe(kspace_traj, grid_size, backend="cufinufft", **kwargs):
"""Compute the density compensation weights using the pipe method.
@flat_traj
def cell_count(traj, shape, osf=1.0):
"""
Compute the number of points in each cell of the grid.

Parameters
----------
kspace_traj: array_like
traj: array_like
array of shape (M, 2) or (M, 3) containing the coordinates of the points.
grid_size: array_like
array of shape (2,) or (3,) containing the size of the grid.
backend: str
backend to use for the computation. Either "cufinufft" or "tensorflow".
shape: tuple
shape of the grid.
osf: float
oversampling factor for the grid. default 1

Returns
-------
weights: array_like
array of shape (M,) containing the density compensation weights.

"""
if backend == "cufinufft":
return pipe_cufinufft(kspace_traj, grid_size, **kwargs)
elif backend == "tensorflow":
return pipe_tfnufft(kspace_traj, grid_size, **kwargs)
elif backend == "gpunufft":
return pipe_gpunufft(kspace_traj, grid_size, **kwargs)
bins = [np.linspace(-0.5, 0.5, int(osf * s) + 1) for s in shape]

h, edges = np.histogramdd(traj, bins)
if len(shape) == 2:
hsum = [np.sum(h, axis=1).astype(int), np.sum(h, axis=0).astype(int)]
else:
raise ValueError("backend not supported")
hsum = [
np.sum(h, axis=(1, 2)).astype(int),
np.sum(h, axis=(0, 2)).astype(int),
np.sum(h, axis=(0, 1)).astype(int),
]

# indices of ascending coordinate in each dimension.
locs_sorted = [np.argsort(traj[:, i]) for i in range(len(shape))]

weights = np.ones(len(traj))
set_xyz = [[], [], []]
for i in range(len(hsum)):
ind = 0
for binsize in hsum[i]:
s = set(locs_sorted[i][ind : ind + binsize])
if s:
set_xyz[i].append(s)
ind += binsize

for sx in set_xyz[0]:
for sy in set_xyz[1]:
sxy = sx.intersection(sy)
if not sxy:
continue
if len(shape) == 2:
weights[list(sxy)] = len(sxy)
continue
for sz in set_xyz[2]:
sxyz = sxy.intersection(sz)
if sxyz:
weights[list(sxyz)] = len(sxyz)

return normalize_weights(weights)
23 changes: 23 additions & 0 deletions src/mrinufft/density/nufft_based.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
"""Density compensation weights using the NUFFT-based methods."""

from mrinufft import get_operator
from .utils import flat_traj


@flat_traj
def pipe(traj, shape, backend="cufinufft", **kwargs):
"""Compute the density compensation weights using the pipe method.

Parameters
----------
traj: array_like
array of shape (M, 2) or (M, 3) containing the coordinates of the points.
shape: array_like
array of shape (2,) or (3,) containing the size of the grid.
backend: str
backend to use for the computation. Either "cufinufft" or "tensorflow".
"""
nufft_class = get_operator(backend)
if hasattr(nufft_class, "pipe"):
return nufft_class.pipe(traj, shape, **kwargs)
raise ValueError("backend does not have pipe iterations method.")
35 changes: 35 additions & 0 deletions src/mrinufft/density/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"""Utilities for density compensation."""
from functools import wraps

import numpy as np

from mrinufft.operators import proper_trajectory


def flat_traj(normalize="unit"):
"""Decorate function to ensure that the trajectory is flatten before calling."""

def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
args = list(args)
args[0] = proper_trajectory(args[0], normalize=normalize)
return func(*args, **kwargs)

return wrapper

if callable(normalize): # call without argument
func = normalize
normalize = "unit"
return decorator(func)
else:
return decorator


def normalize_weights(weights):
"""Normalize samples weights to reflect their importance.

Higher weights have lower importance.
"""
inv_weights = np.sum(weights) / weights
return inv_weights / (np.sum(inv_weights))
Loading