Skip to content

Commit 5f82c3b

Browse files
committed
RF: Pull out pedir reorientation into a tested routine
1 parent a4c7720 commit 5f82c3b

File tree

3 files changed

+66
-33
lines changed

3 files changed

+66
-33
lines changed

sdcflows/interfaces/bspline.py

Lines changed: 7 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
)
4242

4343
from sdcflows.transform import grid_bspline_weights
44+
from sdcflows.utils.tools import reorient_pedir
4445

4546

4647
LOW_MEM_BLOCK_SIZE = 1000
@@ -522,43 +523,27 @@ def _fix_topup_fieldcoeff(in_coeff, fmap_ref, pe_dir, out_file=None):
522523
if out_file is None:
523524
out_file = Path("coefficients.nii.gz").absolute()
524525

525-
# Load reference image
526526
refnii = nb.load(fmap_ref)
527-
528-
# Load coefficients
529527
coeffnii = nb.load(in_coeff)
530528

531-
# Orientations are [[axis, flip], [axis, flip], [axis, flip]] arrays,
532-
# where RAS is "no change"
533-
# e.g. PSL == [[1, -1], [2, 1], [0, -1]]
529+
# Coefficients are in LAS space, and we will convert to RAS.
530+
# Reorient the reference image and phase-encoding direction to RAS
534531
ref_ornt = nb.io_orientation(refnii.affine)
535-
coeff_ornt = nb.io_orientation(coeffnii.affine)
536-
# Get ref_ornt relative to coeff_ornt instead of relative to RAS
537-
ref2coeff = nb.orientations.ornt_transform(ref_ornt, coeff_ornt).astype(int)
538-
539-
# Find the axis index and flip of PE direction in reference space
540-
ref_pe_axis = "ijk".find(pe_dir[0])
541-
if ref_pe_axis == -1:
542-
ref_pe_axis = "xyz".index(pe_dir[0])
543-
544-
# Transform to coefficient space
545-
coeff_pe_axis = "ijk"[ref2coeff[ref_pe_axis, 0]]
532+
refnii_ras = refnii.as_reoriented(ref_ornt)
533+
coeff_pe_dir = reorient_pedir(pe_dir, ref_ornt)
546534

547535
# Coefficients - flip LR and overwrite coeffnii variable
548536
# Internal data orientation of FSL is LAS, so coefficients will be LR flipped,
549537
# and because the affine does not encode orientation (factors instead), this flip
550538
# always is implicit.
551539
# If the PE direction is x/i, the flip in the axis direction causes that the
552540
# fieldmap estimation must also be inverted in direction (multiply by -1.0)
553-
reverse_pe = -1.0 if coeff_pe_axis == "i" else 1.0
554-
lr_axis = np.nonzero(coeff_ornt[:, 0] == 0)[0]
541+
reverse_pe = -1.0 if coeff_pe_dir[0] == "i" else 1.0
555542
coeffnii = coeffnii.__class__(
556-
reverse_pe * np.flip(np.asanyarray(coeffnii.dataobj), axis=lr_axis),
543+
reverse_pe * np.flip(np.asanyarray(coeffnii.dataobj), axis=0),
557544
coeffnii.affine,
558545
coeffnii.header,
559546
)
560-
# Ensure reference has positive director cosines
561-
refnii_ras = nb.as_closest_canonical(refnii)
562547

563548
# Get matrix of B-Spline control knots
564549
coeff_shape = np.array(coeffnii.shape[:3])

sdcflows/interfaces/utils.py

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@
3838
)
3939
from nipype.interfaces.mixins import CopyHeaderInterface as _CopyHeaderInterface
4040

41+
from sdcflows.utils.tools import reorient_pedir
42+
4143
OBLIQUE_THRESHOLD_DEG = 0.5
4244

4345

@@ -202,17 +204,7 @@ def _run_interface(self, runtime):
202204

203205
reoriented = img.as_reoriented(img2target)
204206

205-
pe_dirs = []
206-
for pe_dir in self.inputs.pe_dir:
207-
directions = "ijk" if pe_dir[0] in "ijk" else "xyz"
208-
209-
orig_axis = directions.index(pe_dir[0])
210-
orig_flip = pe_dir[1:] == "-"
211-
212-
reoriented_axis = directions[img2target[orig_axis, 0]]
213-
reoriented_flip = orig_flip ^ (img2target[orig_axis, 1] == -1)
214-
215-
pe_dirs.append(reoriented_axis + ("-" if reoriented_flip else ""))
207+
pe_dirs = [reorient_pedir(pe_dir, img2target) for pe_dir in self.inputs.pe_dir]
216208

217209
self._results = dict(
218210
out_file=fname_presuffix(

sdcflows/utils/tools.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,3 +154,59 @@ def brain_masker(in_file, out_file=None, padding=5):
154154
img.__class__(data, img.affine, img.header).to_filename(out_brain)
155155

156156
return str(out_brain), str(out_probseg), str(out_mask)
157+
158+
159+
def reorient_pedir(pe_dir, source_ornt=None, target_ornt=None):
160+
"""Return updated PhaseEncodingDirection to account for an image data array rotation
161+
162+
This function can accept one or two orientations.
163+
Passing only a source orientation will rotate into RAS:
164+
165+
>>> from nibabel.orientations import axcodes2ornt
166+
>>> reorient_pedir("j", axcodes2ornt("RAS"))
167+
'j'
168+
>>> reorient_pedir("i", axcodes2ornt("PSL"))
169+
'j-'
170+
171+
Passing only a target_ornt will rotate from RAS:
172+
173+
>>> reorient_pedir("j", target_ornt=axcodes2ornt("RAS"))
174+
'j'
175+
>>> reorient_pedir("i", target_ornt=axcodes2ornt("PSL"))
176+
'k-'
177+
178+
Passing both will rotate from source to target.
179+
180+
>>> reorient_pedir("j", axcodes2ornt("LPS"), axcodes2ornt("AIR"))
181+
'i-'
182+
183+
Passing a transform orientation as source_ornt will perform the transform,
184+
and passing it as ``target_ornt`` will invert the transform:
185+
186+
>>> from nibabel.orientations import ornt_transform
187+
>>> xfm = ornt_transform(axcodes2ornt("LPS"), axcodes2ornt("AIR"))
188+
>>> reorient_pedir("j", xfm)
189+
'i-'
190+
>>> reorient_pedir("j", target_ornt=xfm)
191+
'k-'
192+
"""
193+
from nibabel.orientations import ornt_transform
194+
195+
if source_ornt is None:
196+
source_ornt = [[0, 1], [1, 1], [2, 1]]
197+
if target_ornt is None:
198+
target_ornt = [[0, 1], [1, 1], [2, 1]]
199+
200+
xfm = ornt_transform(source_ornt, target_ornt).astype(int) # shape: (3, 2)
201+
202+
directions = "ijk" if pe_dir[0] in "ijk" else "xyz"
203+
204+
source_axis = directions.index(pe_dir[0])
205+
source_flip = pe_dir[1:] == "-"
206+
207+
axis_xfm = xfm[source_axis, :] # shape: (2,)
208+
209+
target_axis = directions[axis_xfm[0]]
210+
target_flip = source_flip ^ (axis_xfm[1] == -1)
211+
212+
return f"{target_axis}-" if target_flip else target_axis

0 commit comments

Comments
 (0)