Skip to content

Commit 03e209f

Browse files
authored
feat: Add registration metadata to boldref-to-anat transforms (#3500)
Closes #3496.
2 parents 80d2b69 + 3ea4c4c commit 03e209f

File tree

4 files changed

+100
-29
lines changed

4 files changed

+100
-29
lines changed

.git-blame-ignore-revs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
1-
# 2024-02-05 - [email protected] - STY: ruff --fix + 1 ignore [git-blame-ignore-rev]
1+
# 2025-08-15 10:33:40 -0400 - [email protected] - sty: Use fmt:skip in workflows.bold.outputs [ignore-rev]
2+
f97f3439a3897346e961bb0039fc01ac9df3c2ea
3+
# 2024-11-03 09:52:44 -0500 - [email protected] - STY: ruff [ignore-rev]
4+
729b5b923599bc80d9d2de6d8fe0e957d4337cab
5+
# 2024-02-05 12:06:27 -0500 - [email protected] - STY: ruff --fix + 1 ignore [git-blame-ignore-rev]
26
e7dc59fbc7df88dfabbff154f4cc3d24721b6b4f
3-
# 2024-01-16 - [email protected] - STY: ruff --fix --unsafe-fixes (with cleanup) [git-blame-ignore-rev]
7+
# 2024-01-16 13:47:18 -0500 - [email protected] - STY: ruff --fix --unsafe-fixes (with cleanup) [git-blame-ignore-rev]
48
66734bd0164d1dae3cf299fa9d9d682c22eeda66
5-
# 2024-01-16 - [email protected] - STY: ruff format and ruff --fix [git-blame-ignore-rev]
9+
# 2024-01-16 13:33:25 -0500 - [email protected] - STY: ruff format and ruff --fix [git-blame-ignore-rev]
610
311686eb329d3594732429f7c3d50d212b8dc519
7-
# 2023-02-02 - [email protected] - STY: black [git-blame-ignore-rev]
11+
# 2023-02-02 10:57:19 -0500 - [email protected] - STY: black [git-blame-ignore-rev]
812
d3f380701b3953087475a18cf23c0fb388f7aed6
913
# 2022-09-22 - [email protected] - STY: black/isort the docker wrapper
1014
9976458388f369cba4b7d81359acc40b52f6621c

fmriprep/workflows/bold/fit.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -682,7 +682,10 @@ def init_bold_fit_wf(
682682
(regref_buffer, bold_reg_wf, [('boldref', 'inputnode.ref_bold_brain')]),
683683
# Incomplete sources
684684
(regref_buffer, ds_boldreg_wf, [('boldref', 'inputnode.source_files')]),
685-
(bold_reg_wf, ds_boldreg_wf, [('outputnode.itk_bold_to_t1', 'inputnode.xform')]),
685+
(bold_reg_wf, ds_boldreg_wf, [
686+
('outputnode.itk_bold_to_t1', 'inputnode.xform'),
687+
('outputnode.metadata', 'inputnode.metadata'),
688+
]),
686689
(ds_boldreg_wf, outputnode, [('outputnode.xform', 'boldref2anat_xfm')]),
687690
(bold_reg_wf, summary, [('outputnode.fallback', 'fallback')]),
688691
]) # fmt:skip

fmriprep/workflows/bold/outputs.py

Lines changed: 12 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,6 @@ def init_func_fit_reports_wf(
267267
mem_gb=1,
268268
)
269269

270-
# fmt:off
271270
workflow.connect([
272271
(inputnode, ds_summary, [
273272
('source_file', 'source_file'),
@@ -288,8 +287,7 @@ def init_func_fit_reports_wf(
288287
('boldref2anat_xfm', 'transforms'),
289288
]),
290289
(t1w_wm, boldref_wm, [('out', 'input_image')]),
291-
])
292-
# fmt:on
290+
]) # fmt:skip
293291

294292
# Reportlets follow the structure of init_bold_fit_wf stages
295293
# - SDC1:
@@ -424,15 +422,13 @@ def init_func_fit_reports_wf(
424422
name='ds_epi_t1_report',
425423
)
426424

427-
# fmt:off
428425
workflow.connect([
429426
(inputnode, epi_t1_report, [('coreg_boldref', 'after')]),
430427
(t1w_boldref, epi_t1_report, [('output_image', 'before')]),
431428
(boldref_wm, epi_t1_report, [('output_image', 'wm_seg')]),
432429
(inputnode, ds_epi_t1_report, [('source_file', 'source_file')]),
433430
(epi_t1_report, ds_epi_t1_report, [('out_report', 'in_file')]),
434-
])
435-
# fmt:on
431+
]) # fmt:skip
436432

437433
return workflow
438434

