Skip to content

Commit 792a293

Browse files
committed
RF: Converge anatomical template workflow to mimic smriprep's
1 parent 45a63a6 commit 792a293

File tree

1 file changed

+113
-137
lines changed

1 file changed

+113
-137
lines changed
Lines changed: 113 additions & 137 deletions
Original file line numberDiff line numberDiff line change
@@ -1,164 +1,146 @@
11
"""Prepare anatomical images for processing."""
22
from nipype.interfaces import utility as niu
33
from nipype.pipeline import engine as pe
4+
from niworkflows.engine.workflows import LiterateWorkflow
45

56

6-
def init_anat_average_wf(
7+
def init_anat_template_wf(
78
*,
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:
1517
"""
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+
)
2727
Parameters
2828
----------
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
3133
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)
3736
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.
3940
sloppy : :obj:`bool`
4041
Run in *sloppy* mode.
41-
42+
name : :obj:`str`, optional
43+
Workflow name (default: anat_template_wf)
4244
Inputs
4345
------
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
4748
Outputs
4849
-------
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
5958
"""
6059
from nipype.interfaces.ants import N4BiasFieldCorrection
6160
from nipype.interfaces.image import Reorient
62-
from niworkflows.engine.workflows import LiterateWorkflow as Workflow
6361
from niworkflows.interfaces.freesurfer import PatchedLTAConvert as LTAConvert
6462
from niworkflows.interfaces.freesurfer import StructuralReference
65-
from niworkflows.interfaces.header import ValidateImage
6663
from niworkflows.interfaces.images import Conform, TemplateDimensions
67-
from niworkflows.interfaces.nibabel import IntensityClip, SplitSeries
64+
from niworkflows.interfaces.nibabel import IntensityClip
6865
from niworkflows.interfaces.nitransforms import ConcatenateXFMs
6966
from niworkflows.utils.misc import add_suffix
70-
from pkg_resources import resource_filename as pkgr
7167

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
7373

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")
7582
outputnode = pe.Node(
7683
niu.IdentityInterface(fields=["out_file", "valid_list", "realign_xfms", "out_report"]),
7784
name="outputnode",
7885
)
7986

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")
9090

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")
11491
# fmt:off
11592
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')]),
128100
])
129101
# fmt:on
130102

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:
135104
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+
137109
# fmt:off
138110
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')]),
142113
])
143114
# fmt:on
144115
return wf
145116

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(
155118
LTAConvert(in_lta="identity.nofile", out_lta=True),
156119
iterfield=["source_file", "target_file"],
157-
name="conform_xfm",
120+
name="anat_conform_xfm",
158121
)
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(
162144
StructuralReference(
163145
auto_detect_sensitivity=True,
164146
initial_timepoint=1, # For deterministic behavior
@@ -168,12 +150,12 @@ def init_anat_average_wf(
168150
no_iteration=not longitudinal,
169151
transform_outputs=True,
170152
),
171-
mem_gb=2 * num_maps - 1,
172-
name="merge",
153+
mem_gb=2 * num_files - 1,
154+
name="anat_merge",
173155
)
174156

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")
177159

178160
merge_xfm = pe.MapNode(
179161
niu.Merge(2),
@@ -193,28 +175,22 @@ def _set_threads(in_list, maximum):
193175

194176
# fmt:off
195177
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')]),
203187
# 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')]),
207191
# 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')]),
211194
])
212195
# fmt:on
213-
214196
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

Comments
 (0)