Skip to content

Commit 8e6b578

Browse files
dPysarokem
authored andcommitted
[ENH] Update DiffusionGradientTable interface to support vector reorientation
1 parent 6cdbbfc commit 8e6b578

File tree

2 files changed

+154
-53
lines changed

2 files changed

+154
-53
lines changed

dmriprep/interfaces/vectors.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,3 +97,71 @@ def _undefined(objekt, name, default=None):
9797
if not isdefined(value):
9898
return default
9999
return value
100+
101+
102+
class _ReorientVectorsInputSpec(BaseInterfaceInputSpec):
103+
dwi_file = File(exists=True)
104+
rasb_file = File(exists=True)
105+
affines = traits.List()
106+
b0_threshold = traits.Float(B0_THRESHOLD, usedefault=True)
107+
108+
109+
class _ReorientVectorsOutputSpec(TraitedSpec):
110+
out_rasb = File(exists=True)
111+
112+
113+
class ReorientVectors(SimpleInterface):
114+
"""
115+
Reorient Vectors
116+
117+
Example
118+
-------
119+
>>> os.chdir(tmpdir)
120+
>>> oldrasb = str(data_dir / 'dwi.tsv')
121+
>>> oldrasb_mat = np.loadtxt(str(data_dir / 'dwi.tsv'), skiprows=1)
122+
>>> # The simple case: all affines are identity
123+
>>> affine_list = np.zeros((len(oldrasb_mat[:, 3][oldrasb_mat[:, 3] != 0]), 4, 4))
124+
>>> for i in range(4):
125+
>>> affine_list[:, i, i] = 1
126+
>>> reor_vecs = ReorientVectors()
127+
>>> reor_vecs = ReorientVectors()
128+
>>> reor_vecs.inputs.affines = affine_list
129+
>>> reor_vecs.inputs.in_rasb = oldrasb
130+
>>> res = reor_vecs.run()
131+
>>> out_rasb = res.outputs.out_rasb
132+
>>> out_rasb_mat = np.loadtxt(out_rasb, skiprows=1)
133+
>>> assert oldrasb_mat == out_rasb_mat
134+
True
135+
"""
136+
137+
input_spec = _ReorientVectorsInputSpec
138+
output_spec = _ReorientVectorsOutputSpec
139+
140+
def _run_interface(self, runtime):
141+
from nipype.utils.filemanip import fname_presuffix
142+
143+
table = DiffusionGradientTable(
144+
dwi_file=self.inputs.dwi_file,
145+
rasb_file=self.inputs.rasb_file,
146+
transforms=self.inputs.affines,
147+
)
148+
table.generate_vecval()
149+
reor_table = table.reorient_rasb()
150+
151+
cwd = Path(runtime.cwd).absolute()
152+
reor_rasb_file = fname_presuffix(
153+
self.inputs.rasb_file,
154+
use_ext=False,
155+
suffix="_reoriented.tsv",
156+
newpath=str(cwd),
157+
)
158+
np.savetxt(
159+
str(reor_rasb_file),
160+
reor_table,
161+
delimiter="\t",
162+
header="\t".join("RASB"),
163+
fmt=["%.8f"] * 3 + ["%g"],
164+
)
165+
166+
self._results["out_rasb"] = reor_rasb_file
167+
return runtime

dmriprep/utils/vectors.py

Lines changed: 86 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,29 @@
1212
class DiffusionGradientTable:
1313
"""Data structure for DWI gradients."""
1414

15-
__slots__ = ['_affine', '_gradients', '_b_scale', '_bvecs', '_bvals', '_normalized',
16-
'_b0_thres', '_bvec_norm_epsilon']
17-
18-
def __init__(self, dwi_file=None, bvecs=None, bvals=None, rasb_file=None,
19-
b_scale=True, b0_threshold=B0_THRESHOLD, bvec_norm_epsilon=BVEC_NORM_EPSILON):
15+
__slots__ = [
16+
"_affine",
17+
"_gradients",
18+
"_b_scale",
19+
"_bvecs",
20+
"_bvals",
21+
"_normalized",
22+
"_transforms",
23+
"_b0_thres",
24+
"_bvec_norm_epsilon",
25+
]
26+
27+
def __init__(
28+
self,
29+
dwi_file=None,
30+
bvecs=None,
31+
bvals=None,
32+
rasb_file=None,
33+
b_scale=True,
34+
transforms=None,
35+
b0_threshold=B0_THRESHOLD,
36+
bvec_norm_epsilon=BVEC_NORM_EPSILON,
37+
):
2038
"""
2139
Create a new table of diffusion gradients.
2240
@@ -34,8 +52,8 @@ def __init__(self, dwi_file=None, bvecs=None, bvals=None, rasb_file=None,
3452
then bvecs and bvals will be dismissed.
3553
b_scale : bool
3654
Whether b-values should be normalized.
37-
3855
"""
56+
self._transforms = transforms
3957
self._b_scale = b_scale
4058
self._b0_thres = b0_threshold
4159
self._bvec_norm_epsilon = bvec_norm_epsilon
@@ -87,7 +105,7 @@ def affine(self, value):
87105
dwi_file = nb.load(str(value))
88106
self._affine = dwi_file.affine.copy()
89107
return
90-
if hasattr(value, 'affine'):
108+
if hasattr(value, "affine"):
91109
self._affine = value.affine
92110
self._affine = np.array(value)
93111

