Skip to content

Commit 634d89a

Browse files
authored
Merge pull request #107 from nipreps/enh/data-driven-b0-identification
ENH: Data-driven b0 identification tool
2 parents 4e9d623 + 71973b4 commit 634d89a

File tree

2 files changed

+54
-0
lines changed

2 files changed

+54
-0
lines changed

dmriprep/utils/tests/test_vectors.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Test vector utilities."""
22
import pytest
33
import numpy as np
4+
import nibabel as nb
45
from dmriprep.utils import vectors as v
56
from collections import namedtuple
67

@@ -100,3 +101,25 @@ def mock_func(*args, **kwargs):
100101
# Miscellaneous tests
101102
with pytest.raises(ValueError):
102103
dgt.to_filename("path", filetype="mrtrix")
104+
105+
106+
def test_b0mask_from_data(tmp_path):
107+
"""Check the estimation of bzeros using the dwi data."""
108+
109+
highb = np.random.normal(100, 5, size=(40, 40, 40, 99))
110+
mask_file = tmp_path / 'mask.nii.gz'
111+
112+
# Test 1: no lowb
113+
dwi_file = tmp_path / 'only_highb.nii.gz'
114+
nb.Nifti1Image(highb.astype(float), np.eye(4), None).to_filename(dwi_file)
115+
nb.Nifti1Image(np.ones((40, 40, 40), dtype=np.uint8),
116+
np.eye(4), None).to_filename(mask_file)
117+
118+
assert v.b0mask_from_data(dwi_file, mask_file).sum() == 0
119+
120+
# Test 1: one lowb
121+
lowb = np.random.normal(400, 50, size=(40, 40, 40, 1))
122+
dwi_file = tmp_path / 'dwi.nii.gz'
123+
nb.Nifti1Image(np.concatenate((lowb, highb), axis=3).astype(float),
124+
np.eye(4), None).to_filename(dwi_file)
125+
assert v.b0mask_from_data(dwi_file, mask_file).sum() == 1

dmriprep/utils/vectors.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -483,3 +483,34 @@ def bvecs2ras(affine, bvecs, norm=True, bvec_norm_epsilon=0.2):
483483
rotated_bvecs[~b0s] /= norms_bvecs[~b0s, np.newaxis]
484484
rotated_bvecs[b0s] = np.zeros(3)
485485
return rotated_bvecs
486+
487+
488+
def rasb_dwi_length_check(dwi_file, rasb_file):
489+
"""Check the number of encoding vectors and number of orientations in the DWI file."""
490+
return nb.load(dwi_file).shape[-1] == len(np.loadtxt(rasb_file, skiprows=1))
491+
492+
493+
def b0mask_from_data(dwi_file, mask_file, z_thres=3.0):
494+
"""
495+
Evaluate B0 locations relative to mean signal variation.
496+
497+
Standardizes (z-score) the average DWI signal within mask and threshold.
498+
This is a data-driven way of estimating which volumes in the DWI dataset are
499+
really encoding *low-b* acquisitions.
500+
501+
Parameters
502+
----------
503+
dwi_file : :obj:`str`
504+
File path to the diffusion-weighted image series.
505+
mask_file : :obj:`str`
506+
File path to a mask corresponding to the DWI file.
507+
z_thres : :obj:`float`
508+
The z-value to consider a volume as a *low-b* orientation.
509+
510+
"""
511+
data = np.asanyarray(nb.load(dwi_file).dataobj)
512+
mask = np.asanyarray(nb.load(mask_file).dataobj) > 0.5
513+
signal_means = np.median(data[mask, np.newaxis], axis=0)
514+
zscored_means = signal_means - np.median(signal_means)
515+
zscored_means /= zscored_means.std()
516+
return zscored_means > z_thres

0 commit comments

Comments
 (0)