2323"""
2424Estimating the susceptibility distortions without fieldmaps.
2525"""
26+ import json
2627
2728from nipype .pipeline import engine as pe
2829from nipype .interfaces import utility as niu
29- from nipype .interfaces .base import Undefined
3030from niworkflows .engine .workflows import LiterateWorkflow as Workflow
3131
3232from ... import data
4444
4545def init_syn_sdc_wf (
4646 * ,
47- atlas_threshold = 3 ,
4847 sloppy = False ,
4948 debug = False ,
5049 name = "syn_sdc_wf" ,
5150 omp_nthreads = 1 ,
5251 laplacian_weight = None ,
53- sd_prior = True ,
5452 ** kwargs ,
5553):
5654 """
@@ -59,10 +57,6 @@ def init_syn_sdc_wf(
5957 SyN deformation is restricted to the phase-encoding (PE) direction.
6058 If no PE direction is specified, anterior-posterior PE is assumed.
6159
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-
6660 Workflow Graph
6761 .. workflow ::
6862 :graph2use: orig
@@ -73,9 +67,6 @@ def init_syn_sdc_wf(
7367
7468 Parameters
7569 ----------
76- atlas_threshold : :obj:`float`
77- Exclude from the registration metric computation areas with average distortions
78- below this threshold (in mm).
7970 sloppy : :obj:`bool`
8071 Whether a fast (less accurate) configuration of the workflow should be applied.
8172 debug : :obj:`bool`
@@ -102,9 +93,6 @@ def init_syn_sdc_wf(
10293 A preprocessed, skull-stripped anatomical (T1w or T2w) image resampled in EPI space.
10394 anat_mask : :obj:`str`
10495 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
10896
10997 Outputs
11098 -------
@@ -150,16 +138,15 @@ def init_syn_sdc_wf(
150138 workflow = Workflow (name = name )
151139 workflow .__desc__ = f"""\
152140 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.
154142The 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].
157144Registration is performed with `antsRegistration`
158145(ANTs { ants_version or "-- version unknown" } ), and
159146the 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.
162148"""
149+
163150 inputnode = pe .Node (niu .IdentityInterface (INPUT_FIELDS ), name = "inputnode" )
164151 outputnode = pe .Node (
165152 niu .IdentityInterface (
@@ -211,10 +198,11 @@ def init_syn_sdc_wf(
211198
212199 epi_umask = pe .Node (Union (), name = "epi_umask" )
213200 moving_masks = pe .Node (
214- niu .Merge (3 ),
201+ niu .Merge (2 ),
215202 name = "moving_masks" ,
216203 run_without_submitting = True ,
217204 )
205+ moving_masks .inputs .in1 = "NULL"
218206
219207 fixed_masks = pe .Node (
220208 niu .Merge (2 ),
@@ -227,9 +215,14 @@ def init_syn_sdc_wf(
227215 find_zooms = pe .Node (niu .Function (function = _adjust_zooms ), name = "find_zooms" )
228216 zooms_epi = pe .Node (RegridToZooms (), name = "zooms_epi" )
229217
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+
230223 # SyN Registration Core
231224 syn = pe .Node (
232- Registration (from_file = data . load ( f"sd_syn { '_sloppy' * sloppy } .json" ) ),
225+ Registration (from_file = syn_config ),
233226 name = "syn" ,
234227 n_procs = omp_nthreads ,
235228 )
@@ -287,9 +280,10 @@ def init_syn_sdc_wf(
287280 (inputnode , amask2epi , [("epi_mask" , "reference_image" )]),
288281 (inputnode , zooms_bmask , [("anat_mask" , "input_image" )]),
289282 (inputnode , fixed_masks , [("anat_mask" , "in1" ),
290- ("anat_mask " , "in2" )]),
283+ ("sd_prior " , "in2" )]),
291284 (inputnode , anat_dilmsk , [("anat_mask" , "in_file" )]),
292285 (inputnode , warp_dir , [("anat_ref" , "fixed_image" )]),
286+ (inputnode , vox_params , [("anat_ref" , "fixed_image" )]),
293287 (inputnode , anat_merge , [("anat_ref" , "in1" )]),
294288 (inputnode , lap_anat , [("anat_ref" , "op1" )]),
295289 (inputnode , find_zooms , [("anat_ref" , "in_anat" ),
@@ -298,9 +292,7 @@ def init_syn_sdc_wf(
298292 (inputnode , epi_umask , [("epi_mask" , "in1" )]),
299293 (lap_anat , lap_anat_norm , [("output_image" , "in_file" )]),
300294 (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" )]),
304296 (clip_epi , epi_merge , [("out_file" , "in1" )]),
305297 (clip_epi , lap_epi , [("out_file" , "op1" )]),
306298 (clip_epi , zooms_epi , [("out_file" , "in_file" )]),
@@ -310,11 +302,15 @@ def init_syn_sdc_wf(
310302 (anat_dilmsk , amask2epi , [("out_file" , "input_image" )]),
311303 (amask2epi , epi_umask , [("output_image" , "in2" )]),
312304 (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" )]),
313308 (warp_dir , syn , [("out" , "restrict_deformation" )]),
314309 (anat_merge , syn , [("out" , "fixed_image" )]),
315310 (fixed_masks , syn , [("out" , "fixed_image_masks" )]),
316311 (epi_merge , syn , [("out" , "moving_image" )]),
317312 (moving_masks , syn , [("out" , "moving_image_masks" )]),
313+ (vox_params , syn , [("out" , "transform_parameters" )]),
318314 (syn , extract_field , [(("forward_transforms" , _pop ), "transform" )]),
319315 (clip_epi , extract_field , [("out_file" , "epi" )]),
320316 (readout_time , extract_field , [("readout_time" , "ro_time" ),
@@ -339,6 +335,7 @@ def init_syn_sdc_wf(
339335
340336def init_syn_preprocessing_wf (
341337 * ,
338+ atlas_threshold = 3 ,
342339 debug = False ,
343340 name = "syn_preprocessing_wf" ,
344341 omp_nthreads = 1 ,
@@ -360,6 +357,9 @@ def init_syn_preprocessing_wf(
360357
361358 Parameters
362359 ----------
360+ atlas_threshold : :obj:`float`
361+ Mask excluding areas with average distortions below this threshold (in mm)
362+ on the prior.
363363 debug : :obj:`bool`
364364 Whether a fast (less accurate) configuration of the workflow should be applied.
365365 name : :obj:`str`
@@ -528,6 +528,8 @@ def _remove_first_mask(in_file):
528528 ])
529529
530530 if sd_prior :
531+ from niworkflows .interfaces .nibabel import Binarize
532+
531533 # Mapping & preparing prior knowledge
532534 # Concatenate transform files:
533535 # 1) MNI -> anat; 2) ATLAS -> MNI
@@ -550,11 +552,14 @@ def _remove_first_mask(in_file):
550552 mem_gb = 0.3 ,
551553 )
552554
555+ prior_msk = pe .Node (Binarize (thresh_low = atlas_threshold ), name = "prior_msk" )
556+
553557 workflow .connect ([
554558 (inputnode , transform_list , [("std2anat_xfm" , "in2" )]),
555559 (transform_list , prior2epi , [("out" , "transforms" )]),
556560 (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" )]),
558563 ]) # fmt:skip
559564
560565 if coregister :
@@ -567,10 +572,8 @@ def _remove_first_mask(in_file):
567572 ])
568573
569574 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" )
574577
575578 workflow .connect ([
576579 (inputnode , epi_reference_wf , [("in_epis" , "inputnode.in_files" )]),
@@ -621,25 +624,52 @@ def _remove_first_mask(in_file):
621624 return workflow
622625
623626
624- def _warp_dir (fixed_image , pe_dir , nlevels = 3 ):
627+ def _warp_dir (moving_image , fixed_image , pe_dir , nlevels = 2 ):
625628 """Extract the ``restrict_deformation`` argument from metadata."""
626629 import numpy as np
627630 import nibabel as nb
628631
629- img = nb .load (fixed_image )
632+ moving = nb .load (moving_image )
633+ fixed = nb .load (fixed_image )
630634
631- if np .any (nb .affines .obliquity (img .affine ) > 0.05 ):
635+ if np .any (nb .affines .obliquity (fixed .affine ) > 0.05 ):
632636 from nipype import logging
633637
634638 logging .getLogger ("nipype.interface" ).warn (
635639 "Running fieldmap-less registration on an oblique dataset"
636640 )
637641
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 )]
641668
642- return nlevels * [retval .tolist ()]
669+ return [
670+ [* level_params [:2 ], level_params [2 ] / pe_res ]
671+ for level_params in params
672+ ]
643673
644674
645675def _merge_meta (epi_ref , meta_list ):
0 commit comments