@@ -102,20 +120,20 @@ def bvecs(self, value):
102120
if isinstance(value, (str, Path)):
103121
value = np.loadtxt(str(value)).T
104122
else:
105-
value = np.array(value, dtype='float32')
123+
value = np.array(value, dtype="float32")
106124

107125
# Correct any b0's in bvecs misstated as 10's.
108126
value[np.any(abs(value) >= 10, axis=1)] = np.zeros(3)
109127
if self.bvals is not None and value.shape[0] != self.bvals.shape[0]:
110-
raise ValueError('The number of b-vectors and b-values do not match')
128+
raise ValueError("The number of b-vectors and b-values do not match")
111129
self._bvecs = value
112130

113131
@bvals.setter
114132
def bvals(self, value):
115133
if isinstance(value, (str, Path)):
116134
value = np.loadtxt(str(value)).flatten()
117135
if self.bvecs is not None and value.shape[0] != self.bvecs.shape[0]:
118-
raise ValueError('The number of b-vectors and b-values do not match')
136+
raise ValueError("The number of b-vectors and b-values do not match")
119137
self._bvals = np.array(value)
120138

121139
@property
@@ -129,10 +147,12 @@ def normalize(self):
129147
return
130148

131149
self._bvecs, self._bvals = normalize_gradients(
132-
self.bvecs, self.bvals,
150+
self.bvecs,
151+
self.bvals,
133152
b0_threshold=self._b0_thres,
134153
bvec_norm_epsilon=self._bvec_norm_epsilon,
135-
b_scale=self._b_scale)
154+
b_scale=self._b_scale,
155+
)
136156
self._normalized = True
137157

138158
def generate_rasb(self):
@@ -142,14 +162,50 @@ def generate_rasb(self):
142162
_ras = bvecs2ras(self.affine, self.bvecs)
143163
self.gradients = np.hstack((_ras, self.bvals[..., np.newaxis]))
144164

165+
def reorient_rasb(self):
166+
"""Reorient the vectors based o a list of affine transforms."""
167+
from dipy.core.gradients import gradient_table_from_bvals_bvecs, reorient_bvecs
168+
169+
affines = self._transforms.copy()
170+
bvals = self._bvals
171+
bvecs = self._bvecs
172+
173+
# Verify that number of non-B0 volumes corresponds to the number of affines.
174+
# If not, raise an error.
175+
if len(self._bvals[self._bvals >= self._b0_thres]) != len(affines):
176+
b0_indices = np.where(self._bvals <= self._b0_thres)[0].tolist()
177+
if len(self._bvals[self._bvals >= self._b0_thres]) < len(affines):
178+
for i in sorted(b0_indices, reverse=True):
179+
del affines[i]
180+
if len(self._bvals[self._bvals >= self._b0_thres]) > len(affines):
181+
ras_b_mat = self._gradients.copy()
182+
ras_b_mat = np.delete(ras_b_mat, tuple(b0_indices), axis=0)
183+
bvals = ras_b_mat[:, 3]
184+
bvecs = ras_b_mat[:, 0:3]
185+
if len(self._bvals[self._bvals > self._b0_thres]) != len(affines):
186+
raise ValueError(
187+
"Affine transformations do not correspond to gradients"
188+
)
189+
190+
# Build gradient table object
191+
gt = gradient_table_from_bvals_bvecs(bvals, bvecs, b0_threshold=self._b0_thres)
192+
193+
# Reorient table
194+
new_gt = reorient_bvecs(gt, [np.load(aff) for aff in affines])
195+
196+
return np.hstack((new_gt.bvecs, new_gt.bvals[..., np.newaxis]))
197+
145198
def generate_vecval(self):
146199
"""Compose a bvec/bval pair in image coordinates."""
147200
if self.bvecs is None or self.bvals is None:
148201
if self.affine is None:
149202
raise TypeError(
150203
"Cannot generate b-vectors & b-values in image coordinates. "
151-
"Please set the corresponding DWI image's affine matrix.")
152-
self._bvecs = bvecs2ras(np.linalg.inv(self.affine), self.gradients[..., :-1])
204+
"Please set the corresponding DWI image's affine matrix."
205+
)
206+
self._bvecs = bvecs2ras(
207+
np.linalg.inv(self.affine), self.gradients[..., :-1]
208+
)
153209
self._bvals = self.gradients[..., -1].flatten()
154210

