12
12
class DiffusionGradientTable :
13
13
"""Data structure for DWI gradients."""
14
14
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
+ ):
20
38
"""
21
39
Create a new table of diffusion gradients.
22
40
@@ -34,8 +52,8 @@ def __init__(self, dwi_file=None, bvecs=None, bvals=None, rasb_file=None,
34
52
then bvecs and bvals will be dismissed.
35
53
b_scale : bool
36
54
Whether b-values should be normalized.
37
-
38
55
"""
56
+ self ._transforms = transforms
39
57
self ._b_scale = b_scale
40
58
self ._b0_thres = b0_threshold
41
59
self ._bvec_norm_epsilon = bvec_norm_epsilon
@@ -87,7 +105,7 @@ def affine(self, value):
87
105
dwi_file = nb .load (str (value ))
88
106
self ._affine = dwi_file .affine .copy ()
89
107
return
90
- if hasattr (value , ' affine' ):
108
+ if hasattr (value , " affine" ):
91
109
self ._affine = value .affine
92
110
self ._affine = np .array (value )
93
111
@@ -102,20 +120,20 @@ def bvecs(self, value):
102
120
if isinstance (value , (str , Path )):
103
121
value = np .loadtxt (str (value )).T
104
122
else :
105
- value = np .array (value , dtype = ' float32' )
123
+ value = np .array (value , dtype = " float32" )
106
124
107
125
# Correct any b0's in bvecs misstated as 10's.
108
126
value [np .any (abs (value ) >= 10 , axis = 1 )] = np .zeros (3 )
109
127
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" )
111
129
self ._bvecs = value
112
130
113
131
@bvals .setter
114
132
def bvals (self , value ):
115
133
if isinstance (value , (str , Path )):
116
134
value = np .loadtxt (str (value )).flatten ()
117
135
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" )
119
137
self ._bvals = np .array (value )
120
138
121
139
@property
@@ -129,10 +147,12 @@ def normalize(self):
129
147
return
130
148
131
149
self ._bvecs , self ._bvals = normalize_gradients (
132
- self .bvecs , self .bvals ,
150
+ self .bvecs ,
151
+ self .bvals ,
133
152
b0_threshold = self ._b0_thres ,
134
153
bvec_norm_epsilon = self ._bvec_norm_epsilon ,
135
- b_scale = self ._b_scale )
154
+ b_scale = self ._b_scale ,
155
+ )
136
156
self ._normalized = True
137
157
138
158
def generate_rasb (self ):
@@ -142,14 +162,50 @@ def generate_rasb(self):
142
162
_ras = bvecs2ras (self .affine , self .bvecs )
143
163
self .gradients = np .hstack ((_ras , self .bvals [..., np .newaxis ]))
144
164
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
+
145
198
def generate_vecval (self ):
146
199
"""Compose a bvec/bval pair in image coordinates."""
147
200
if self .bvecs is None or self .bvals is None :
148
201
if self .affine is None :
149
202
raise TypeError (
150
203
"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
+ )
153
209
self ._bvals = self .gradients [..., - 1 ].flatten ()
154
210
155
211
@property
@@ -161,22 +217,28 @@ def pole(self):
161
217
162
218
"""
163
219
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
+ )
165
223
166
- def to_filename (self , filename , filetype = ' rasb' ):
224
+ def to_filename (self , filename , filetype = " rasb" ):
167
225
"""Write files (RASB, bvecs/bvals) to a given path."""
168
- if filetype .lower () == ' rasb' :
226
+ if filetype .lower () == " rasb" :
169
227
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" :
174
236
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" )
177
239
else :
178
240
raise ValueError ('Unknown filetype "%s"' % filetype )
179
-
241
+
180
242
181
243
def normalize_gradients (bvecs , bvals , b0_threshold = B0_THRESHOLD ,
182
244
bvec_norm_epsilon = BVEC_NORM_EPSILON , b_scale = True ):
@@ -375,32 +437,3 @@ def bvecs2ras(affine, bvecs, norm=True, bvec_norm_epsilon=0.2):
375
437
rotated_bvecs [~ b0s ] /= norms_bvecs [~ b0s , np .newaxis ]
376
438
rotated_bvecs [b0s ] = np .zeros (3 )
377
439
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