Skip to content

Commit 68a7cd4

Browse files
authored
Merge pull request #3529 from l-espana/iss3345
[ENH] Issue 3345: Adding FreeSurfer longitudinal interfaces
2 parents a9e25e1 + 78d580b commit 68a7cd4

File tree

4 files changed

+167
-17
lines changed

4 files changed

+167
-17
lines changed

.zenodo.json

+5
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,11 @@
379379
{
380380
"name": "Schwartz, Yannick"
381381
},
382+
{
383+
"affiliation": "Medical College of Wisconsin",
384+
"name": "Espana, Lezlie",
385+
"orcid": "0000-0002-6466-4653"
386+
},
382387
{
383388
"affiliation": "The University of Iowa",
384389
"name": "Ghayoor, Ali",

nipype/interfaces/freesurfer/longitudinal.py

+19-2
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,25 @@
66
import os
77

88
from ... import logging
9-
from ..base import TraitedSpec, File, traits, InputMultiPath, OutputMultiPath, isdefined
10-
from .base import FSCommand, FSTraitedSpec, FSCommandOpenMP, FSTraitedSpecOpenMP
9+
from ..base import (
10+
TraitedSpec,
11+
File,
12+
traits,
13+
InputMultiPath,
14+
OutputMultiPath,
15+
isdefined,
16+
InputMultiObject,
17+
Directory,
18+
)
19+
from .base import (
20+
FSCommand,
21+
FSTraitedSpec,
22+
FSCommandOpenMP,
23+
FSTraitedSpecOpenMP,
24+
CommandLine,
25+
)
26+
from .preprocess import ReconAllInputSpec
27+
from ..io import FreeSurferSource
1128

1229
__docformat__ = "restructuredtext"
1330
iflogger = logging.getLogger("nipype.interface")

nipype/interfaces/freesurfer/preprocess.py

+116-14
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
CommandLine,
2626
CommandLineInputSpec,
2727
isdefined,
28+
InputMultiObject,
2829
)
2930
from .base import FSCommand, FSTraitedSpec, FSTraitedSpecOpenMP, FSCommandOpenMP, Info
3031
from .utils import copy2subjdir
@@ -816,7 +817,10 @@ def _gen_filename(self, name):
816817

817818
class ReconAllInputSpec(CommandLineInputSpec):
818819
subject_id = traits.Str(
819-
"recon_all", argstr="-subjid %s", desc="subject name", usedefault=True
820+
"recon_all",
821+
argstr="-subjid %s",
822+
desc="subject name",
823+
xor=["base_template_id", "longitudinal_timepoint_id"],
820824
)
821825
directive = traits.Enum(
822826
"all",
@@ -842,21 +846,32 @@ class ReconAllInputSpec(CommandLineInputSpec):
842846
usedefault=True,
843847
position=0,
844848
)
845-
hemi = traits.Enum("lh", "rh", desc="hemisphere to process", argstr="-hemi %s")
849+
hemi = traits.Enum(
850+
"lh",
851+
"rh",
852+
desc="hemisphere to process",
853+
argstr="-hemi %s",
854+
requires=["subject_id"],
855+
)
846856
T1_files = InputMultiPath(
847-
File(exists=True), argstr="-i %s...", desc="name of T1 file to process"
857+
File(exists=True),
858+
argstr="-i %s...",
859+
desc="name of T1 file to process",
860+
requires=["subject_id"],
848861
)
849862
T2_file = File(
850863
exists=True,
851864
argstr="-T2 %s",
852865
min_ver="5.3.0",
853866
desc="Convert T2 image to orig directory",
867+
requires=["subject_id"],
854868
)
855869
FLAIR_file = File(
856870
exists=True,
857871
argstr="-FLAIR %s",
858872
min_ver="5.3.0",
859873
desc="Convert FLAIR image to orig directory",
874+
requires=["subject_id"],
860875
)
861876
use_T2 = traits.Bool(
862877
argstr="-T2pial",
@@ -885,18 +900,22 @@ class ReconAllInputSpec(CommandLineInputSpec):
885900
"Assume scan parameters are MGH MP-RAGE "
886901
"protocol, which produces darker gray matter"
887902
),
903+
requires=["subject_id"],
888904
)
889905
big_ventricles = traits.Bool(
890906
argstr="-bigventricles",
891907
desc=("For use in subjects with enlarged ventricles"),
892908
)
893909
brainstem = traits.Bool(
894-
argstr="-brainstem-structures", desc="Segment brainstem structures"
910+
argstr="-brainstem-structures",
911+
desc="Segment brainstem structures",
912+
requires=["subject_id"],
895913
)
896914
hippocampal_subfields_T1 = traits.Bool(
897915
argstr="-hippocampal-subfields-T1",
898916
min_ver="6.0.0",
899917
desc="segment hippocampal subfields using input T1 scan",
918+
requires=["subject_id"],
900919
)
901920
hippocampal_subfields_T2 = traits.Tuple(
902921
File(exists=True),
@@ -907,6 +926,7 @@ class ReconAllInputSpec(CommandLineInputSpec):
907926
"segment hippocampal subfields using T2 scan, identified by "
908927
"ID (may be combined with hippocampal_subfields_T1)"
909928
),
929+
requires=["subject_id"],
910930
)
911931
expert = File(
912932
exists=True, argstr="-expert %s", desc="Set parameters using expert file"
@@ -927,6 +947,29 @@ class ReconAllInputSpec(CommandLineInputSpec):
927947
)
928948
flags = InputMultiPath(traits.Str, argstr="%s", desc="additional parameters")
929949

