Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ENH: Restore resampling BOLD to volumetric templates #3121

Merged
merged 5 commits into from Nov 3, 2023
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
112 changes: 66 additions & 46 deletions fmriprep/utils/transforms.py
@@ -1,4 +1,5 @@
"""Utilities for loading transforms for resampling"""
import warnings
from pathlib import Path

import h5py
Expand Down Expand Up @@ -36,57 +37,76 @@
return chain


def load_ants_h5(filename: Path) -> nt.TransformChain:
"""Load ANTs H5 files as a nitransforms TransformChain"""
affine, warp, warp_affine = parse_combined_hdf5(filename)
warp_transform = nt.DenseFieldTransform(nb.Nifti1Image(warp, warp_affine))
return nt.TransformChain([warp_transform, nt.Affine(affine)])
FIXED_PARAMS = np.array([
193.0, 229.0, 193.0, # Size
96.0, 132.0, -78.0, # Origin
1.0, 1.0, 1.0, # Spacing
-1.0, 0.0, 0.0, # Directions
0.0, -1.0, 0.0,
0.0, 0.0, 1.0,
]) # fmt:skip


def parse_combined_hdf5(h5_fn, to_ras=True):
def load_ants_h5(filename: Path) -> nt.base.TransformBase:
"""Load ANTs H5 files as a nitransforms TransformChain"""
# Borrowed from https://github.com/feilong/process
# process.resample.parse_combined_hdf5()
h = h5py.File(h5_fn)
#
# Changes:
# * Tolerate a missing displacement field
# * Return the original affine without a round-trip
# * Always return a nitransforms TransformChain
#
# This should be upstreamed into nitransforms
h = h5py.File(filename)

Check warning on line 61 in fmriprep/utils/transforms.py

View check run for this annotation

Codecov / codecov/patch

fmriprep/utils/transforms.py#L61

Added line #L61 was not covered by tests
xform = ITKCompositeH5.from_h5obj(h)
affine = xform[0].to_ras()

# nt.Affine
transforms = [nt.Affine(xform[0].to_ras())]

Check warning on line 65 in fmriprep/utils/transforms.py

View check run for this annotation

Codecov / codecov/patch

fmriprep/utils/transforms.py#L65

Added line #L65 was not covered by tests

if '2' not in h['TransformGroup']:
return transforms[0]

Check warning on line 68 in fmriprep/utils/transforms.py

View check run for this annotation

Codecov / codecov/patch

fmriprep/utils/transforms.py#L67-L68

Added lines #L67 - L68 were not covered by tests
effigies marked this conversation as resolved.
Show resolved Hide resolved

transform2 = h['TransformGroup']['2']

Check warning on line 70 in fmriprep/utils/transforms.py

View check run for this annotation

Codecov / codecov/patch

fmriprep/utils/transforms.py#L70

Added line #L70 was not covered by tests

# Confirm these transformations are applicable
assert (
h['TransformGroup']['2']['TransformType'][:][0] == b'DisplacementFieldTransform_float_3_3'
)
assert np.array_equal(
h['TransformGroup']['2']['TransformFixedParameters'][:],
np.array(
[
193.0,
229.0,
193.0,
96.0,
132.0,
-78.0,
1.0,
1.0,
1.0,
-1.0,
0.0,
0.0,
0.0,
-1.0,
0.0,
0.0,
0.0,
1.0,
]
),
)
if transform2['TransformType'][:][0] != b'DisplacementFieldTransform_float_3_3':
msg = 'Unknown transform type [2]\n'
for i in h['TransformGroup'].keys():
msg += f'[{i}]: {h["TransformGroup"][i]["TransformType"][:][0]}\n'
raise ValueError(msg)

Check warning on line 77 in fmriprep/utils/transforms.py

View check run for this annotation

Codecov / codecov/patch

fmriprep/utils/transforms.py#L73-L77

Added lines #L73 - L77 were not covered by tests

fixed_params = transform2['TransformFixedParameters'][:]
if not np.array_equal(fixed_params, FIXED_PARAMS):
msg = 'Unexpected fixed parameters\n'
msg += f'Expected: {FIXED_PARAMS}\n'
msg += f'Found: {fixed_params}'
if not np.array_equal(fixed_params[6:], FIXED_PARAMS[6:]):
raise ValueError(msg)
warnings.warn(msg)

Check warning on line 86 in fmriprep/utils/transforms.py

View check run for this annotation

Codecov / codecov/patch

