1
1
"""Prepare anatomical images for processing."""
2
2
from nipype .interfaces import utility as niu
3
3
from nipype .pipeline import engine as pe
4
+ from niworkflows .engine .workflows import LiterateWorkflow
4
5
5
6
6
- def init_anat_average_wf (
7
+ def init_anat_template_wf (
7
8
* ,
8
- bspline_fitting_distance = 200 ,
9
- longitudinal = False ,
10
- name = "anat_average_wf" ,
11
- num_maps = 1 ,
12
- omp_nthreads = None ,
13
- sloppy = False ,
14
- ):
9
+ contrast : str ,
10
+ num_files : int ,
11
+ omp_nthreads : int ,
12
+ longitudinal : bool = False ,
13
+ bspline_fitting_distance : int = 200 ,
14
+ sloppy : bool = False ,
15
+ name : str = "anat_template_wf" ,
16
+ ) -> LiterateWorkflow :
15
17
"""
16
- Create an average from several images of the same modality.
17
-
18
- Each image undergoes a clipping step, removing background noise and
19
- high-intensity outliers, which is required by INU correction with the
20
- N4 algorithm.
21
- Then INU correction is performed for each of the inputs and the range
22
- of the image clipped again to fit within uint8.
23
- Finally, each image is reoriented to have RAS+ data matrix and, if
24
- more than one inputs, aligned and averaged with FreeSurfer's
25
- ``mri_robust_template``.
26
-
18
+ Generate a canonically-oriented, structural average from all input images.
19
+ Workflow Graph
20
+ .. workflow::
21
+ :graph2use: orig
22
+ :simple_form: yes
23
+ from nibabies.workflows.anatomical.template import init_anat_template_wf
24
+ wf = init_anat_template_wf(
25
+ longitudinal=False, omp_nthreads=1, num_files=1, contrast="T1w"
26
+ )
27
27
Parameters
28
28
----------
29
- bspline_fitting_distance : :obj:`float`
30
- Distance in mm between B-Spline control points for N4 INU estimation.
29
+ contrast : :obj:`str`
30
+ Name of contrast, for reporting purposes, e.g., T1w, T2w, PDw
31
+ num_files : :obj:`int`
32
+ Number of images
31
33
longitudinal : :obj:`bool`
32
- Whether an unbiased middle point should be calculated.
33
- name : :obj:`str`
34
- This particular workflow's unique name (Nipype requirement).
35
- num_maps : :obj:`int`
36
- Then number of input 3D volumes to be averaged.
34
+ Create unbiased structural average, regardless of number of inputs
35
+ (may increase runtime)
37
36
omp_nthreads : :obj:`int`
38
- The number of threads for individual processes in this workflow.
37
+ Maximum number of threads an individual process may use
38
+ bspline_fitting_distance : :obj:`float`
39
+ Distance in mm between B-Spline control points for N4 INU estimation.
39
40
sloppy : :obj:`bool`
40
41
Run in *sloppy* mode.
41
-
42
+ name : :obj:`str`, optional
43
+ Workflow name (default: anat_template_wf)
42
44
Inputs
43
45
------
44
- in_files : :obj:`list`
45
- A list of one or more input files. They can be 3D or 4D.
46
-
46
+ anat_files
47
+ List of structural images
47
48
Outputs
48
49
-------
49
- out_file : :obj:`str`
50
- The output averaged reference file.
51
- valid_list : :obj:`list`
52
- A list of accepted/discarded volumes from the input list.
53
- realign_xfms : :obj:`list`
54
- List of rigid-body transformation matrices that bring every volume
55
- into alignment with the average reference.
56
- out_report : :obj:`str`
57
- Path to a reportlet summarizing what happened in this workflow.
58
-
50
+ anat_ref
51
+ Structural reference averaging input images
52
+ anat_valid_list
53
+ List of structural images accepted for combination
54
+ anat_realign_xfm
55
+ List of affine transforms to realign input images to final reference
56
+ out_report
57
+ Conformation report
59
58
"""
60
59
from nipype .interfaces .ants import N4BiasFieldCorrection
61
60
from nipype .interfaces .image import Reorient
62
- from niworkflows .engine .workflows import LiterateWorkflow as Workflow
63
61
from niworkflows .interfaces .freesurfer import PatchedLTAConvert as LTAConvert
64
62
from niworkflows .interfaces .freesurfer import StructuralReference
65
- from niworkflows .interfaces .header import ValidateImage
66
63
from niworkflows .interfaces .images import Conform , TemplateDimensions
67
- from niworkflows .interfaces .nibabel import IntensityClip , SplitSeries
64
+ from niworkflows .interfaces .nibabel import IntensityClip
68
65
from niworkflows .interfaces .nitransforms import ConcatenateXFMs
69
66
from niworkflows .utils .misc import add_suffix
70
- from pkg_resources import resource_filename as pkgr
71
67
72
- wf = Workflow (name = name )
68
+ from nibabies .utils .misc import get_file
69
+
70
+ wf = LiterateWorkflow (name = name )
71
+ if num_files > 1 :
72
+ import nipype .interfaces .freesurfer as fs
73
73
74
- inputnode = pe .Node (niu .IdentityInterface (fields = ["in_files" ]), name = "inputnode" )
74
+ fs_ver = fs .Info ().looseversion () or "<ver>"
75
+ wf .__desc__ = f"""\
76
+ An anatomical { contrast } -reference map was computed after registration of
77
+ { num_files } { contrast } images (after INU-correction) using
78
+ `mri_robust_template` [FreeSurfer { fs_ver } , @fs_template].
79
+ """
80
+
81
+ inputnode = pe .Node (niu .IdentityInterface (fields = ["anat_files" ]), name = "inputnode" )
75
82
outputnode = pe .Node (
76
83
niu .IdentityInterface (fields = ["out_file" , "valid_list" , "realign_xfms" , "out_report" ]),
77
84
name = "outputnode" ,
78
85
)
79
86
80
- # 1. Validate each of the input images
81
- validate = pe .MapNode (
82
- ValidateImage (),
83
- iterfield = "in_file" ,
84
- name = "validate" ,
85
- run_without_submitting = True ,
86
- )
87
-
88
- # 2. Ensure we don't have two timepoints and implicitly squeeze image
89
- split = pe .MapNode (SplitSeries (), iterfield = "in_file" , name = "split" )
87
+ # 0. Reorient anatomical image(s) to RAS and resample to common voxel space
88
+ anat_ref_dimensions = pe .Node (TemplateDimensions (), name = "anat_ref_dimensions" )
89
+ anat_conform = pe .MapNode (Conform (), iterfield = "in_file" , name = "anat_conform" )
90
90
91
- # 3. INU correction of all independent volumes
92
- clip_preinu = pe .MapNode (IntensityClip (p_min = 50 ), iterfield = "in_file" , name = "clip_preinu" )
93
- correct_inu = pe .MapNode (
94
- N4BiasFieldCorrection (
95
- dimension = 3 ,
96
- save_bias = False ,
97
- copy_header = True ,
98
- n_iterations = [50 ] * (5 - 2 * sloppy ),
99
- convergence_threshold = 1e-7 ,
100
- shrink_factor = 4 ,
101
- bspline_fitting_distance = bspline_fitting_distance ,
102
- ),
103
- iterfield = "input_image" ,
104
- n_procs = omp_nthreads ,
105
- name = "correct_inu" ,
106
- )
107
- clip_postinu = pe .MapNode (
108
- IntensityClip (p_min = 10.0 , p_max = 99.5 ), iterfield = "in_file" , name = "clip_postinu"
109
- )
110
-
111
- # 4. Reorient T2w image(s) to RAS and resample to common voxel space
112
- ref_dimensions = pe .Node (TemplateDimensions (), name = "ref_dimensions" )
113
- conform = pe .MapNode (Conform (), iterfield = "in_file" , name = "conform" )
114
91
# fmt:off
115
92
wf .connect ([
116
- (inputnode , ref_dimensions , [("in_files" , "t1w_list" )]),
117
- (inputnode , validate , [("in_files" , "in_file" )]),
118
- (validate , split , [("out_file" , "in_file" )]),
119
- (split , clip_preinu , [(("out_files" , _flatten ), "in_file" )]),
120
- (clip_preinu , correct_inu , [("out_file" , "input_image" )]),
121
- (correct_inu , clip_postinu , [("output_image" , "in_file" )]),
122
- (ref_dimensions , conform , [
123
- ("t1w_valid_list" , "in_file" ),
124
- ("target_zooms" , "target_zooms" ),
125
- ("target_shape" , "target_shape" )]),
126
- (ref_dimensions , outputnode , [("out_report" , "out_report" ),
127
- ("t1w_valid_list" , "valid_list" )]),
93
+ (inputnode , anat_ref_dimensions , [('anat_files' , 't1w_list' )]),
94
+ (anat_ref_dimensions , anat_conform , [
95
+ ('t1w_valid_list' , 'in_file' ),
96
+ ('target_zooms' , 'target_zooms' ),
97
+ ('target_shape' , 'target_shape' )]),
98
+ (anat_ref_dimensions , outputnode , [('out_report' , 'out_report' ),
99
+ ('t1w_valid_list' , 'anat_valid_list' )]),
128
100
])
129
101
# fmt:on
130
102
131
- # 5. Reorient template to RAS, if needed (mri_robust_template may set to LIA)
132
- ensure_ras = pe .Node (Reorient (), name = "ensure_ras" )
133
-
134
- if num_maps == 1 :
103
+ if num_files == 1 :
135
104
get1st = pe .Node (niu .Select (index = [0 ]), name = "get1st" )
136
- outputnode .inputs .realign_xfms = [pkgr ("smriprep" , "data/itkIdentityTransform.txt" )]
105
+ outputnode .inputs .anat_realign_xfm = [
106
+ get_file ("smriprep" , "data/itkIdentityTransform.txt" )
107
+ ]
108
+
137
109
# fmt:off
138
110
wf .connect ([
139
- (conform , get1st , [("out_file" , "inlist" )]),
140
- (get1st , ensure_ras , [("out" , "in_file" )]),
141
- (ensure_ras , outputnode , [("out_file" , "out_file" )]),
111
+ (anat_conform , get1st , [('out_file' , 'inlist' )]),
112
+ (get1st , outputnode , [('out' , 'anat_ref' )]),
142
113
])
143
114
# fmt:on
144
115
return wf
145
116
146
- from nipype .interfaces import freesurfer as fs
147
-
148
- wf .__desc__ = f"""\
149
- An anatomical reference-map was computed after registration of
150
- { num_maps } images (after INU-correction) using
151
- `mri_robust_template` [FreeSurfer { fs .Info ().looseversion () or "<ver>" } , @fs_template].
152
- """
153
-
154
- conform_xfm = pe .MapNode (
117
+ anat_conform_xfm = pe .MapNode (
155
118
LTAConvert (in_lta = "identity.nofile" , out_lta = True ),
156
119
iterfield = ["source_file" , "target_file" ],
157
- name = "conform_xfm " ,
120
+ name = "anat_conform_xfm " ,
158
121
)
159
-
160
- # 6. StructuralReference is fs.RobustTemplate if > 1 volume, copying otherwise
161
- merge = pe .Node (
122
+ # 1. Template (only if several anatomical images)
123
+ # 1a. Correct for bias field: the bias field is an additive factor
124
+ # in log-transformed intensity units. Therefore, it is not a linear
125
+ # combination of fields and N4 fails with merged images.
126
+ # 1b. Align and merge if several anatomical images are provided
127
+ clip_preinu = pe .MapNode (IntensityClip (p_min = 50 ), iterfield = "in_file" , name = "clip_preinu" )
128
+ n4_correct = pe .MapNode (
129
+ N4BiasFieldCorrection (
130
+ dimension = 3 ,
131
+ save_bias = False ,
132
+ copy_header = True ,
133
+ n_iterations = [50 ] * (5 - 2 * sloppy ),
134
+ convergence_threshold = 1e-7 ,
135
+ shrink_factor = 4 ,
136
+ bspline_fitting_distance = bspline_fitting_distance ,
137
+ ),
138
+ iterfield = "input_image" ,
139
+ name = "n4_correct" ,
140
+ n_procs = 1 ,
141
+ )
142
+ # StructuralReference is fs.RobustTemplate if > 1 volume, copying otherwise
143
+ anat_merge = pe .Node (
162
144
StructuralReference (
163
145
auto_detect_sensitivity = True ,
164
146
initial_timepoint = 1 , # For deterministic behavior
@@ -168,12 +150,12 @@ def init_anat_average_wf(
168
150
no_iteration = not longitudinal ,
169
151
transform_outputs = True ,
170
152
),
171
- mem_gb = 2 * num_maps - 1 ,
172
- name = "merge " ,
153
+ mem_gb = 2 * num_files - 1 ,
154
+ name = "anat_merge " ,
173
155
)
174
156
175
- # 7. Final intensity equalization/conformation
176
- clip_final = pe .Node (IntensityClip ( p_min = 2.0 , p_max = 99.9 ), name = "clip_final " )
157
+ # 2. Reorient template to RAS, if needed (mri_robust_template may set to LIA)
158
+ anat_reorient = pe .Node (Reorient ( ), name = "anat_reorient " )
177
159
178
160
merge_xfm = pe .MapNode (
179
161
niu .Merge (2 ),
@@ -193,28 +175,22 @@ def _set_threads(in_list, maximum):
193
175
194
176
# fmt:off
195
177
wf .connect ([
196
- (ref_dimensions , conform_xfm , [("t1w_valid_list" , "source_file" )]),
197
- (conform , conform_xfm , [("out_file" , "target_file" )]),
198
- (conform , merge , [
199
- ("out_file" , "in_files" ),
200
- (("out_file" , _set_threads , omp_nthreads ), "num_threads" ),
201
- (("out_file" , add_suffix , "_template" ), "out_file" )]),
202
- (merge , ensure_ras , [("out_file" , "in_file" )]),
178
+ (anat_ref_dimensions , anat_conform_xfm , [('t1w_valid_list' , 'source_file' )]),
179
+ (anat_conform , anat_conform_xfm , [('out_file' , 'target_file' )]),
180
+ (anat_conform , clip_preinu , [('out_file' , 'in_file' )]),
181
+ (anat_conform , anat_merge , [
182
+ (('out_file' , _set_threads , omp_nthreads ), 'num_threads' ),
183
+ (('out_file' , add_suffix , '_template' ), 'out_file' )]),
184
+ (clip_preinu , n4_correct , [('out_file' , 'input_image' )]),
185
+ (n4_correct , anat_merge , [('output_image' , 'in_files' )]),
186
+ (anat_merge , anat_reorient , [('out_file' , 'in_file' )]),
203
187
# Combine orientation and template transforms
204
- (conform_xfm , merge_xfm , [(" out_lta" , " in1" )]),
205
- (merge , merge_xfm , [(" transform_outputs" , " in2" )]),
206
- (merge_xfm , concat_xfms , [(" out" , " in_xfms" )]),
188
+ (anat_conform_xfm , merge_xfm , [(' out_lta' , ' in1' )]),
189
+ (anat_merge , merge_xfm , [(' transform_outputs' , ' in2' )]),
190
+ (merge_xfm , concat_xfms , [(' out' , ' in_xfms' )]),
207
191
# Output
208
- (ensure_ras , clip_final , [("out_file" , "in_file" )]),
209
- (clip_final , outputnode , [("out_file" , "out_file" )]),
210
- (concat_xfms , outputnode , [("out_xfm" , "realign_xfms" )]),
192
+ (anat_reorient , outputnode , [('out_file' , 'anat_ref' )]),
193
+ (concat_xfms , outputnode , [('out_xfm' , 'anat_realign_xfm' )]),
211
194
])
212
195
# fmt:on
213
-
214
196
return wf
215
-
216
-
217
- def _flatten (inlist ):
218
- from bids .utils import listify
219
-
220
- return [el for items in listify (inlist ) for el in listify (items )]
0 commit comments