950+
# Longitudinal runs
951+
base_template_id = traits.Str(
952+
argstr="-base %s",
953+
desc="base template id",
954+
xor=["subject_id", "longitudinal_timepoint_id"],
955+
requires=["base_timepoint_ids"],
956+
)
957+
base_timepoint_ids = InputMultiObject(
958+
traits.Str(),
959+
argstr="-base-tp %s...",
960+
desc="processed timepoint to use in template",
961+
)
962+
longitudinal_timepoint_id = traits.Str(
963+
argstr="-long %s",
964+
desc="longitudinal session/timepoint id",
965+
xor=["subject_id", "base_template_id"],
966+
requires=["longitudinal_template_id"],
967+
position=1,
968+
)
969+
longitudinal_template_id = traits.Str(
970+
argstr="%s", desc="longitudinal base template id", position=2
971+
)
972+
930973
# Expert options
931974
talairach = traits.Str(desc="Flags to pass to talairach commands", xor=["expert"])
932975
mri_normalize = traits.Str(
@@ -1019,7 +1062,7 @@ class ReconAll(CommandLine):
10191062
>>> reconall.inputs.subject_id = 'foo'
10201063
>>> reconall.inputs.directive = 'all'
10211064
>>> reconall.inputs.subjects_dir = '.'
1022-
>>> reconall.inputs.T1_files = 'structural.nii'
1065+
>>> reconall.inputs.T1_files = ['structural.nii']
10231066
>>> reconall.cmdline
10241067
'recon-all -all -i structural.nii -subjid foo -sd .'
10251068
>>> reconall.inputs.flags = "-qcache"
@@ -1049,7 +1092,7 @@ class ReconAll(CommandLine):
10491092
>>> reconall_subfields.inputs.subject_id = 'foo'
10501093
>>> reconall_subfields.inputs.directive = 'all'
10511094
>>> reconall_subfields.inputs.subjects_dir = '.'
1052-
>>> reconall_subfields.inputs.T1_files = 'structural.nii'
1095+
>>> reconall_subfields.inputs.T1_files = ['structural.nii']
10531096
>>> reconall_subfields.inputs.hippocampal_subfields_T1 = True
10541097
>>> reconall_subfields.cmdline
10551098
'recon-all -all -i structural.nii -hippocampal-subfields-T1 -subjid foo -sd .'
@@ -1060,6 +1103,24 @@ class ReconAll(CommandLine):
10601103
>>> reconall_subfields.inputs.hippocampal_subfields_T1 = False
10611104
>>> reconall_subfields.cmdline
10621105
'recon-all -all -i structural.nii -hippocampal-subfields-T2 structural.nii test -subjid foo -sd .'
1106+
1107+
Base template creation for longitudinal pipeline:
1108+
>>> baserecon = ReconAll()
1109+
>>> baserecon.inputs.base_template_id = 'sub-template'
1110+
>>> baserecon.inputs.base_timepoint_ids = ['ses-1','ses-2']
1111+
>>> baserecon.inputs.directive = 'all'
1112+
>>> baserecon.inputs.subjects_dir = '.'
1113+
>>> baserecon.cmdline
1114+
'recon-all -all -base sub-template -base-tp ses-1 -base-tp ses-2 -sd .'
1115+
1116+
Longitudinal timepoint run:
1117+
>>> longrecon = ReconAll()
1118+
>>> longrecon.inputs.longitudinal_timepoint_id = 'ses-1'
1119+
>>> longrecon.inputs.longitudinal_template_id = 'sub-template'
1120+
>>> longrecon.inputs.directive = 'all'
1121+
>>> longrecon.inputs.subjects_dir = '.'
1122+
>>> longrecon.cmdline
1123+
'recon-all -all -long ses-1 sub-template -sd .'
10631124
"""
10641125

10651126
_cmd = "recon-all"
@@ -1523,21 +1584,62 @@ def _list_outputs(self):
15231584

15241585
outputs = self._outputs().get()
15251586

1526-
outputs.update(
1527-
FreeSurferSource(
1528-
subject_id=self.inputs.subject_id, subjects_dir=subjects_dir, hemi=hemi
1529-
)._list_outputs()
1530-
)
1531-
outputs["subject_id"] = self.inputs.subject_id
1587+
# If using longitudinal pipeline, update subject id accordingly,
1588+
# otherwise use original/default subject_id
1589+
if isdefined(self.inputs.base_template_id):
1590+
outputs.update(
1591+
FreeSurferSource(
1592+
subject_id=self.inputs.base_template_id,
1593+
subjects_dir=subjects_dir,
1594+
hemi=hemi,
1595+
)._list_outputs()
1596+
)
1597+
outputs["subject_id"] = self.inputs.base_template_id
1598+
elif isdefined(self.inputs.longitudinal_timepoint_id):
1599+
subject_id = f"{self.inputs.longitudinal_timepoint_id}.long.{self.inputs.longitudinal_template_id}"
1600+
outputs.update(
1601+
FreeSurferSource(
1602+
subject_id=subject_id, subjects_dir=subjects_dir, hemi=hemi
1603+
)._list_outputs()
1604+
)
1605+
outputs["subject_id"] = subject_id
1606+
else:
1607+
outputs.update(
1608+
FreeSurferSource(
1609+
subject_id=self.inputs.subject_id,
1610+
subjects_dir=subjects_dir,
1611+
hemi=hemi,
1612+
)._list_outputs()
1613+
)
1614+
outputs["subject_id"] = self.inputs.subject_id
1615+
15321616
outputs["subjects_dir"] = subjects_dir
15331617
return outputs
15341618

15351619
def _is_resuming(self):
15361620
subjects_dir = self.inputs.subjects_dir
15371621
if not isdefined(subjects_dir):
15381622
subjects_dir = self._gen_subjects_dir()
1539-
if os.path.isdir(os.path.join(subjects_dir, self.inputs.subject_id, "mri")):
1540-
return True
1623+
1624+
# Check for longitudinal pipeline
1625+
if not isdefined(self.inputs.subject_id):
1626+
if isdefined(self.inputs.base_template_id):
1627+
if os.path.isdir(
1628+
os.path.join(subjects_dir, self.inputs.base_template_id, "mri")
1629+
):
1630+
return True
1631+
elif isdefined(self.inputs.longitudinal_template_id):
1632+
if os.path.isdir(
1633+
os.path.join(
1634+
subjects_dir,
1635+
f"{self.inputs.longitudinal_timepoint_id}.long.{self.inputs.longitudinal_template_id}",
1636+
"mri",
1637+
)
1638+
):
1639+
return True
1640+
else:
1641+
if os.path.isdir(os.path.join(subjects_dir, self.inputs.subject_id, "mri")):
1642+
return True
15411643
return False
15421644

15431645
def _format_arg(self, name, trait_spec, value):

nipype/interfaces/freesurfer/tests/test_auto_ReconAll.py

+27-1
Original file line numberDiff line numberDiff line change
@@ -8,23 +8,35 @@ def test_ReconAll_inputs():
88
argstr="-FLAIR %s",
99
extensions=None,
1010
min_ver="5.3.0",
11+
requires=["subject_id"],
1112
),
1213
T1_files=dict(
1314
argstr="-i %s...",
15+
requires=["subject_id"],
1416
),
1517
T2_file=dict(
1618
argstr="-T2 %s",
1719
extensions=None,
1820
min_ver="5.3.0",
21+
requires=["subject_id"],
1922
),
2023
args=dict(
2124
argstr="%s",
2225
),
26+
base_template_id=dict(
27+
argstr="-base %s",
28+
requires=["base_timepoint_ids"],
29+
xor=["subject_id", "longitudinal_timepoint_id"],
30+
),
31+
base_timepoint_ids=dict(
32+
argstr="-base-tp %s...",
33+
),
2334
big_ventricles=dict(
2435
argstr="-bigventricles",
2536
),
2637
brainstem=dict(
2738
argstr="-brainstem-structures",
39+
requires=["subject_id"],
2840
),
2941
directive=dict(
3042
argstr="-%s",
@@ -44,21 +56,35 @@ def test_ReconAll_inputs():
4456
),
4557
hemi=dict(
4658
argstr="-hemi %s",
59+
requires=["subject_id"],
4760
),
4861
hippocampal_subfields_T1=dict(
4962
argstr="-hippocampal-subfields-T1",
5063
min_ver="6.0.0",
64+
requires=["subject_id"],
5165
),
5266
hippocampal_subfields_T2=dict(
5367
argstr="-hippocampal-subfields-T2 %s %s",
5468
min_ver="6.0.0",
69+
requires=["subject_id"],
5570
),
5671
hires=dict(
5772
argstr="-hires",
5873
min_ver="6.0.0",
5974
),
75+
longitudinal_template_id=dict(
76+
argstr="%s",
77+
position=2,
78+
),
79+
longitudinal_timepoint_id=dict(
80+
argstr="-long %s",
81+
position=1,
82+
requires=["longitudinal_template_id"],
83+
xor=["subject_id", "base_template_id"],
84+
),
6085
mprage=dict(
6186
argstr="-mprage",
87+
requires=["subject_id"],
6288
),
6389
mri_aparc2aseg=dict(
6490
xor=["expert"],
@@ -143,7 +169,7 @@ def test_ReconAll_inputs():
143169
),
144170
subject_id=dict(
145171
argstr="-subjid %s",
146-
usedefault=True,
172+
xor=["base_template_id", "longitudinal_timepoint_id"],
147173
),
148174
subjects_dir=dict(
149175
argstr="-sd %s",

0 commit comments

Comments
 (0)