Skip to content

Commit 124fde1

Browse files
authored
Merge pull request #2690 from effigies/bp/2302
FIX: Clarify phase encoding direction, rather than axis
2 parents 8d75e73 + bc56706 commit 124fde1

File tree

4 files changed

+64
-7
lines changed

4 files changed

+64
-7
lines changed

fmriprep/interfaces/reports.py

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import os
66
import time
77
import re
8+
import logging
89

910
from collections import Counter
1011
from nipype.interfaces.base import (
@@ -14,6 +15,8 @@
1415
from smriprep.interfaces.freesurfer import ReconAll
1516

1617

18+
LOGGER = logging.getLogger('nipype.interface')
19+
1720
SUBJECT_TEMPLATE = """\
1821
\t<ul class="elem-desc">
1922
\t\t<li>Subject ID: {subject_id}</li>
@@ -30,6 +33,7 @@
3033
\t\t<details open>
3134
\t\t<summary>Summary</summary>
3235
\t\t<ul class="elem-desc">
36+
\t\t\t<li>Original orientation: {ornt}</li>
3337
\t\t\t<li>Repetition time (TR): {tr:.03g}s</li>
3438
\t\t\t<li>Phase-encoding (PE) direction: {pedir}</li>
3539
\t\t\t<li>{multiecho}</li>
@@ -158,7 +162,7 @@ class FunctionalSummaryInputSpec(BaseInterfaceInputSpec):
158162
desc='Slice timing correction used')
159163
distortion_correction = traits.Str(desc='Susceptibility distortion correction method',
160164
mandatory=True)
161-
pe_direction = traits.Enum(None, 'i', 'i-', 'j', 'j-', mandatory=True,
165+
pe_direction = traits.Enum(None, 'i', 'i-', 'j', 'j-', 'k', 'k-', mandatory=True,
162166
desc='Phase-encoding direction detected')
163167
registration = traits.Enum('FSL', 'FreeSurfer', mandatory=True,
164168
desc='Functional/anatomical registration method')
@@ -173,6 +177,7 @@ class FunctionalSummaryInputSpec(BaseInterfaceInputSpec):
173177
dummy_scans = traits.Either(traits.Int(), None, desc='number of dummy scans specified by user')
174178
algo_dummy_scans = traits.Int(desc='number of dummy scans determined by algorithm')
175179
echo_idx = traits.List([], usedefault=True, desc="BIDS echo identifiers")
180+
orientation = traits.Str(mandatory=True, desc='Orientation of the voxel axes')
176181

177182

178183
class FunctionalSummary(SummaryInterface):
@@ -194,10 +199,8 @@ def _generate_segment(self):
194199
'(boundary-based registration, BBR) - %d dof' % dof,
195200
'FreeSurfer <code>mri_coreg</code> - %d dof' % dof],
196201
}[self.inputs.registration][self.inputs.fallback]
197-
if self.inputs.pe_direction is None:
198-
pedir = 'MISSING - Assuming Anterior-Posterior'
199-
else:
200-
pedir = {'i': 'Left-Right', 'j': 'Anterior-Posterior'}[self.inputs.pe_direction[0]]
202+
203+
pedir = get_world_pedir(self.inputs.orientation, self.inputs.pe_direction)
201204

202205
if isdefined(self.inputs.confounds_file):
203206
with open(self.inputs.confounds_file) as cfh:
@@ -233,7 +236,7 @@ def _generate_segment(self):
233236
return FUNCTIONAL_TEMPLATE.format(
234237
pedir=pedir, stc=stc, sdc=self.inputs.distortion_correction, registration=reg,
235238
confounds=re.sub(r'[\t ]+', ', ', conflist), tr=self.inputs.tr,
236-
dummy_scan_desc=dummy_scan_msg, multiecho=multiecho)
239+
dummy_scan_desc=dummy_scan_msg, multiecho=multiecho, ornt=self.inputs.orientation)
237240

238241

239242
class AboutSummaryInputSpec(BaseInterfaceInputSpec):
@@ -249,3 +252,27 @@ def _generate_segment(self):
249252
return ABOUT_TEMPLATE.format(version=self.inputs.version,
250253
command=self.inputs.command,
251254
date=time.strftime("%Y-%m-%d %H:%M:%S %z"))
255+
256+
257+
def get_world_pedir(ornt, pe_direction):
258+
"""Return world direction of phase encoding"""
259+
axes = (
260+
("Right", "Left"),
261+
("Anterior", "Posterior"),
262+
("Superior", "Inferior")
263+
)
264+
ax_idcs = {"i": 0, "j": 1, "k": 2}
265+
266+
if pe_direction is not None:
267+
axcode = ornt[ax_idcs[pe_direction[0]]]
268+
inv = pe_direction[1:] == "-"
269+
270+
for ax in axes:
271+
for flip in (ax, ax[::-1]):
272+
if flip[not inv].startswith(axcode):
273+
return "-".join(flip)
274+
LOGGER.warning(
275+
"Cannot determine world direction of phase encoding. "
276+
f"Orientation: {ornt}; PE dir: {pe_direction}"
277+
)
278+
return "Could not be determined - assuming Anterior-Posterior"

fmriprep/interfaces/tests/__init__.py

Whitespace-only changes.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import pytest
2+
3+
from ..reports import get_world_pedir
4+
5+
6+
@pytest.mark.parametrize("orientation,pe_dir,expected", [
7+
('RAS', 'j', 'Posterior-Anterior'),
8+
('RAS', 'j-', 'Anterior-Posterior'),
9+
('RAS', 'i', 'Left-Right'),
10+
('RAS', 'i-', 'Right-Left'),
11+
('RAS', 'k', 'Inferior-Superior'),
12+
('RAS', 'k-', 'Superior-Inferior'),
13+
('LAS', 'j', 'Posterior-Anterior'),
14+
('LAS', 'i-', 'Left-Right'),
15+
('LAS', 'k-', 'Superior-Inferior'),
16+
('LPI', 'j', 'Anterior-Posterior'),
17+
('LPI', 'i-', 'Left-Right'),
18+
('LPI', 'k-', 'Inferior-Superior'),
19+
])
20+
def test_get_world_pedir(tmpdir, orientation, pe_dir, expected):
21+
assert get_world_pedir(orientation, pe_dir) == expected

fmriprep/workflows/bold/base.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,8 @@ def init_func_preproc_wf(bold_file):
168168
# Take first file as reference
169169
ref_file = pop_file(bold_file)
170170
metadata = layout.get_metadata(ref_file)
171+
# get original image orientation
172+
ref_orientation = get_img_orientation(ref_file)
171173

172174
echo_idxs = listify(entities.get("echo", []))
173175
multiecho = len(echo_idxs) > 2
@@ -278,7 +280,8 @@ def init_func_preproc_wf(bold_file):
278280
registration_init=config.workflow.bold2t1w_init,
279281
pe_direction=metadata.get("PhaseEncodingDirection"),
280282
echo_idx=echo_idxs,
281-
tr=metadata["RepetitionTime"]),
283+
tr=metadata["RepetitionTime"],
284+
orientation=ref_orientation),
282285
name='summary', mem_gb=config.DEFAULT_MEMORY_MIN_GB, run_without_submitting=True)
283286
summary.inputs.dummy_scans = config.workflow.dummy_scans
284287

@@ -971,3 +974,9 @@ def _unique(inlist):
971974
return {
972975
k: _unique(v) for k, v in entities.items()
973976
}
977+
978+
979+
def get_img_orientation(imgf):
980+
"""Return the image orientation as a string"""
981+
img = nb.load(imgf)
982+
return ''.join(nb.aff2axcodes(img.affine))

0 commit comments

Comments
 (0)