@@ -473,15 +469,13 @@ def init_ds_boldref_wf(
473469
run_without_submitting=True,
474470
)
475471

476-
# fmt:off
477472
workflow.connect([
478473
(inputnode, sources, [('source_files', 'in1')]),
479474
(inputnode, ds_boldref, [('boldref', 'in_file'),
480475
('source_files', 'source_file')]),
481476
(sources, ds_boldref, [('out', 'Sources')]),
482477
(ds_boldref, outputnode, [('out_file', 'boldref')]),
483-
])
484-
# fmt:on
478+
]) # fmt:skip
485479

486480
return workflow
487481

@@ -546,7 +540,7 @@ def init_ds_registration_wf(
546540
workflow = pe.Workflow(name=name)
547541

548542
inputnode = pe.Node(
549-
niu.IdentityInterface(fields=['source_files', 'xform']),
543+
niu.IdentityInterface(fields=['source_files', 'xform', 'metadata']),
550544
name='inputnode',
551545
)
552546
outputnode = pe.Node(niu.IdentityInterface(fields=['xform']), name='outputnode')
@@ -574,15 +568,16 @@ def init_ds_registration_wf(
574568
mem_gb=DEFAULT_MEMORY_MIN_GB,
575569
)
576570

577-
# fmt:off
578571
workflow.connect([
579572
(inputnode, sources, [('source_files', 'in1')]),
580-
(inputnode, ds_xform, [('xform', 'in_file'),
581-
('source_files', 'source_file')]),
573+
(inputnode, ds_xform, [
574+
('xform', 'in_file'),
575+
('source_files', 'source_file'),
576+
('metadata', 'meta_dict'),
577+
]),
582578
(sources, ds_xform, [('out', 'Sources')]),
583579
(ds_xform, outputnode, [('out_file', 'xform')]),
584-
])
585-
# fmt:on
580+
]) # fmt:skip
586581

587582
return workflow
588583

@@ -624,15 +619,13 @@ def init_ds_hmc_wf(
624619
run_without_submitting=True,
625620
)
626621

627-
# fmt:off
628622
workflow.connect([
629623
(inputnode, sources, [('source_files', 'in1')]),
630624
(inputnode, ds_xforms, [('xforms', 'in_file'),
631625
('source_files', 'source_file')]),
632626
(sources, ds_xforms, [('out', 'Sources')]),
633627
(ds_xforms, outputnode, [('out_file', 'xforms')]),
634-
])
635-
# fmt:on
628+
]) # fmt:skip
636629

637630
return workflow
638631

@@ -1010,15 +1003,13 @@ def init_bold_preproc_report_wf(
10101003
mem_gb=DEFAULT_MEMORY_MIN_GB,
10111004
run_without_submitting=True,
10121005
)
1013-
# fmt:off
10141006
workflow.connect([
10151007
(inputnode, ds_report_bold, [('name_source', 'source_file')]),
10161008
(inputnode, pre_tsnr, [('in_pre', 'in_file')]),
10171009
(inputnode, pos_tsnr, [('in_post', 'in_file')]),
10181010
(pre_tsnr, bold_rpt, [('stddev_file', 'before')]),
10191011
(pos_tsnr, bold_rpt, [('stddev_file', 'after')]),
10201012
(bold_rpt, ds_report_bold, [('out_report', 'in_file')]),
1021-
])
1022-
# fmt:on
1013+
]) # fmt:skip
10231014

10241015
return workflow

fmriprep/workflows/bold/registration.py

Lines changed: 76 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,8 @@ def init_bold_reg_wf(
128128
Affine transform from T1 space to BOLD space (ITK format)
129129
fallback
130130
Boolean indicating whether BBR was rejected (mri_coreg registration returned)
131+
metadata
132+
Output metadata from the registration workflow
131133
132134
See Also
133135
--------
@@ -154,7 +156,7 @@ def init_bold_reg_wf(
154156
)
155157

156158
outputnode = pe.Node(
157-
niu.IdentityInterface(fields=['itk_bold_to_t1', 'itk_t1_to_bold', 'fallback']),
159+
niu.IdentityInterface(fields=['itk_bold_to_t1', 'itk_t1_to_bold', 'fallback', 'metadata']),
158160
name='outputnode',
159161
)
160162

@@ -188,6 +190,7 @@ def init_bold_reg_wf(
188190
('outputnode.itk_bold_to_t1', 'itk_bold_to_t1'),
189191
('outputnode.itk_t1_to_bold', 'itk_t1_to_bold'),
190192
('outputnode.fallback', 'fallback'),
193+
('outputnode.metadata', 'metadata'),
191194
]),
192195
]) # fmt:skip
193196