fmriprep/utils/transforms.py#L79-L86

Added lines #L79 - L86 were not covered by tests
mgxd marked this conversation as resolved.
Show resolved Hide resolved

shape = tuple(fixed_params[:3].astype(int))

Check warning on line 88 in fmriprep/utils/transforms.py

View check run for this annotation

Codecov / codecov/patch

fmriprep/utils/transforms.py#L88

Added line #L88 was not covered by tests
warp = h['TransformGroup']['2']['TransformParameters'][:]
warp = warp.reshape((193, 229, 193, 3)).transpose(2, 1, 0, 3)
warp = warp.reshape((*shape, 3)).transpose(2, 1, 0, 3)

Check warning on line 90 in fmriprep/utils/transforms.py

View check run for this annotation

Codecov / codecov/patch

fmriprep/utils/transforms.py#L90

Added line #L90 was not covered by tests
warp *= np.array([-1, -1, 1])
warp_affine = np.array(
[
[1.0, 0.0, 0.0, -96.0],
[0.0, 1.0, 0.0, -132.0],
[0.0, 0.0, 1.0, -78.0],
[0.0, 0.0, 0.0, 1.0],
]
)
return affine, warp, warp_affine

warp_affine = np.eye(4)
warp_affine[:3, :3] = fixed_params[9:].reshape((3, 3))
warp_affine[:3, 3] = fixed_params[3:6]
lps_to_ras = np.eye(4) * np.array([-1, -1, 1, 1])
warp_affine = lps_to_ras @ warp_affine
if np.array_equal(fixed_params, FIXED_PARAMS):

Check warning on line 98 in fmriprep/utils/transforms.py

View check run for this annotation

Codecov / codecov/patch

fmriprep/utils/transforms.py#L93-L98

Added lines #L93 - L98 were not covered by tests
# Confirm that we construct the right affine when fixed parameters are known
assert np.array_equal(

Check warning on line 100 in fmriprep/utils/transforms.py

View check run for this annotation

Codecov / codecov/patch

fmriprep/utils/transforms.py#L100

Added line #L100 was not covered by tests
warp_affine,
np.array(
[
[1.0, 0.0, 0.0, -96.0],
[0.0, 1.0, 0.0, -132.0],
[0.0, 0.0, 1.0, -78.0],
[0.0, 0.0, 0.0, 1.0],
]
),
)
transforms.insert(0, nt.DenseFieldTransform(nb.Nifti1Image(warp, warp_affine)))
return nt.TransformChain(transforms)

Check warning on line 112 in fmriprep/utils/transforms.py

View check run for this annotation

Codecov / codecov/patch

fmriprep/utils/transforms.py#L111-L112

Added lines #L111 - L112 were not covered by tests
28 changes: 25 additions & 3 deletions fmriprep/workflows/base.py
Expand Up @@ -151,6 +151,7 @@
from niworkflows.utils.misc import fix_multi_T1w_source_name
from niworkflows.utils.spaces import Reference
from smriprep.workflows.anatomical import init_anat_fit_wf
from smriprep.workflows.outputs import init_template_iterator_wf

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

View check run for this annotation

Codecov / codecov/patch

fmriprep/workflows/base.py#L154

Added line #L154 was not covered by tests

from fmriprep.workflows.bold.base import init_bold_wf

Expand Down Expand Up @@ -310,7 +311,6 @@
skull_strip_fixed_seed=config.workflow.skull_strip_fixed_seed,
)

# fmt:off
workflow.connect([
(inputnode, anat_fit_wf, [('subjects_dir', 'inputnode.subjects_dir')]),
(bidssrc, bids_info, [(('t1w', fix_multi_T1w_source_name), 'in_file')]),
Expand All @@ -329,8 +329,18 @@
(bidssrc, ds_report_about, [(('t1w', fix_multi_T1w_source_name), 'source_file')]),
(summary, ds_report_summary, [('out_report', 'in_file')]),
(about, ds_report_about, [('out_report', 'in_file')]),
])
# fmt:on
]) # fmt:skip

# Set up the template iterator once, if used
if config.workflow.level == "full":
if spaces.get_spaces(nonstandard=False, dim=(3,)):
template_iterator_wf = init_template_iterator_wf(spaces=spaces)
workflow.connect([

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

View check run for this annotation

Codecov / codecov/patch

fmriprep/workflows/base.py#L335-L338

Added lines #L335 - L338 were not covered by tests
(anat_fit_wf, template_iterator_wf, [
('outputnode.template', 'inputnode.template'),
('outputnode.anat2std_xfm', 'inputnode.anat2std_xfm'),
]),
]) # fmt:skip

