Skip to content

Commit

Permalink
Merge pull request #867 from EmmaRenauld/clarify_b0_threshold
Browse files Browse the repository at this point in the history
Clarify b0 threshold
  • Loading branch information
arnaudbore authored Feb 29, 2024
2 parents 4c2848c + 3ce0db0 commit 1c3029a
Show file tree
Hide file tree
Showing 29 changed files with 539 additions and 309 deletions.
77 changes: 42 additions & 35 deletions scilpy/gradients/bvec_bval_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,58 +55,65 @@ def normalize_bvecs(bvecs):
return bvecs


def check_b0_threshold(
force_b0_threshold, bvals_min, b0_thr=DEFAULT_B0_THRESHOLD
):
"""Check if the minimal bvalue is under zero or over the threshold.
If `force_b0_threshold` is true, don't raise an error even if the minimum
bvalue is over the threshold.
def check_b0_threshold(min_bval, b0_thr, skip_b0_check):
"""
Check if the minimal bvalue is under the threshold. If not, raise an
error to ask user to update the b0_thr.
Also verifies if the b0_thr is suspicious (should be included in range
[0, DEFAULT_B0_THRESHOLD]).
Parameters
----------
force_b0_threshold : bool
If True, don't raise an error.
bvals_min : float
min_bval : float
Minimum bvalue.
b0_thr : float
b0_thr: float
Maximum bvalue considered as a b0.
skip_b0_check: bool
If True, and no b0 is found, only print a warning, do not raise
an error.
Returns
-------
b0_thr: float
Either the unmodified b0_thr, or, in the case where the minimal b-value
is larger than b0_thr, and skip_b0_check is set to True, then returns
min_bval.
Raises
------
ValueError
If the minimal bvalue is over the threshold, and
`force_b0_threshold` is False.
If the minimal bvalue is over the threshold (and skip_b0_check is
False).
"""
if b0_thr > DEFAULT_B0_THRESHOLD:
logging.warning(
'Warning: Your defined threshold is {}. This is suspicious. We '
'recommend using volumes with bvalues no higher '
'than {} as b0s.'.format(b0_thr, DEFAULT_B0_THRESHOLD)
)
'Your defined b0 threshold is {}. This is suspicious. We '
'recommend using volumes with bvalues no higher than {} as b0s'
.format(b0_thr, DEFAULT_B0_THRESHOLD))

if bvals_min < 0:
if min_bval < 0:
logging.warning(
'Warning: Your dataset contains negative b-values (minimal '
'bvalue of {}). This is suspicious. We recommend you check '
'your data.')
'Warning: Your dataset contains negative b-values (minimal bvalue '
'of {}). This is suspicious. We recommend you check your data.'
.format(min_bval))

if bvals_min > b0_thr:
if force_b0_threshold:
if min_bval > b0_thr:
if skip_b0_check:
logging.warning(
'Warning: Your minimal bvalue is {}, but the threshold '
'is set to {}. Since --force_b0_threshold was specified, '
'the script will proceed with a threshold of {}'
'.'.format(bvals_min, b0_thr, bvals_min))
return bvals_min
'Your minimal bvalue ({}), is above the threshold ({})\n'
'Since --skip_b0_check was specified, the script will '
'proceed with a b0 threshold of {}.'
.format(min_bval, b0_thr, min_bval))
return min_bval
else:
raise ValueError('The minimal bvalue ({}) is greater than the '
'threshold ({}). No b0 volumes can be found.\n'
'Please check your data to ensure everything '
'is correct.\n'
'Use --force_b0_threshold to execute '
'regardless.'
.format(bvals_min, b0_thr))

raise ValueError(
'The minimal bvalue ({}) is above the threshold ({})\n'
'No b0 volumes can be found.\n'
'Please check your data to ensure everything is correct.\n'
'You may also increase the threshold or use '
'--skip_b0_check'
.format(min_bval, b0_thr))
return b0_thr


Expand Down
18 changes: 13 additions & 5 deletions scilpy/gradients/tests/test_bvec_bval_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
import numpy as np

from scilpy.gradients.bvec_bval_tools import (
identify_shells, is_normalized_bvecs, flip_gradient_sampling,
normalize_bvecs, round_bvals_to_shell, str_to_axis_index,
swap_gradient_axis)
check_b0_threshold, identify_shells, is_normalized_bvecs,
flip_gradient_sampling, normalize_bvecs, round_bvals_to_shell,
str_to_axis_index, swap_gradient_axis)

bvecs = np.asarray([[1.0, 1.0, 1.0],
[1.0, 0.0, 1.0],
Expand All @@ -23,8 +23,16 @@ def test_normalize_bvecs():


def test_check_b0_threshold():
# toDo To be modified (see PR#867).
pass
assert check_b0_threshold(min_bval=0, b0_thr=0, skip_b0_check=False) == 0
assert check_b0_threshold(min_bval=0, b0_thr=20, skip_b0_check=False) == 20
assert check_b0_threshold(min_bval=20, b0_thr=0, skip_b0_check=True) == 20

error_raised = False
try:
_ = check_b0_threshold(min_bval=20, b0_thr=0, skip_b0_check=False)
except ValueError:
error_raised = True
assert error_raised


def test_identify_shells():
Expand Down
18 changes: 14 additions & 4 deletions scilpy/io/btensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@

from scilpy.dwi.utils import extract_dwi_shell
from scilpy.gradients.bvec_bval_tools import (normalize_bvecs,
is_normalized_bvecs)
is_normalized_bvecs,
check_b0_threshold)


bshapes = {0: "STE", 1: "LTE", -0.5: "PTE", 0.5: "CTE"}
Expand Down Expand Up @@ -50,11 +51,14 @@ def convert_bdelta_to_bshape(b_deltas):


def generate_btensor_input(in_dwis, in_bvals, in_bvecs, in_bdeltas,
do_pa_signals=False, tol=20):
do_pa_signals=False, tol=20, skip_b0_check=False):
"""Generate b-tensor input from an ensemble of data, bvals and bvecs files.
This generated input is mandatory for all scripts using b-tensor encoding
data. Also generate the powder-averaged (PA) data if set.
For the moment, this does not enable the use of a b0_threshold different
than the tolerance.
Parameters
----------
in_dwis : list of strings (file paths)
Expand All @@ -71,6 +75,10 @@ def generate_btensor_input(in_dwis, in_bvals, in_bvecs, in_bdeltas,
each bvals.
tol : int
tolerance gap for b-values clustering. Defaults to 20
skip_b0_check: bool
(See full explanation in io.utils.add_skip_b0_check_arg.) If true,
script will continue even if no b-values are found below the tolerance
(no b0s found).
Returns
-------
Expand Down Expand Up @@ -105,6 +113,8 @@ def generate_btensor_input(in_dwis, in_bvals, in_bvecs, in_bdeltas,
if inputf: # verifies if the input file exists
vol = nib.load(inputf)
bvals, bvecs = read_bvals_bvecs(bvalsf, bvecsf)
_ = check_b0_threshold(bvals.min(), b0_thr=tol,
skip_b0_check=skip_b0_check)
if np.sum([bvals > tol]) != 0:
bvals = np.round(bvals)
if not is_normalized_bvecs(bvecs):
Expand Down Expand Up @@ -156,7 +166,7 @@ def generate_btensor_input(in_dwis, in_bvals, in_bvecs, in_bdeltas,
gtab_infos[3] = acq_index_full
if np.sum([ubvals_full < tol]) < acq_index - 1:
gtab_infos[3] *= 0
return(pa_signals, gtab_infos)
return pa_signals, gtab_infos
# Removing the duplicate b0s from ubvals_full
duplicate_b0_ind = np.union1d(np.argwhere(ubvals_full == min(ubvals_full)),
np.argwhere(ubvals_full > tol))
Expand Down Expand Up @@ -185,4 +195,4 @@ def generate_btensor_input(in_dwis, in_bvals, in_bvecs, in_bdeltas,
b0_threshold=bvals_full.min(),
btens=b_shapes)

return(gtab_full, data_full, ubvals_full, ub_deltas_full)
return gtab_full, data_full, ubvals_full, ub_deltas_full
54 changes: 49 additions & 5 deletions scilpy/io/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,11 +215,55 @@ def add_overwrite_arg(parser):
help='Force overwriting of the output files.')


def add_force_b0_arg(parser):
parser.add_argument('--force_b0_threshold', action='store_true',
help='If set, the script will continue even if the '
'minimum bvalue is suspiciously high ( > {})'
.format(DEFAULT_B0_THRESHOLD))
def add_tolerance_arg(parser):
parser.add_argument(
'--tolerance', type=int, default=20, metavar='tol',
help='The tolerated gap between the b-values to extract and the '
'current b-value.\n'
'[Default: %(default)s]\n'
'* Note. We would expect to find at least one b-value in the \n'
' range [0, tolerance]. To skip this check, use '
'--skip_b0_check.')


def add_b0_thresh_arg(parser):
parser.add_argument(
'--b0_threshold', type=float, default=DEFAULT_B0_THRESHOLD,
metavar='thr',
help='Threshold under which b-values are considered to be b0s.\n'
'[Default: %(default)s] \n'
'* Note. We would expect to find at least one b-value in the \n'
' range [0, b0_threshold]. To skip this check, use '
'--skip_b0_check.')


def add_skip_b0_check_arg(parser, will_overwrite_with_min,
b0_tol_name='--b0_threshold'):
"""
Parameters
----------
parser: argparse.ArgumentParser object
Parser.
will_overwrite_with_min: bool
If true, the help message will explain that b0_threshold could be
overwritten.
b0_tol_name: str
Name of the argparse parameter acting as b0_threshold. Should probably
be either '--b0_threshold' or '--tolerance'.
"""
msg = ('By default, we supervise that at least one b0 exists in your '
'data\n'
'(i.e. b-values below the default {}). Use this option to \n'
'allow continuing even if the minimum b-value is suspiciously '
'high.\n'.format(b0_tol_name))
if will_overwrite_with_min:
msg += ('If no b-value is found below the threshold, the script will '
'continue \nwith your minimal b-value as new {}.\n'
.format(b0_tol_name))
msg += 'Use with care, and only if you understand your data.'

parser.add_argument(
'--skip_b0_check', action='store_true', help=msg)


def add_verbose_arg(parser):
Expand Down
43 changes: 26 additions & 17 deletions scilpy/reconst/frf.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@
import numpy as np

from scilpy.gradients.bvec_bval_tools import (check_b0_threshold,
is_normalized_bvecs, normalize_bvecs)
is_normalized_bvecs,
normalize_bvecs,
DEFAULT_B0_THRESHOLD)


def compute_ssst_frf(data, bvals, bvecs, mask=None, mask_wm=None,
fa_thresh=0.7, min_fa_thresh=0.5, min_nvox=300,
roi_radii=10, roi_center=None, force_b0_threshold=False):
def compute_ssst_frf(data, bvals, bvecs, b0_threshold=DEFAULT_B0_THRESHOLD,
mask=None, mask_wm=None, fa_thresh=0.7, min_fa_thresh=0.5,
min_nvox=300, roi_radii=10, roi_center=None):
"""Compute a single-shell (under b=1500), single-tissue single Fiber
Response Function from a DWI volume.
A DTI fit is made, and voxels containing a single fiber population are
Expand All @@ -29,6 +31,8 @@ def compute_ssst_frf(data, bvals, bvecs, mask=None, mask_wm=None,
1D bvals array with shape (N,)
bvecs : ndarray
2D bvecs array with shape (N, 3)
b0_threshold: float
Value under which bvals are considered b0s.
mask : ndarray, optional
3D mask with shape (X,Y,Z)
Binary mask. Only the data inside the mask will be used for
Expand All @@ -55,8 +59,6 @@ def compute_ssst_frf(data, bvals, bvecs, mask=None, mask_wm=None,
roi_center : tuple(3), optional
Use this center to span the roi of size roi_radius (center of the
3D volume).
force_b0_threshold : bool, optional
If set, will continue even if the minimum bvalue is suspiciously high.
Returns
-------
Expand All @@ -78,9 +80,7 @@ def compute_ssst_frf(data, bvals, bvecs, mask=None, mask_wm=None,
logging.warning("Your b-vectors do not seem normalized... Normalizing")
bvecs = normalize_bvecs(bvecs)

b0_thr = check_b0_threshold(force_b0_threshold, bvals.min(), bvals.min())

gtab = gradient_table(bvals, bvecs, b0_threshold=b0_thr)
gtab = gradient_table(bvals, bvecs, b0_threshold=b0_threshold)

if mask is not None:
data = applymask(data, mask)
Expand Down Expand Up @@ -137,8 +137,7 @@ def compute_msmt_frf(data, bvals, bvecs, btens=None, data_dti=None,
mask=None, mask_wm=None, mask_gm=None, mask_csf=None,
fa_thr_wm=0.7, fa_thr_gm=0.2, fa_thr_csf=0.1,
md_thr_gm=0.0007, md_thr_csf=0.003, min_nvox=300,
roi_radii=10, roi_center=None, tol=20,
force_b0_threshold=False):
roi_radii=10, roi_center=None, tol=20):
"""Compute a multi-shell, multi-tissue single Fiber
Response Function from a DWI volume.
A DTI fit is made, and voxels containing a single fiber population are
Expand All @@ -152,6 +151,18 @@ def compute_msmt_frf(data, bvals, bvecs, btens=None, data_dti=None,
1D bvals array with shape (N,)
bvecs : ndarray
2D bvecs array with shape (N, 3)
btens: ndarray 1D
btens array with shape (N,), describing the btensor shape of every
pair of bval/bvec.
data_dti: ndarray 4D
Input diffusion volume with shape (X, Y, Z, M), where M is the number
of DTI directions.
bvals_dti: ndarray 1D
bvals array with shape (M,)
bvecs_dti: ndarray 2D
bvals array with shape (M, 3)
btens_dti: ndarray 1D
bvals array with shape (M,)
mask : ndarray, optional
3D mask with shape (X,Y,Z)
Binary mask. Only the data inside the mask will be used for
Expand Down Expand Up @@ -200,8 +211,6 @@ def compute_msmt_frf(data, bvals, bvecs, btens=None, data_dti=None,
3D volume).
tol : int
tolerance gap for b-values clustering. Defaults to 20
force_b0_threshold : bool, optional
If set, will continue even if the minimum bvalue is suspiciously high.
Returns
-------
Expand All @@ -221,9 +230,10 @@ def compute_msmt_frf(data, bvals, bvecs, btens=None, data_dti=None,
logging.warning('Your b-vectors do not seem normalized...')
bvecs = normalize_bvecs(bvecs)

b0_thr = check_b0_threshold(force_b0_threshold, bvals.min(), bvals.min())

gtab = gradient_table(bvals, bvecs, btens=btens, b0_threshold=b0_thr)
# Note. Using the tolerance here because currently, the gtab.b0s_mask is
# not used. Below, we use the tolerance only (in dipy).
# An issue has been added in dipy too.
gtab = gradient_table(bvals, bvecs, btens=btens, b0_threshold=tol)

if data_dti is None and bvals_dti is None and bvecs_dti is None:
logging.warning(
Expand All @@ -244,7 +254,6 @@ def compute_msmt_frf(data, bvals, bvecs, btens=None, data_dti=None,
logging.warning('Your b-vectors do not seem normalized...')
bvecs_dti = normalize_bvecs(bvecs_dti)

check_b0_threshold(force_b0_threshold, bvals_dti.min())
gtab_dti = gradient_table(bvals_dti, bvecs_dti, btens=btens_dti)

wm_frf_mask, gm_frf_mask, csf_frf_mask \
Expand Down
Loading

0 comments on commit 1c3029a

Please sign in to comment.