@@ -268,11 +271,14 @@ def init_bbreg_wf(
268271
Affine transform from T1 space to BOLD space (ITK format)
269272
fallback
270273
Boolean indicating whether BBR was rejected (mri_coreg registration returned)
274+
metadata
275+
Output metadata from the registration workflow
271276
272277
"""
273278
from nipype.interfaces.freesurfer import BBRegister
274279
from niworkflows.engine.workflows import LiterateWorkflow as Workflow
275280
from niworkflows.interfaces.nitransforms import ConcatenateXFMs
281+
from niworkflows.interfaces.utility import DictMerge
276282

277283
from fmriprep.interfaces.patches import FreeSurferSource, MRICoreg
278284

@@ -309,7 +315,7 @@ def init_bbreg_wf(
309315
name='inputnode',
310316
)
311317
outputnode = pe.Node(
312-
niu.IdentityInterface(['itk_bold_to_t1', 'itk_t1_to_bold', 'fallback']),
318+
niu.IdentityInterface(['itk_bold_to_t1', 'itk_t1_to_bold', 'fallback', 'metadata']),
313319
name='outputnode',
314320
)
315321

@@ -341,6 +347,8 @@ def init_bbreg_wf(
341347
dof=bold2anat_dof,
342348
contrast_type='t2',
343349
out_lta_file=True,
350+
# Bug in nipype prevents using init_cost_file=True
351+
init_cost_file='bbregister.initcost',
344352
),
345353
name='bbregister',
346354
mem_gb=12,
@@ -357,6 +365,12 @@ def init_bbreg_wf(
357365
merge_ltas = pe.Node(niu.Merge(2), name='merge_ltas', run_without_submitting=True)
358366
concat_xfm = pe.Node(ConcatenateXFMs(inverse=True), name='concat_xfm')
359367

368+
# Set up GeneratedBy metadata and add a merge node for cost, if available
369+
gen_by = pe.Node(niu.Merge(2), run_without_submitting=True, name='gen_by')
370+
select_gen = pe.Node(niu.Select(index=0), run_without_submitting=True, name='select_gen')
371+
metadata = pe.Node(niu.Merge(2), run_without_submitting=True, name='metadata')
372+
merge_meta = pe.Node(DictMerge(), run_without_submitting=True, name='merge_meta')
373+
360374
workflow.connect([
361375
(inputnode, merge_ltas, [('fsnative2t1w_xfm', 'in2')]),
362376
# Wire up the co-registration alternatives
@@ -365,10 +379,20 @@ def init_bbreg_wf(
365379
(merge_ltas, concat_xfm, [('out', 'in_xfms')]),
366380
(concat_xfm, outputnode, [('out_xfm', 'itk_bold_to_t1')]),
367381
(concat_xfm, outputnode, [('out_inv', 'itk_t1_to_bold')]),
382+
# Wire up the metadata alternatives
383+
(gen_by, select_gen, [('out', 'inlist')]),
384+
(select_gen, metadata, [('out', 'in1')]),
385+
(metadata, merge_meta, [('out', 'in_dicts')]),
386+
(merge_meta, outputnode, [('out_dict', 'metadata')]),
368387
]) # fmt:skip
369388

370389
# Do not initialize with header, use mri_coreg
371390
if bold2anat_init != 'header':
391+
gen_by.inputs.in2 = {
392+
'GeneratedBy': [
393+
{'Name': 'mri_coreg', 'Version': mri_coreg.interface.version or '<unknown>'}
394+
]
395+
}
372396
workflow.connect([
373397
(inputnode, mri_coreg, [('subjects_dir', 'subjects_dir'),
374398
('subject_id', 'subject_id'),
@@ -400,6 +424,26 @@ def init_bbreg_wf(
400424
(bbregister, transforms, [('out_lta_file', 'in1')]),
401425
]) # fmt:skip
402426

427+
gen_by.inputs.in1 = {
428+
'GeneratedBy': [
429+
{'Name': 'bbregister', 'Version': bbregister.interface.version or '<unknown>'}
430+
]
431+
}
432+
433+
costs = pe.Node(niu.Merge(2), run_without_submitting=True, name='costs')
434+
select_cost = pe.Node(niu.Select(index=0), run_without_submitting=True, name='select_cost')
435+
read_cost = pe.Node(niu.Function(function=_read_cost), name='read_cost')
436+
437+
workflow.connect([
438+
(bbregister, costs, [
439+
('min_cost_file', 'in1'),
440+
('init_cost_file', 'in2'),
441+
]),
442+
(costs, select_cost, [('out', 'inlist')]),
443+
(select_cost, read_cost, [('out', 'cost_file')]),
444+
(read_cost, metadata, [('out', 'in2')]),
445+
]) # fmt:skip
446+
403447
# Short-circuit workflow building, use boundary-based registration
404448
if use_bbr is True:
405449
outputnode.inputs.fallback = False
@@ -413,6 +457,8 @@ def init_bbreg_wf(
413457
(transforms, compare_transforms, [('out', 'lta_list')]),
414458
(compare_transforms, outputnode, [('out', 'fallback')]),
415459
(compare_transforms, select_transform, [('out', 'index')]),
460+
(compare_transforms, select_gen, [('out', 'index')]),
461+
(compare_transforms, select_cost, [('out', 'index')]),
416462
]) # fmt:skip
417463

418464
return workflow
@@ -493,6 +539,8 @@ def init_fsl_bbr_wf(
493539
Affine transform from T1 space to BOLD space (ITK format)
494540
fallback
495541
Boolean indicating whether BBR was rejected (rigid FLIRT registration returned)
542+
metadata
543+
Output metadata from the registration workflow
496544
497545
"""
498546
from nipype.interfaces.freesurfer import MRICoreg
@@ -532,7 +580,7 @@ def init_fsl_bbr_wf(
532580
name='inputnode',
533581
)
534582
outputnode = pe.Node(
535-
niu.IdentityInterface(['itk_bold_to_t1', 'itk_t1_to_bold', 'fallback']),
583+
niu.IdentityInterface(['itk_bold_to_t1', 'itk_t1_to_bold', 'fallback', 'metadata']),
536584
name='outputnode',
537585
)
538586

@@ -549,6 +597,9 @@ def init_fsl_bbr_wf(
549597
'T2w intermediate for FSL is not implemented, registering with T1w instead.'
550598
)
551599

600+
metadata = pe.Node(niu.Merge(2), run_without_submitting=True, name='metadata')
601+
select_meta = pe.Node(niu.Select(index=0), run_without_submitting=True, name='select_meta')
602+
552603
# Mask T1w_preproc with T1w_mask to make T1w_brain
553604
mask_t1w_brain = pe.Node(ApplyMask(), name='mask_t1w_brain')
554605

@@ -565,6 +616,12 @@ def init_fsl_bbr_wf(
565616
mem_gb=DEFAULT_MEMORY_MIN_GB,
566617
)
567618

619+
metadata.inputs.in2 = {
620+
'GeneratedBy': [
621+
{'Name': 'mri_coreg', 'Version': mri_coreg.interface.version or '<unknown>'}
622+
]
623+
}
624+
568625
workflow.connect([
569626
(inputnode, mask_t1w_brain, [
570627
('t1w_preproc', 'in_file'),
@@ -578,6 +635,9 @@ def init_fsl_bbr_wf(
578635
('out_xfm', 'itk_bold_to_t1'),
579636
('out_inv', 'itk_t1_to_bold'),
580637
]),
638+
# Wire up the metadata alternatives
639+
(metadata, select_meta, [('out', 'inlist')]),
640+
(select_meta, outputnode, [('out', 'metadata')]),
581641
]) # fmt:skip
582642

583643
# Short-circuit workflow building, use rigid registration
@@ -604,6 +664,10 @@ def init_fsl_bbr_wf(
604664
LOGGER.warning('FSLDIR unset - using packaged BBR schedule')
605665
flt_bbr.inputs.schedule = data.load('flirtsch/bbr.sch')
606666

667+
metadata.inputs.in1 = {
668+
'GeneratedBy': [{'Name': 'flirt', 'Version': flt_bbr.interface.version or '<unknown>'}]
669+
}
670+
607671
workflow.connect([
608672
(inputnode, wm_mask, [('t1w_dseg', 'in_seg')]),
609673
(inputnode, flt_bbr, [('in_file', 'in_file')]),
@@ -658,6 +722,8 @@ def init_fsl_bbr_wf(
658722
(transforms, select_transform, [('out', 'inlist')]),
659723
(compare_transforms, select_transform, [('out', 'index')]),
660724
(select_transform, xfm2itk, [('out', 'in_xfm')]),
725+
# Select metadata
726+
(compare_transforms, select_meta, [('out', 'index')]),
661727
]) # fmt:skip
662728

663729
return workflow
@@ -749,3 +815,10 @@ def _conditional_downsampling(in_file, in_mask, zoom_th=4.0):
749815
nb.Nifti1Image(newmaskdata, newmask.affine, hdr).to_filename(out_mask)
750816

751817
return str(out_file), str(out_mask)
818+
819+
820+
def _read_cost(cost_file) -> dict[str, float]:
821+
"""Read a cost from a file."""
822+
# Cost file contains mincost, WM intensity, Ctx intensity, Pct Contrast
823+
with open(cost_file) as fobj:
824+
return {'FinalCost': float(fobj.read().split()[0])}

0 commit comments

Comments
 (0)