Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions fmriprep/data/fmap_spec.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"queries": {
"fieldmaps": {
"fieldmap": {
"datatype": "fmap",
"desc": "preproc",
"suffix": "fieldmap",
"extension": [
".nii.gz",
".nii"
]
},
"coeffs": {
"datatype": "fmap",
"desc": ["coeff", "coeff0", "coeff1"],
"suffix": "fieldmap",
"extension": [
".nii.gz",
".nii"
]
},
"magnitude": {
"datatype": "fmap",
"desc": "magnitude",
"suffix": "fieldmap",
"extension": [
".nii.gz",
".nii"
]
}
}
}
}
2 changes: 1 addition & 1 deletion fmriprep/data/io_spec.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
"boldref2anat": {
"datatype": "func",
"from": "boldref",
"to": "anat",
"to": ["anat", "T1w", "T2w"],
"mode": "image",
"suffix": "xfm",
"extension": ".txt"
Expand Down
33 changes: 20 additions & 13 deletions fmriprep/interfaces/reports.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,11 @@
desc='Phase-encoding direction detected',
)
registration = traits.Enum(
'FSL', 'FreeSurfer', mandatory=True, desc='Functional/anatomical registration method'
'FSL',
'FreeSurfer',
'Precomputed',
mandatory=True,
desc='Functional/anatomical registration method',
)
fallback = traits.Bool(desc='Boundary-based registration rejected')
registration_dof = traits.Enum(
Expand Down Expand Up @@ -239,18 +243,21 @@
else:
stc = 'n/a'
# TODO: Add a note about registration_init below?
reg = {
'FSL': [
'FSL <code>flirt</code> with boundary-based registration'
f' (BBR) metric - {dof} dof',
'FSL <code>flirt</code> rigid registration - 6 dof',
],
'FreeSurfer': [
'FreeSurfer <code>bbregister</code> '
f'(boundary-based registration, BBR) - {dof} dof',
f'FreeSurfer <code>mri_coreg</code> - {dof} dof',
],
}[self.inputs.registration][self.inputs.fallback]
if self.inputs.registration == 'Precomputed':
reg = 'Precomputed affine transformation'

Check warning on line 247 in fmriprep/interfaces/reports.py

View check run for this annotation

Codecov / codecov/patch

fmriprep/interfaces/reports.py#L247

Added line #L247 was not covered by tests
else:
reg = {

Check warning on line 249 in fmriprep/interfaces/reports.py

View check run for this annotation

Codecov / codecov/patch

fmriprep/interfaces/reports.py#L249

Added line #L249 was not covered by tests
'FSL': [
'FSL <code>flirt</code> with boundary-based registration'
f' (BBR) metric - {dof} dof',
'FSL <code>flirt</code> rigid registration - 6 dof',
],
'FreeSurfer': [
'FreeSurfer <code>bbregister</code> '
f'(boundary-based registration, BBR) - {dof} dof',
f'FreeSurfer <code>mri_coreg</code> - {dof} dof',
],
}[self.inputs.registration][self.inputs.fallback]

pedir = get_world_pedir(self.inputs.orientation, self.inputs.pe_direction)

Expand Down
9 changes: 9 additions & 0 deletions fmriprep/interfaces/resampling.py
Original file line number Diff line number Diff line change
Expand Up @@ -671,6 +671,15 @@
target.__class__(target.dataobj, projected_affine, target.header),
)
else:
# Hack. Sometimes the reference array is rotated relative to the fieldmap
# and coefficient grids. As far as I know, coefficients are always RAS,
# but good to check before doing this.
if (
nb.aff2axcodes(coefficients[-1].affine)
== ('R', 'A', 'S')
!= nb.aff2axcodes(fmap_reference.affine)
):
fmap_reference = nb.as_closest_canonical(fmap_reference)

Check warning on line 682 in fmriprep/interfaces/resampling.py

View check run for this annotation

Codecov / codecov/patch

fmriprep/interfaces/resampling.py#L682