155211
@property
@@ -161,22 +217,28 @@ def pole(self):
161217
162218
"""
163219
self.generate_rasb()
164-
return calculate_pole(self.gradients[..., :-1], bvec_norm_epsilon=self._bvec_norm_epsilon)
220+
return calculate_pole(
221+
self.gradients[..., :-1], bvec_norm_epsilon=self._bvec_norm_epsilon
222+
)
165223

166-
def to_filename(self, filename, filetype='rasb'):
224+
def to_filename(self, filename, filetype="rasb"):
167225
"""Write files (RASB, bvecs/bvals) to a given path."""
168-
if filetype.lower() == 'rasb':
226+
if filetype.lower() == "rasb":
169227
self.generate_rasb()
170-
np.savetxt(str(filename), self.gradients,
171-
delimiter='\t', header='\t'.join('RASB'),
172-
fmt=['%.8f'] * 3 + ['%g'])
173-
elif filetype.lower() == 'fsl':
228+
np.savetxt(
229+
str(filename),
230+
self.gradients,
231+
delimiter="\t",
232+
header="\t".join("RASB"),
233+
fmt=["%.8f"] * 3 + ["%g"],
234+
)
235+
elif filetype.lower() == "fsl":
174236
self.generate_vecval()
175-
np.savetxt('%s.bvec' % filename, self.bvecs.T, fmt='%.6f')
176-
np.savetxt('%s.bval' % filename, self.bvals, fmt='%.6f')
237+
np.savetxt("%s.bvec" % filename, self.bvecs.T, fmt="%.6f")
238+
np.savetxt("%s.bval" % filename, self.bvals, fmt="%.6f")
177239
else:
178240
raise ValueError('Unknown filetype "%s"' % filetype)
179-
241+
180242

181243
def normalize_gradients(bvecs, bvals, b0_threshold=B0_THRESHOLD,
182244
bvec_norm_epsilon=BVEC_NORM_EPSILON, b_scale=True):
@@ -375,32 +437,3 @@ def bvecs2ras(affine, bvecs, norm=True, bvec_norm_epsilon=0.2):
375437
rotated_bvecs[~b0s] /= norms_bvecs[~b0s, np.newaxis]
376438
rotated_bvecs[b0s] = np.zeros(3)
377439
return rotated_bvecs
378-
379-
380-
def reorient_bvecs_from_ras_b(ras_b, affines):
381-
"""
382-
Reorient the vectors from a rasb .tsv file.
383-
When correcting for motion, rotation of the diffusion-weighted volumes
384-
might cause systematic bias in rotationally invariant measures, such as FA
385-
and MD, and also cause characteristic biases in tractography, unless the
386-
gradient directions are appropriately reoriented to compensate for this
387-
effect [Leemans2009]_.
388-
389-
Parameters
390-
----------
391-
rasb_file : str or os.pathlike
392-
File path to a RAS-B gradient table. If rasb_file is provided,
393-
then bvecs and bvals will be dismissed.
394-
395-
affines : list or ndarray of shape (n, 4, 4) or (n, 3, 3)
396-
Each entry in this list or array contain either an affine
397-
transformation (4,4) or a rotation matrix (3, 3).
398-
In both cases, the transformations encode the rotation that was applied
399-
to the image corresponding to one of the non-zero gradient directions.
400-
"""
401-
from dipy.core.gradients import gradient_table_from_bvals_bvecs, reorient_bvecs
402-
403-
ras_b_mat = np.genfromtxt(ras_b, delimiter='\t')
404-
gt = gradient_table_from_bvals_bvecs(ras_b_mat[:,3], ras_b_mat[:,0:3], b0_threshold=50)
405-
406-
return reorient_bvecs(gt, affines)

0 commit comments

Comments
 (0)