Skip to content

Commit 2b45237

Browse files
committed
FIX: Generation of RAS displacements fields from VSMs
After much thought, I have come to the conclusion that the VOX2RAS affine must be applied in all cases, regardless of whether the dataset is oblique or not. After all, the VSM (voxel-shift-map) is no more than a regularly gridded field of voxel coordinates. The transformation between voxels and mm is biyective and univocal, and formalized by the affine matrix of the image. This PR simplifies the calculation, and theoretically should resolve most of the issues we are experiencing when resampling data. Resolves: #218. Resolves: #236. Related: nipreps/fmriprep#2210.
1 parent 4b464fa commit 2b45237

File tree

1 file changed

+18
-26
lines changed

1 file changed

+18
-26
lines changed

sdcflows/transform.py

Lines changed: 18 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -215,38 +215,30 @@ def to_displacements(self, ro_time, pe_dir):
215215
A NIfTI 1.0 object containing the distortion.
216216
217217
"""
218-
from math import pi
219-
from nibabel.affines import voxel_sizes, obliquity
220-
from nibabel.orientations import io_orientation
221-
222-
# Generate warp field
223-
data = self.shifts.get_fdata(dtype="float32").copy()
218+
# Set polarity & scale VSM (voxel-shift-map) by readout time
219+
vsm = self.shifts.get_fdata().copy()
224220
pe_axis = "ijk".index(pe_dir[0])
225-
pe_sign = -1.0 if pe_dir.endswith("-") else 1.0
226-
pe_size = self.shifts.header.get_zooms()[pe_axis]
227-
data *= pe_sign * ro_time * pe_size
221+
vsm *= -1.0 if pe_dir.endswith("-") else 1.0
222+
vsm *= ro_time
228223

229-
fieldshape = tuple(list(data.shape[:3]) + [3])
224+
# Shape of displacements field
225+
fieldshape = tuple(list(vsm.shape[:3]) + [3])
230226

231-
# Compose a vector field
232-
field = np.zeros((data.size, 3), dtype="float32")
233-
field[..., pe_axis] = data.reshape(-1)
227+
# Convert VSM to voxel displacements
228+
ijk_deltas = np.zeros((vsm.size, 3), dtype="float32")
229+
ijk_deltas[..., pe_axis] = vsm.reshape(-1)
234230

235-
# If coordinate system is oblique, project displacements through directions matrix
231+
# To convert from VSM to RAS field we just apply the affine
236232
aff = self.shifts.affine
237-
if obliquity(aff).max() * 180 / pi > 0.01:
238-
dirmat = np.eye(4)
239-
dirmat[:3, :3] = aff[:3, :3] / (
240-
voxel_sizes(aff) * io_orientation(aff)[:, 1]
241-
)
242-
field = nb.affines.apply_affine(dirmat, field)
243-
244-
warpnii = nb.Nifti1Image(
245-
field.reshape(fieldshape)[:, :, :, np.newaxis, :], aff, None
233+
xyz_deltas = nb.affines.apply_affine(aff, ijk_deltas)
234+
xyz_nii = nb.Nifti1Image(
235+
# WARNING: ITK NIfTI fields are 5D (have an empty 4th dim).
236+
# Hence, the ``np.newaxis``.
237+
xyz_deltas.reshape(fieldshape)[:, :, :, np.newaxis, :], aff, None
246238
)
247-
warpnii.header.set_intent("vector", (), "")
248-
warpnii.header.set_xyzt_units("mm")
249-
return warpnii
239+
xyz_nii.header.set_intent("vector", (), "")
240+
xyz_nii.header.set_xyzt_units("mm")
241+
return xyz_nii
250242

251243

252244
def _cubic_bspline(d):

0 commit comments

Comments
 (0)