23
23
"""
24
24
Estimating the susceptibility distortions without fieldmaps.
25
25
"""
26
+ import json
26
27
27
28
from nipype .pipeline import engine as pe
28
29
from nipype .interfaces import utility as niu
29
- from nipype .interfaces .base import Undefined
30
30
from niworkflows .engine .workflows import LiterateWorkflow as Workflow
31
31
32
32
from ... import data
44
44
45
45
def init_syn_sdc_wf (
46
46
* ,
47
- atlas_threshold = 3 ,
48
47
sloppy = False ,
49
48
debug = False ,
50
49
name = "syn_sdc_wf" ,
51
50
omp_nthreads = 1 ,
52
51
laplacian_weight = None ,
53
- sd_prior = True ,
54
52
** kwargs ,
55
53
):
56
54
"""
@@ -59,10 +57,6 @@ def init_syn_sdc_wf(
59
57
SyN deformation is restricted to the phase-encoding (PE) direction.
60
58
If no PE direction is specified, anterior-posterior PE is assumed.
61
59
62
- SyN deformation is also restricted to regions that are expected to have a
63
- >3mm (approximately 1 voxel) warp, based on the fieldmap atlas.
64
-
65
-
66
60
Workflow Graph
67
61
.. workflow ::
68
62
:graph2use: orig
@@ -73,9 +67,6 @@ def init_syn_sdc_wf(
73
67
74
68
Parameters
75
69
----------
76
- atlas_threshold : :obj:`float`
77
- Exclude from the registration metric computation areas with average distortions
78
- below this threshold (in mm).
79
70
sloppy : :obj:`bool`
80
71
Whether a fast (less accurate) configuration of the workflow should be applied.
81
72
debug : :obj:`bool`
@@ -102,9 +93,6 @@ def init_syn_sdc_wf(
102
93
A preprocessed, skull-stripped anatomical (T1w or T2w) image resampled in EPI space.
103
94
anat_mask : :obj:`str`
104
95
Path to the brain mask corresponding to ``anat_ref`` in EPI space.
105
- sd_prior : :obj:`str`
106
- A template map of areas with strong susceptibility distortions (SD) to regularize
107
- the cost function of SyN
108
96
109
97
Outputs
110
98
-------
@@ -150,16 +138,15 @@ def init_syn_sdc_wf(
150
138
workflow = Workflow (name = name )
151
139
workflow .__desc__ = f"""\
152
140
A deformation field to correct for susceptibility distortions was estimated
153
- based on *fMRIPrep*'s *fieldmap-less* approach.
141
+ based on *SDCFlows*' *fieldmap-less* approach.
154
142
The deformation field is that resulting from co-registering the EPI reference
155
- to the same-subject T1w-reference with its intensity inverted [@fieldmapless1;
156
- @fieldmapless2].
143
+ to the same-subject's T1w-reference [@fieldmapless1; @fieldmapless2].
157
144
Registration is performed with `antsRegistration`
158
145
(ANTs { ants_version or "-- version unknown" } ), and
159
146
the process regularized by constraining deformation to be nonzero only
160
- along the phase-encoding direction, and modulated with an average fieldmap
161
- template [@fieldmapless3].
147
+ along the phase-encoding direction.
162
148
"""
149
+
163
150
inputnode = pe .Node (niu .IdentityInterface (INPUT_FIELDS ), name = "inputnode" )
164
151
outputnode = pe .Node (
165
152
niu .IdentityInterface (
@@ -211,10 +198,11 @@ def init_syn_sdc_wf(
211
198
212
199
epi_umask = pe .Node (Union (), name = "epi_umask" )
213
200
moving_masks = pe .Node (
214
- niu .Merge (3 ),
201
+ niu .Merge (2 ),
215
202
name = "moving_masks" ,
216
203
run_without_submitting = True ,
217
204
)
205
+ moving_masks .inputs .in1 = "NULL"
218
206
219
207
fixed_masks = pe .Node (
220
208
niu .Merge (2 ),
@@ -227,9 +215,14 @@ def init_syn_sdc_wf(
227
215
find_zooms = pe .Node (niu .Function (function = _adjust_zooms ), name = "find_zooms" )
228
216
zooms_epi = pe .Node (RegridToZooms (), name = "zooms_epi" )
229
217
218
+ syn_config = data .load (f"sd_syn{ '_sloppy' * sloppy } .json" )
219
+
220
+ vox_params = pe .Node (niu .Function (function = _mm2vox ), name = "vox_params" )
221
+ vox_params .inputs .registration_config = json .loads (syn_config .read_text ())
222
+
230
223
# SyN Registration Core
231
224
syn = pe .Node (
232
- Registration (from_file = data . load ( f"sd_syn { '_sloppy' * sloppy } .json" ) ),
225
+ Registration (from_file = syn_config ),
233
226
name = "syn" ,
234
227
n_procs = omp_nthreads ,
235
228
)
@@ -287,9 +280,10 @@ def init_syn_sdc_wf(
287
280
(inputnode , amask2epi , [("epi_mask" , "reference_image" )]),
288
281
(inputnode , zooms_bmask , [("anat_mask" , "input_image" )]),
289
282
(inputnode , fixed_masks , [("anat_mask" , "in1" ),
290
- ("anat_mask " , "in2" )]),
283
+ ("sd_prior " , "in2" )]),
291
284
(inputnode , anat_dilmsk , [("anat_mask" , "in_file" )]),
292
285
(inputnode , warp_dir , [("anat_ref" , "fixed_image" )]),
286
+ (inputnode , vox_params , [("anat_ref" , "fixed_image" )]),
293
287
(inputnode , anat_merge , [("anat_ref" , "in1" )]),
294
288
(inputnode , lap_anat , [("anat_ref" , "op1" )]),
295
289
(inputnode , find_zooms , [("anat_ref" , "in_anat" ),
@@ -298,9 +292,7 @@ def init_syn_sdc_wf(
298
292
(inputnode , epi_umask , [("epi_mask" , "in1" )]),
299
293
(lap_anat , lap_anat_norm , [("output_image" , "in_file" )]),
300
294
(lap_anat_norm , anat_merge , [("out" , "in2" )]),
301
- (epi_umask , moving_masks , [("out_file" , "in1" ),
302
- ("out_file" , "in2" ),
303
- ("out_file" , "in3" )]),
295
+ (epi_umask , moving_masks , [("out_file" , "in2" )]),
304
296
(clip_epi , epi_merge , [("out_file" , "in1" )]),
305
297
(clip_epi , lap_epi , [("out_file" , "op1" )]),
306
298
(clip_epi , zooms_epi , [("out_file" , "in_file" )]),
@@ -310,11 +302,15 @@ def init_syn_sdc_wf(
310
302
(anat_dilmsk , amask2epi , [("out_file" , "input_image" )]),
311
303
(amask2epi , epi_umask , [("output_image" , "in2" )]),
312
304
(readout_time , warp_dir , [("pe_direction" , "pe_dir" )]),
305
+ (readout_time , vox_params , [("pe_direction" , "pe_dir" )]),
306
+ (clip_epi , warp_dir , [("out_file" , "moving_image" )]),
307
+ (clip_epi , vox_params , [("out_file" , "moving_image" )]),
313
308
(warp_dir , syn , [("out" , "restrict_deformation" )]),
314
309
(anat_merge , syn , [("out" , "fixed_image" )]),
315
310
(fixed_masks , syn , [("out" , "fixed_image_masks" )]),
316
311
(epi_merge , syn , [("out" , "moving_image" )]),
317
312
(moving_masks , syn , [("out" , "moving_image_masks" )]),
313
+ (vox_params , syn , [("out" , "transform_parameters" )]),
318
314
(syn , extract_field , [(("forward_transforms" , _pop ), "transform" )]),
319
315
(clip_epi , extract_field , [("out_file" , "epi" )]),
320
316
(readout_time , extract_field , [("readout_time" , "ro_time" ),
@@ -339,6 +335,7 @@ def init_syn_sdc_wf(
339
335
340
336
def init_syn_preprocessing_wf (
341
337
* ,
338
+ atlas_threshold = 3 ,
342
339
debug = False ,
343
340
name = "syn_preprocessing_wf" ,
344
341
omp_nthreads = 1 ,
@@ -360,6 +357,9 @@ def init_syn_preprocessing_wf(
360
357
361
358
Parameters
362
359
----------
360
+ atlas_threshold : :obj:`float`
361
+ Mask excluding areas with average distortions below this threshold (in mm)
362
+ on the prior.
363
363
debug : :obj:`bool`
364
364
Whether a fast (less accurate) configuration of the workflow should be applied.
365
365
name : :obj:`str`
@@ -528,6 +528,8 @@ def _remove_first_mask(in_file):
528
528
])
529
529
530
530
if sd_prior :
531
+ from niworkflows .interfaces .nibabel import Binarize
532
+
531
533
# Mapping & preparing prior knowledge
532
534
# Concatenate transform files:
533
535
# 1) MNI -> anat; 2) ATLAS -> MNI
@@ -550,11 +552,14 @@ def _remove_first_mask(in_file):
550
552
mem_gb = 0.3 ,
551
553
)
552
554
555
+ prior_msk = pe .Node (Binarize (thresh_low = atlas_threshold ), name = "prior_msk" )
556
+
553
557
workflow .connect ([
554
558
(inputnode , transform_list , [("std2anat_xfm" , "in2" )]),
555
559
(transform_list , prior2epi , [("out" , "transforms" )]),
556
560
(sampling_ref , prior2epi , [("out_file" , "reference_image" )]),
557
- (prior2epi , outputnode , [("output_image" , "sd_prior" )]),
561
+ (prior2epi , prior_msk , [("output_image" , "in_file" )]),
562
+ (prior_msk , outputnode , [("out_mask" , "sd_prior" )]),
558
563
]) # fmt:skip
559
564
560
565
if coregister :
@@ -567,10 +572,8 @@ def _remove_first_mask(in_file):
567
572
])
568
573
569
574
else :
570
- # no prior to be used
571
- # MG: Future goal is to allow using alternative mappings
572
- # i.e. in the case of infants, where priors change depending on development
573
- outputnode .inputs .sd_prior = Undefined
575
+ # no prior to be used -> set anatomical mask as prior
576
+ workflow .connect (mask_dtype , "out" , outputnode , "sd_prior" )
574
577
575
578
workflow .connect ([
576
579
(inputnode , epi_reference_wf , [("in_epis" , "inputnode.in_files" )]),
@@ -621,25 +624,52 @@ def _remove_first_mask(in_file):
621
624
return workflow
622
625
623
626
624
- def _warp_dir (fixed_image , pe_dir , nlevels = 3 ):
627
+ def _warp_dir (moving_image , fixed_image , pe_dir , nlevels = 2 ):
625
628
"""Extract the ``restrict_deformation`` argument from metadata."""
626
629
import numpy as np
627
630
import nibabel as nb
628
631
629
- img = nb .load (fixed_image )
632
+ moving = nb .load (moving_image )
633
+ fixed = nb .load (fixed_image )
630
634
631
- if np .any (nb .affines .obliquity (img .affine ) > 0.05 ):
635
+ if np .any (nb .affines .obliquity (fixed .affine ) > 0.05 ):
632
636
from nipype import logging
633
637
634
638
logging .getLogger ("nipype.interface" ).warn (
635
639
"Running fieldmap-less registration on an oblique dataset"
636
640
)
637
641
638
- vs = nb .affines .voxel_sizes (img .affine )
639
- order = np .around (np .abs (img .affine [:3 , :3 ] / vs ))
640
- retval = order @ [1 if pe_dir [0 ] == ax else 0.1 for ax in "ijk" ]
642
+ moving_axcodes = nb .aff2axcodes (moving .affine , ["RR" , "AA" , "SS" ])
643
+ moving_pe_axis = moving_axcodes ["ijk" .index (pe_dir [0 ])]
644
+
645
+ fixed_axcodes = nb .aff2axcodes (fixed .affine , ["RR" , "AA" , "SS" ])
646
+
647
+ deformation = [0.1 , 0.1 , 0.1 ]
648
+ deformation [fixed_axcodes .index (moving_pe_axis )] = 1.0
649
+
650
+ return nlevels * [deformation ]
651
+
652
+
653
+ def _mm2vox (moving_image , fixed_image , pe_dir , registration_config ):
654
+ import nibabel as nb
655
+
656
+ params = registration_config ['transform_parameters' ]
657
+
658
+ moving = nb .load (moving_image )
659
+ # Use duplicate axcodes to ignore sign
660
+ moving_axcodes = nb .aff2axcodes (moving .affine , ["RR" , "AA" , "SS" ])
661
+ moving_pe_axis = moving_axcodes ["ijk" .index (pe_dir [0 ])]
662
+
663
+ fixed = nb .load (fixed_image )
664
+ fixed_axcodes = nb .aff2axcodes (fixed .affine , ["RR" , "AA" , "SS" ])
665
+
666
+ zooms = nb .affines .voxel_sizes (fixed .affine )
667
+ pe_res = zooms [fixed_axcodes .index (moving_pe_axis )]
641
668
642
- return nlevels * [retval .tolist ()]
669
+ return [
670
+ [* level_params [:2 ], level_params [2 ] / pe_res ]
671
+ for level_params in params
672
+ ]
643
673
644
674
645
675
def _merge_meta (epi_ref , meta_list ):
0 commit comments