if config.workflow.anat_only:
return clean_datasinks(workflow)
Expand Down Expand Up @@ -510,6 +520,18 @@
]),
]) # fmt:skip

if config.workflow.level == "full":
workflow.connect([

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

View check run for this annotation

Codecov / codecov/patch

fmriprep/workflows/base.py#L523-L524

Added lines #L523 - L524 were not covered by tests
(template_iterator_wf, bold_wf, [
("outputnode.anat2std_xfm", "inputnode.anat2std_xfm"),
("outputnode.space", "inputnode.std_space"),
("outputnode.resolution", "inputnode.std_resolution"),
("outputnode.cohort", "inputnode.std_cohort"),
("outputnode.std_t1w", "inputnode.std_t1w"),
("outputnode.std_mask", "inputnode.std_mask"),
]),
]) # fmt:skip

return clean_datasinks(workflow)


Expand Down
144 changes: 60 additions & 84 deletions fmriprep/workflows/bold/base.py
Expand Up @@ -198,6 +198,13 @@
"fmap_mask",
"fmap_id",
"sdc_method",
# Volumetric templates
"anat2std_xfm",
"std_space",
"std_resolution",
"std_cohort",
"std_t1w",
"std_mask",
],
),
name="inputnode",
Expand Down Expand Up @@ -381,6 +388,59 @@
(bold_anat_wf, ds_bold_t1_wf, [('outputnode.bold_file', 'inputnode.bold')]),
]) # fmt:skip

if spaces.get_spaces(nonstandard=False, dim=(3,)):

Check warning on line 391 in fmriprep/workflows/bold/base.py

View check run for this annotation

Codecov / codecov/patch

fmriprep/workflows/bold/base.py#L391

Added line #L391 was not covered by tests
# Missing:
# * Clipping BOLD after resampling
# * Resampling parcellations
mgxd marked this conversation as resolved.
Show resolved Hide resolved
bold_std_wf = init_bold_volumetric_resample_wf(

Check warning on line 395 in fmriprep/workflows/bold/base.py

View check run for this annotation

Codecov / codecov/patch

fmriprep/workflows/bold/base.py#L395

Added line #L395 was not covered by tests
metadata=all_metadata[0],
fieldmap_id=fieldmap_id if not multiecho else None,
omp_nthreads=omp_nthreads,
name='bold_std_wf',
)
ds_bold_std_wf = init_ds_volumes_wf(

Check warning on line 401 in fmriprep/workflows/bold/base.py

View check run for this annotation

Codecov / codecov/patch

fmriprep/workflows/bold/base.py#L401

Added line #L401 was not covered by tests
bids_root=str(config.execution.bids_dir),
output_dir=fmriprep_dir,
multiecho=multiecho,
metadata=all_metadata[0],
name='ds_bold_std_wf',
)
ds_bold_std_wf.inputs.inputnode.source_files = bold_series

Check warning on line 408 in fmriprep/workflows/bold/base.py

View check run for this annotation

Codecov / codecov/patch

fmriprep/workflows/bold/base.py#L408

Added line #L408 was not covered by tests

workflow.connect([

Check warning on line 410 in fmriprep/workflows/bold/base.py

View check run for this annotation

Codecov / codecov/patch

fmriprep/workflows/bold/base.py#L410

Added line #L410 was not covered by tests
(inputnode, bold_std_wf, [
("std_t1w", "inputnode.target_ref_file"),
("std_mask", "inputnode.target_mask"),
("anat2std_xfm", "inputnode.anat2std_xfm"),
("fmap_ref", "inputnode.fmap_ref"),
("fmap_coeff", "inputnode.fmap_coeff"),
("fmap_id", "inputnode.fmap_id"),
]),
(bold_fit_wf, bold_std_wf, [
("outputnode.coreg_boldref", "inputnode.bold_ref_file"),
("outputnode.boldref2fmap_xfm", "inputnode.boldref2fmap_xfm"),
("outputnode.boldref2anat_xfm", "inputnode.boldref2anat_xfm"),
]),
(bold_native_wf, bold_std_wf, [
("outputnode.bold_minimal", "inputnode.bold_file"),
("outputnode.motion_xfm", "inputnode.motion_xfm"),
]),
(inputnode, ds_bold_std_wf, [
('std_t1w', 'inputnode.ref_file'),
('anat2std_xfm', 'inputnode.anat2std_xfm'),
('std_space', 'inputnode.space'),
('std_resolution', 'inputnode.resolution'),
('std_cohort', 'inputnode.cohort'),
]),
(bold_fit_wf, ds_bold_std_wf, [
('outputnode.bold_mask', 'inputnode.bold_mask'),
('outputnode.coreg_boldref', 'inputnode.bold_ref'),
('outputnode.boldref2anat_xfm', 'inputnode.boldref2anat_xfm'),
]),
(bold_native_wf, ds_bold_std_wf, [('outputnode.t2star_map', 'inputnode.t2star')]),
(bold_std_wf, ds_bold_std_wf, [('outputnode.bold_file', 'inputnode.bold')]),
]) # fmt:skip