Added line #L682 was not covered by tests
if not aligned(fmap_reference.affine, coefficients[-1].affine):
raise ValueError('Reference passed is not aligned with spline grids')
reference, _ = ensure_positive_cosines(fmap_reference)
Expand Down
38 changes: 36 additions & 2 deletions fmriprep/utils/bids.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import os
import sys
from collections import defaultdict
from functools import cache
from pathlib import Path

from bids.layout import BIDSLayout
Expand All @@ -38,6 +39,15 @@
from ..data import load as load_data


@cache
def _get_layout(derivatives_dir: Path) -> BIDSLayout:
import niworkflows.data

return BIDSLayout(
derivatives_dir, config=[niworkflows.data.load('nipreps.json')], validate=False
)


def collect_derivatives(
derivatives_dir: Path,
entities: dict,
Expand All @@ -57,8 +67,7 @@
patterns = _patterns

derivs_cache = defaultdict(list, {})
layout = BIDSLayout(derivatives_dir, config=['bids', 'derivatives'], validate=False)
derivatives_dir = Path(derivatives_dir)
layout = _get_layout(derivatives_dir)

# search for both boldrefs
for k, q in spec['baseline'].items():
Expand Down Expand Up @@ -86,6 +95,31 @@
return derivs_cache


def collect_fieldmaps(
derivatives_dir: Path,
entities: dict,
spec: dict | None = None,
):
"""Gather existing derivatives and compose a cache."""
if spec is None:
spec = json.loads(load_data.readable('fmap_spec.json').read_text())['queries']

Check warning on line 105 in fmriprep/utils/bids.py

View check run for this annotation

Codecov / codecov/patch

fmriprep/utils/bids.py#L105

Added line #L105 was not covered by tests

fmap_cache = defaultdict(dict, {})
layout = _get_layout(derivatives_dir)

Check warning on line 108 in fmriprep/utils/bids.py

View check run for this annotation

Codecov / codecov/patch

fmriprep/utils/bids.py#L107-L108

Added lines #L107 - L108 were not covered by tests

fmapids = layout.get_fmapids(**entities)

Check warning on line 110 in fmriprep/utils/bids.py

View check run for this annotation

Codecov / codecov/patch

fmriprep/utils/bids.py#L110

Added line #L110 was not covered by tests

for fmapid in fmapids:
for k, q in spec['fieldmaps'].items():
query = {**entities, **q}
item = layout.get(return_type='filename', fmapid=fmapid, **query)

Check warning on line 115 in fmriprep/utils/bids.py

View check run for this annotation

Codecov / codecov/patch

fmriprep/utils/bids.py#L114-L115

Added lines #L114 - L115 were not covered by tests
if not item:
continue
fmap_cache[fmapid][k] = item[0] if len(item) == 1 else item

Check warning on line 118 in fmriprep/utils/bids.py

View check run for this annotation

Codecov / codecov/patch

fmriprep/utils/bids.py#L117-L118

Added lines #L117 - L118 were not covered by tests

return fmap_cache

Check warning on line 120 in fmriprep/utils/bids.py

View check run for this annotation

Codecov / codecov/patch

fmriprep/utils/bids.py#L120

Added line #L120 was not covered by tests


def write_bidsignore(deriv_dir):
bids_ignore = (
'*.html',
Expand Down
97 changes: 78 additions & 19 deletions fmriprep/workflows/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"""

import os
import re
import sys
import warnings
from copy import deepcopy
Expand Down Expand Up @@ -337,6 +338,9 @@

# allow to run with anat-fast-track on fMRI-only dataset
if 't1w_preproc' in anatomical_cache and not subject_data['t1w']:
config.loggers.workflow.debug(

Check warning on line 341 in fmriprep/workflows/base.py

View check run for this annotation

Codecov / codecov/patch

fmriprep/workflows/base.py#L341

Added line #L341 was not covered by tests
'No T1w image found; using precomputed T1w image: %s', anatomical_cache['t1w_preproc']
)
workflow.connect([
(bidssrc, bids_info, [(('bold', fix_multi_T1w_source_name), 'in_file')]),
(anat_fit_wf, summary, [('outputnode.t1w_preproc', 't1w')]),
Expand Down Expand Up @@ -533,7 +537,21 @@
if config.workflow.anat_only:
return clean_datasinks(workflow)

fmap_estimators, estimator_map = map_fieldmap_estimation(
fmap_cache = {}
if config.execution.derivatives:
from fmriprep.utils.bids import collect_fieldmaps

Check warning on line 542 in fmriprep/workflows/base.py

View check run for this annotation

Codecov / codecov/patch

fmriprep/workflows/base.py#L542

Added line #L542 was not covered by tests

for deriv_dir in config.execution.derivatives.values():
fmaps = collect_fieldmaps(

Check warning on line 545 in fmriprep/workflows/base.py

View check run for this annotation

Codecov / codecov/patch

fmriprep/workflows/base.py#L545

Added line #L545 was not covered by tests
derivatives_dir=deriv_dir,
entities={'subject': subject_id},
)
config.loggers.workflow.debug(

Check warning on line 549 in fmriprep/workflows/base.py

View check run for this annotation

Codecov / codecov/patch

fmriprep/workflows/base.py#L549

Added line #L549 was not covered by tests
'Detected precomputed fieldmaps in %s for fieldmap IDs: %s', deriv_dir, list(fmaps)
)
fmap_cache.update(fmaps)

Check warning on line 552 in fmriprep/workflows/base.py

View check run for this annotation

Codecov / codecov/patch

fmriprep/workflows/base.py#L552

Added line #L552 was not covered by tests

all_estimators, estimator_map = map_fieldmap_estimation(
layout=config.execution.layout,
subject_id=subject_id,
bold_data=bold_runs,
Expand All @@ -543,6 +561,48 @@
filters=config.execution.get().get('bids_filters', {}).get('fmap'),
)

fmap_buffers = {
field: pe.Node(niu.Merge(2), name=f'{field}_merge', run_without_submitting=True)
for field in ['fmap', 'fmap_ref', 'fmap_coeff', 'fmap_mask', 'fmap_id', 'sdc_method']
}

fmap_estimators = []
if all_estimators:
# Find precomputed fieldmaps that apply to this workflow
pared_cache = {}

Check warning on line 572 in fmriprep/workflows/base.py

View check run for this annotation

Codecov / codecov/patch

fmriprep/workflows/base.py#L572

Added line #L572 was not covered by tests
for est in all_estimators:
if found := fmap_cache.get(re.sub(r'[^a-zA-Z0-9]', '', est.bids_id)):
pared_cache[est.bids_id] = found

Check warning on line 575 in fmriprep/workflows/base.py

View check run for this annotation

Codecov / codecov/patch

fmriprep/workflows/base.py#L575

Added line #L575 was not covered by tests
else:
fmap_estimators.append(est)

Check warning on line 577 in fmriprep/workflows/base.py

View check run for this annotation

Codecov / codecov/patch

fmriprep/workflows/base.py#L577

Added line #L577 was not covered by tests

if pared_cache:
config.loggers.workflow.info(

Check warning on line 580 in fmriprep/workflows/base.py

View check run for this annotation

Codecov / codecov/patch

fmriprep/workflows/base.py#L580

Added line #L580 was not covered by tests
'Precomputed B0 field inhomogeneity maps found for the following '
f'{len(pared_cache)} estimator(s): {list(pared_cache)}.'
)

fieldmaps = [fmap['fieldmap'] for fmap in pared_cache.values()]
refs = [fmap['magnitude'] for fmap in pared_cache.values()]
coeffs = [fmap['coeffs'] for fmap in pared_cache.values()]
config.loggers.workflow.debug('Reusing fieldmaps: %s', fieldmaps)
config.loggers.workflow.debug('Reusing references: %s', refs)
config.loggers.workflow.debug('Reusing coefficients: %s', coeffs)

Check warning on line 590 in fmriprep/workflows/base.py

View check run for this annotation

Codecov / codecov/patch

fmriprep/workflows/base.py#L588-L590

Added lines #L588 - L590 were not covered by tests

fmap_buffers['fmap'].inputs.in1 = fieldmaps
fmap_buffers['fmap_ref'].inputs.in1 = refs
fmap_buffers['fmap_coeff'].inputs.in1 = coeffs

Check warning on line 594 in fmriprep/workflows/base.py

View check run for this annotation

Codecov / codecov/patch

fmriprep/workflows/base.py#L592-L594

Added lines #L592 - L594 were not covered by tests

# Note that masks are not emitted. The BOLD-fmap transforms cannot be
# computed with precomputed fieldmaps until we either start emitting masks
# or start skull-stripping references on the fly.
fmap_buffers['fmap_mask'].inputs.in1 = [
pared_cache[fmapid].get('mask', 'MISSING') for fmapid in pared_cache
]
fmap_buffers['fmap_id'].inputs.in1 = list(pared_cache)
fmap_buffers['sdc_method'].inputs.in1 = ['precomputed'] * len(pared_cache)

Check warning on line 603 in fmriprep/workflows/base.py

View check run for this annotation

Codecov / codecov/patch

fmriprep/workflows/base.py#L602-L603

Added lines #L602 - L603 were not covered by tests

# Estimators without precomputed fieldmaps
if fmap_estimators:
config.loggers.workflow.info(
'B0 field inhomogeneity map will be estimated with the following '
Expand Down Expand Up @@ -573,24 +633,31 @@
if node.split('.')[-1].startswith('ds_'):
fmap_wf.get_node(node).interface.out_path_base = ''

workflow.connect([
(fmap_wf, fmap_buffers[field], [
# We get "sdc_method" as "method" from estimator workflows
# All else stays the same, and no other sdc_ prefixes are used
(f'outputnode.{field.removeprefix("sdc_")}', 'in2'),
])
for field in fmap_buffers
]) # fmt:skip

fmap_select_std = pe.Node(
KeySelect(fields=['std2anat_xfm'], key='MNI152NLin2009cAsym'),
name='fmap_select_std',
run_without_submitting=True,
)
if any(estimator.method == fm.EstimatorType.ANAT for estimator in fmap_estimators):
# fmt:off
workflow.connect([
(anat_fit_wf, fmap_select_std, [
('outputnode.std2anat_xfm', 'std2anat_xfm'),
('outputnode.template', 'keys')]),
])
# fmt:on
]) # fmt:skip

for estimator in fmap_estimators:
config.loggers.workflow.info(
f"""\
Setting-up fieldmap "{estimator.bids_id}" ({estimator.method}) with \
Setting up fieldmap "{estimator.bids_id}" ({estimator.method}) with \
<{', '.join(s.path.name for s in estimator.sources)}>"""
)

Expand Down Expand Up @@ -633,7 +700,6 @@
syn_preprocessing_wf.inputs.inputnode.in_epis = sources
syn_preprocessing_wf.inputs.inputnode.in_meta = source_meta

# fmt:off
workflow.connect([
(anat_fit_wf, syn_preprocessing_wf, [
('outputnode.t1w_preproc', 'inputnode.in_anat'),
Expand All @@ -649,8 +715,7 @@
('outputnode.anat_mask', f'in_{estimator.bids_id}.anat_mask'),
('outputnode.sd_prior', f'in_{estimator.bids_id}.sd_prior'),
]),
])
# fmt:on
]) # fmt:skip

# Append the functional section to the existing anatomical excerpt
# That way we do not need to stream down the number of bold datasets
Expand Down Expand Up @@ -718,17 +783,11 @@
),
]),
]) # fmt:skip
if fieldmap_id:
workflow.connect([
(fmap_wf, bold_wf, [
('outputnode.fmap', 'inputnode.fmap'),
('outputnode.fmap_ref', 'inputnode.fmap_ref'),
('outputnode.fmap_coeff', 'inputnode.fmap_coeff'),
('outputnode.fmap_mask', 'inputnode.fmap_mask'),
('outputnode.fmap_id', 'inputnode.fmap_id'),
('outputnode.method', 'inputnode.sdc_method'),
]),
]) # fmt:skip

workflow.connect([
(buffer, bold_wf, [('out', f'inputnode.{field}')])
for field, buffer in fmap_buffers.items()
]) # fmt:skip

if config.workflow.level == 'full':
if template_iterator_wf is not None:
Expand Down
Loading
Loading