Skip to content

Commit eeb99e3

Browse files
committed
Enhance user feedback: Add is_data_peaks heuristic and shell distance warnings
1 parent 7f7d3ca commit eeb99e3

3 files changed

Lines changed: 87 additions & 9 deletions

File tree

src/scilpy/cli/scil_frf_ssst.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@
2020
import nibabel as nib
2121
import numpy as np
2222

23-
from scilpy.gradients.bvec_bval_tools import check_b0_threshold
23+
from scilpy.gradients.bvec_bval_tools import (check_b0_threshold,
24+
identify_shells)
2425
from scilpy.io.image import get_data_as_mask
2526
from scilpy.io.utils import (add_b0_thresh_arg, add_overwrite_arg,
2627
add_precision_arg,
@@ -111,6 +112,21 @@ def main():
111112
b0_thr=args.b0_threshold,
112113
skip_b0_check=args.skip_b0_check)
113114

115+
shells_centroids, _ = identify_shells(bvals, args.b0_threshold,
116+
round_centroids=True)
117+
shells_centroids = list(sorted(
118+
shells_centroids[shells_centroids > args.b0_threshold]))
119+
min_non_b0_shell = np.min(shells_centroids) \
120+
if len(shells_centroids) > 0 else 0
121+
max_non_b0_delta = np.ediff1d(shells_centroids)[0] \
122+
if len(shells_centroids) > 1 else 0
123+
if max_non_b0_delta >= min_non_b0_shell:
124+
logging.warning(
125+
'Your shells seem to be very far apart (max delta: {}, '
126+
'min non-b0 shell: {}). This might cause problems for the '
127+
'estimation of the FRF. Consider using scil_frf_msmt.py.'
128+
.format(max_non_b0_delta, min_non_b0_shell))
129+
114130
mask = get_data_as_mask(nib.load(args.mask),
115131
dtype=bool) if args.mask else None
116132
mask_wm = get_data_as_mask(nib.load(args.mask_wm),

src/scilpy/reconst/utils.py

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,9 @@ def get_maximas(data, sphere, b_matrix, threshold, absolute_threshold,
3333
spherical_func = np.dot(data, b_matrix.T)
3434
spherical_func[np.nonzero(spherical_func < absolute_threshold)] = 0.
3535
return peak_directions(
36-
spherical_func, sphere, threshold, min_separation_angle)
36+
spherical_func, sphere,
37+
relative_peak_threshold=threshold,
38+
min_separation_angle=min_separation_angle)
3739

3840

3941
def get_sphere_neighbours(sphere, max_angle):
@@ -57,3 +59,68 @@ def get_sphere_neighbours(sphere, max_angle):
5759
np.outer(zs, zs))
5860
neighbours = scalar_prods >= np.cos(max_angle)
5961
return neighbours
62+
63+
64+
def is_data_peaks(img_data):
65+
"""
66+
Heuristic to find out if the input are peaks or fodf.
67+
fodf are always around 0.15 and peaks around 0.75.
68+
Peaks have more zero values than fodf. The first value of fodf is
69+
usually the highest.
70+
71+
Parameters
72+
----------
73+
img_data : np.ndarray
74+
4D image data where the last dimension contains directional info.
75+
76+
Returns
77+
-------
78+
is_peaks : bool
79+
True if data is likely peaks, False if likely fODF (SH).
80+
"""
81+
last_dim = img_data.shape[-1]
82+
if last_dim == 3:
83+
return True
84+
85+
# Sum of absolute values to detect non-zero voxels correctly
86+
non_zeros_mask = np.any(np.abs(img_data) > 0, axis=-1)
87+
if not np.count_nonzero(non_zeros_mask):
88+
return False
89+
90+
try:
91+
order, full = get_sh_order_and_fullness(last_dim)
92+
# Symmetric SH must be even order
93+
if not full and order % 2 != 0:
94+
return False
95+
except ValueError:
96+
# If not a valid SH number of coefficients, and not 3,
97+
# it might be something else, but if it's a multiple of 3
98+
# it's likely Peaks.
99+
if last_dim % 3 == 0:
100+
return True
101+
return False
102+
103+
data_nz = img_data[non_zeros_mask]
104+
105+
# If all triplets have the same norm, it is likely peaks, otherwise SH.
106+
if last_dim % 3 == 0:
107+
norm = np.linalg.norm(data_nz.reshape(-1, 3), axis=-1)
108+
if np.all(np.isclose(norm, norm[0])):
109+
return True
110+
111+
# If the max is in the first triplet but not at index 0, it's likely Peaks.
112+
# Smoothed SH almost always has max at index 0
113+
argmax_indices = np.argmax(np.abs(data_nz), axis=-1)
114+
if last_dim % 3 == 0 and \
115+
np.mean(np.logical_or(argmax_indices == 1,
116+
argmax_indices == 2)) > 0.1:
117+
return True
118+
119+
# Exact zeros. SH almost never has exact zeros in real data.
120+
# Peaks often have exact zeros for unused lobes
121+
zero_ratio = np.mean(data_nz == 0)
122+
if zero_ratio > 0.05:
123+
return True
124+
125+
# Default to SH
126+
return False

src/scilpy/tracking/utils.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -351,13 +351,8 @@ def get_direction_getter(in_img, algo, sphere, sub_sphere, theta, sh_basis,
351351
# Theta depends on user choice and algorithm
352352
theta = get_theta(theta, algo)
353353

354-
# Heuristic to find out if the input are peaks or fodf
355-
# fodf are always around 0.15 and peaks around 0.75
356-
# Peaks have more zero values than fodf. The first value of fodf is
357-
# usually the highest.
358-
non_zeros_count = np.count_nonzero(np.sum(img_data, axis=-1))
359-
non_first_val_count = np.count_nonzero(np.argmax(img_data, axis=-1))
360-
is_peaks = non_first_val_count / non_zeros_count > 0.5
354+
from scilpy.reconst.utils import is_data_peaks
355+
is_peaks = is_data_peaks(img_data)
361356

362357
if algo in ['det', 'prob', 'ptt']:
363358
if is_peaks:

0 commit comments

Comments
 (0)