# Fill-in datasinks of reportlets seen so far
for node in workflow.list_node_names():
if node.split(".")[-1].startswith("ds_report"):
Expand Down Expand Up @@ -629,90 +689,6 @@
)
bold_confounds_wf.get_node("inputnode").inputs.t1_transform_flags = [False]

if spaces.get_spaces(nonstandard=False, dim=(3,)):
# Apply transforms in 1 shot
bold_std_trans_wf = init_bold_std_trans_wf(
freesurfer=freesurfer,
mem_gb=mem_gb["resampled"],
omp_nthreads=omp_nthreads,
spaces=spaces,
multiecho=multiecho,
name="bold_std_trans_wf",
use_compression=not config.execution.low_mem,
)
bold_std_trans_wf.inputs.inputnode.fieldwarp = "identity"

# fmt:off
workflow.connect([
(inputnode, bold_std_trans_wf, [
("template", "inputnode.templates"),
("anat2std_xfm", "inputnode.anat2std_xfm"),
("bold_file", "inputnode.name_source"),
("t1w_aseg", "inputnode.bold_aseg"),
("t1w_aparc", "inputnode.bold_aparc"),
]),
(bold_final, bold_std_trans_wf, [
("mask", "inputnode.bold_mask"),
("t2star", "inputnode.t2star"),
]),
(bold_reg_wf, bold_std_trans_wf, [
("outputnode.itk_bold_to_t1", "inputnode.itk_bold_to_t1"),
]),
(bold_std_trans_wf, outputnode, [
("outputnode.bold_std", "bold_std"),
("outputnode.bold_std_ref", "bold_std_ref"),
("outputnode.bold_mask_std", "bold_mask_std"),
]),
])
# fmt:on

if freesurfer:
# fmt:off
workflow.connect([
(bold_std_trans_wf, func_derivatives_wf, [
("outputnode.bold_aseg_std", "inputnode.bold_aseg_std"),
("outputnode.bold_aparc_std", "inputnode.bold_aparc_std"),
]),
(bold_std_trans_wf, outputnode, [
("outputnode.bold_aseg_std", "bold_aseg_std"),
("outputnode.bold_aparc_std", "bold_aparc_std"),
]),
])
# fmt:on

if not multiecho:
# fmt:off
workflow.connect([
(bold_split, bold_std_trans_wf, [("out_files", "inputnode.bold_split")]),
(bold_hmc_wf, bold_std_trans_wf, [
("outputnode.xforms", "inputnode.hmc_xforms"),
]),
])
# fmt:on
else:
# fmt:off
workflow.connect([
(split_opt_comb, bold_std_trans_wf, [("out_files", "inputnode.bold_split")]),
(bold_std_trans_wf, outputnode, [("outputnode.t2star_std", "t2star_std")]),
])
# fmt:on

# Already applied in bold_bold_trans_wf, which inputs to bold_t2s_wf
bold_std_trans_wf.inputs.inputnode.hmc_xforms = "identity"

# fmt:off
# func_derivatives_wf internally parametrizes over snapshotted spaces.
workflow.connect([
(bold_std_trans_wf, func_derivatives_wf, [
("outputnode.template", "inputnode.template"),
("outputnode.spatial_reference", "inputnode.spatial_reference"),
("outputnode.bold_std_ref", "inputnode.bold_std_ref"),
("outputnode.bold_std", "inputnode.bold_std"),
("outputnode.bold_mask_std", "inputnode.bold_mask_std"),
]),
])
# fmt:on

# SURFACES ##################################################################################
# Freesurfer
if freesurfer and freesurfer_spaces:
Expand Down