Skip to content

Commit 6d0b2e2

Browse files
authored
Merge branch 'master' into syn-refactor
2 parents 9b369f9 + f3b32fc commit 6d0b2e2

File tree

5 files changed

+222
-52
lines changed

5 files changed

+222
-52
lines changed

.github/workflows/build-test-publish.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ jobs:
122122
git config --global user.name 'NiPreps Bot'
123123
git config --global user.email '[email protected]'
124124
- name: Install the latest version of uv
125-
uses: astral-sh/setup-uv@v5
125+
uses: astral-sh/setup-uv@v6
126126
- name: Set up Python ${{ matrix.python-version }}
127127
uses: conda-incubator/setup-miniconda@v3
128128
with:

sdcflows/data/sd_syn.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
"shrink_factors": [ [ 1, 1 ], [ 1 ] ],
1515
"sigma_units": [ "vox", "vox" ],
1616
"smoothing_sigmas": [ [ 2, 0 ], [ 0 ] ],
17-
"transform_parameters": [ [ 0.8, 6.0, 3.0 ], [ 0.8, 2.0, 1.0 ] ],
17+
"transform_parameters": [ [ 0.8, 6.0, 10.0 ], [ 0.8, 2.0, 0.5 ] ],
1818
"transforms": [ "SyN", "SyN" ],
1919
"use_histogram_matching": [ true, true ],
2020
"winsorize_lower_quantile": 0.001,

sdcflows/data/sd_syn_sloppy.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
"shrink_factors": [ [ 1, 1 ], [ 1 ] ],
1515
"sigma_units": [ "vox", "vox" ],
1616
"smoothing_sigmas": [ [ 2, 0 ], [ 0 ] ],
17-
"transform_parameters": [ [ 0.8, 6.0, 3.0 ], [ 0.8, 2.0, 1.0 ] ],
17+
"transform_parameters": [ [ 0.8, 6.0, 10.0 ], [ 0.8, 2.0, 0.5 ] ],
1818
"transforms": [ "SyN", "SyN" ],
1919
"use_histogram_matching": [ true, true ],
2020
"verbose": true,

sdcflows/workflows/fit/syn.py

Lines changed: 66 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,10 @@
2323
"""
2424
Estimating the susceptibility distortions without fieldmaps.
2525
"""
26+
import json
2627

2728
from nipype.pipeline import engine as pe
2829
from nipype.interfaces import utility as niu
29-
from nipype.interfaces.base import Undefined
3030
from niworkflows.engine.workflows import LiterateWorkflow as Workflow
3131

3232
from ... import data
@@ -44,13 +44,11 @@
4444

4545
def 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.
154142
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].
157144
Registration is performed with `antsRegistration`
158145
(ANTs {ants_version or "-- version unknown"}), and
159146
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.
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

340336
def 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

645675
def _merge_meta(epi_ref, meta_list):

0 commit comments

Comments
 (0)