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

Support complex-valued dwidenoising #679

Merged
merged 28 commits into from
Feb 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
56772f6
Limit DWI data to magnitude and collect phase sep.
tsalo Jan 24, 2024
f1bea5f
Draft steps to do complex dwidenoise.
tsalo Jan 24, 2024
3d6b10d
Feed in dwidenoisecomplex throughout package.
tsalo Jan 24, 2024
3d1ce20
Fix up some interfaces.
tsalo Jan 24, 2024
a6bc0a3
Fix BIDSDataGrabber.
tsalo Jan 25, 2024
970f367
Keep working.
tsalo Jan 25, 2024
88844cf
Add docstrings.
tsalo Jan 25, 2024
a305138
Grab phase within init_merge_and_denoise_wf.
tsalo Jan 25, 2024
ad1974c
Simplify boilerplate.
tsalo Jan 25, 2024
9f06f2a
Remove unused phase_available.
tsalo Jan 25, 2024
35df866
Use layout to get file's metadata.
tsalo Jan 26, 2024
0774bc3
Merge remote-tracking branch 'upstream/master' into phase
tsalo Jan 26, 2024
b182f12
Merge remote-tracking branch 'upstream/master' into phase
tsalo Jan 26, 2024
ddea10e
Merge remote-tracking branch 'upstream/master' into phase
tsalo Jan 28, 2024
2aef726
Fix connection in phase denoising.
tsalo Jan 29, 2024
39c50ff
Generate out_file names in interfaces.
tsalo Jan 29, 2024
310d9f2
Reduce memory usage of PhaseToRad interface.
tsalo Jan 30, 2024
16aeb98
Merge remote-tracking branch 'upstream/master' into phase
tsalo Feb 2, 2024
e09cfb5
Use updated conversion code.
tsalo Feb 7, 2024
ae88d98
Merge remote-tracking branch 'upstream/master' into phase
tsalo Feb 12, 2024
f155c35
Merge remote-tracking branch 'upstream/master' into phase
tsalo Feb 12, 2024
d9c8bdb
Merge remote-tracking branch 'upstream/master' into phase
tsalo Feb 12, 2024
2e04518
Fix imports.
tsalo Feb 12, 2024
3d3588f
Merge remote-tracking branch 'upstream/master' into phase
tsalo Feb 22, 2024
09394f3
Rerun black and isort.
tsalo Feb 22, 2024
30aab41
Merge remote-tracking branch 'upstream/master' into phase
tsalo Feb 28, 2024
ae8acc7
Update docstrings to reflect Nipreps licensing.
tsalo Feb 29, 2024
2e983b5
Strict phase file matching.
tsalo Feb 29, 2024
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
10 changes: 7 additions & 3 deletions qsiprep/cli/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -284,9 +284,13 @@ def get_parser():
action="store",
nargs="+",
default=[],
choices=["fieldmaps"],
help="ignore selected aspects of the input dataset to disable "
"corresponding parts of the workflow (a space delimited list)",
choices=["fieldmaps", "phase"],
help=(
"ignore selected aspects of the input dataset to disable "
"corresponding parts of the workflow (a space delimited list). "
'Ignoring "phase" will disable complex-valued denoising, '
"when phase DWI data are available."
),
)
g_conf.add_argument(
"--longitudinal",
Expand Down
152 changes: 152 additions & 0 deletions qsiprep/interfaces/dwi_merge.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import json
import os.path as op

import nibabel as nb
import numpy as np
import pandas as pd
from nilearn.image import concat_imgs, index_img, iter_img, load_img, math_img
Expand Down Expand Up @@ -714,3 +715,154 @@ def create_provenance_dataframe(
image_df = pd.concat(series_confounds, axis=0, ignore_index=True)
image_df["original_file"] = bids_sources
return image_df


class _PhaseToRadInputSpec(BaseInterfaceInputSpec):
"""Output spec for PhaseToRad interface.

STATEMENT OF CHANGES: This class is derived from sources licensed under the Apache-2.0 terms,
and the code has been changed.

Notes
-----
The code is derived from
https://github.com/nipreps/sdcflows/blob/c6cd42944f4b6d638716ce020ffe51010e9eb58a/\
sdcflows/utils/phasemanip.py#L26.

License
-------
ORIGINAL WORK'S ATTRIBUTION NOTICE:

Copyright 2021 The NiPreps Developers <[email protected]>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

We support and encourage derived works from this project, please read
about our expectations at

https://www.nipreps.org/community/licensing/

"""

phase_file = File(exists=True, mandatory=True)


class _PhaseToRadOutputSpec(TraitedSpec):
"""Output spec for PhaseToRad interface.

STATEMENT OF CHANGES: This class is derived from sources licensed under the Apache-2.0 terms,
and the code has been changed.

Notes
-----
The code is derived from
https://github.com/nipreps/sdcflows/blob/c6cd42944f4b6d638716ce020ffe51010e9eb58a/\
sdcflows/utils/phasemanip.py#L26.

License
-------
ORIGINAL WORK'S ATTRIBUTION NOTICE:

Copyright 2021 The NiPreps Developers <[email protected]>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

We support and encourage derived works from this project, please read
about our expectations at

https://www.nipreps.org/community/licensing/

"""

phase_file = File(exists=True)


class PhaseToRad(SimpleInterface):
"""Convert phase image from arbitrary units (au) to radians.

This method assumes that the phase image's minimum and maximum values correspond to
-pi and pi, respectively, and scales the image to be between 0 and 2*pi.

STATEMENT OF CHANGES: This class is derived from sources licensed under the Apache-2.0 terms,
and the code has not been changed.

Notes
-----
The code is derived from
https://github.com/nipreps/sdcflows/blob/c6cd42944f4b6d638716ce020ffe51010e9eb58a/\
sdcflows/utils/phasemanip.py#L26.

License
-------
ORIGINAL WORK'S ATTRIBUTION NOTICE:

Copyright 2021 The NiPreps Developers <[email protected]>
mattcieslak marked this conversation as resolved.
Show resolved Hide resolved

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

We support and encourage derived works from this project, please read
about our expectations at

https://www.nipreps.org/community/licensing/

"""

input_spec = _PhaseToRadInputSpec
output_spec = _PhaseToRadOutputSpec

def _run_interface(self, runtime):
im = nb.load(self.inputs.phase_file)
data = im.get_fdata(caching="unchanged") # Read as float64 for safety
hdr = im.header.copy()

# Rescale to [0, 2*pi]
data = (data - data.min()) * (2 * np.pi / (data.max() - data.min()))

# Round to float32 and clip
data = np.clip(np.float32(data), 0.0, 2 * np.pi)

hdr.set_data_dtype(np.float32)
hdr.set_xyzt_units("mm")

# Set the output file name
self._results["phase_file"] = fname_presuffix(
self.inputs.phase_file,
suffix="_rad.nii.gz",
newpath=runtime.cwd,
use_ext=False,
)

# Save the output image
nb.Nifti1Image(data, None, hdr).to_filename(self._results["phase_file"])

return runtime
51 changes: 51 additions & 0 deletions qsiprep/interfaces/mrtrix.py
Original file line number Diff line number Diff line change
Expand Up @@ -1365,3 +1365,54 @@ class TransformHeader(CommandLine):
input_spec = _TransformHeaderInputSpec
output_spec = _TransformHeaderOutputSpec
_cmd = "mrtransform -strides -1,-2,3"


class _PolarToComplexInputSpec(CommandLineInputSpec):
mag_file = traits.File(exists=True, mandatory=True, position=0, argstr="%s")
phase_file = traits.File(exists=True, mandatory=True, position=1, argstr="%s")
out_file = traits.File(
exists=False,
name_source="mag_file",
name_template="%s_complex.nii.gz",
keep_extension=False,
position=-1,
argstr="-polar %s",
)


class _PolarToComplexOutputSpec(TraitedSpec):
out_file = File(exists=True)


class PolarToComplex(CommandLine):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you remember where you found the mrcalc commands? Was it on the mrtrix forum?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"""Convert a magnitude and phase image pair to a single complex image using mrcalc."""

input_spec = _PolarToComplexInputSpec
output_spec = _PolarToComplexOutputSpec

_cmd = "mrcalc"


class _ComplexToMagnitudeInputSpec(CommandLineInputSpec):
complex_file = traits.File(exists=True, mandatory=True, position=0, argstr="%s")
out_file = traits.File(
exists=False,
name_source="complex_file",
name_template="%s_mag.nii.gz",
keep_extension=False,
position=-1,
argstr="-abs %s",
)


class _ComplexToMagnitudeOutputSpec(TraitedSpec):
out_file = File(exists=True)


class ComplexToMagnitude(CommandLine):
"""Extract the magnitude portion of a complex image using mrcalc."""

input_spec = _ComplexToMagnitudeInputSpec
output_spec = _ComplexToMagnitudeOutputSpec

_cmd = "mrcalc"
7 changes: 2 additions & 5 deletions qsiprep/utils/bids.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,10 +157,7 @@ def collect_participants(bids_dir, participant_label=None, strict=False, bids_va


def collect_data(bids_dir, participant_label, filters=None, bids_validate=True):
"""
Uses pybids to retrieve the input data for a given participant

"""
"""Use pybids to retrieve the input data for a given participant."""
if isinstance(bids_dir, BIDSLayout):
layout = bids_dir
else:
Expand All @@ -173,7 +170,7 @@ def collect_data(bids_dir, participant_label, filters=None, bids_validate=True):
"t2w": {"datatype": "anat", "suffix": "T2w"},
"t1w": {"datatype": "anat", "suffix": "T1w"},
"roi": {"datatype": "anat", "suffix": "roi"},
"dwi": {"datatype": "dwi", "suffix": "dwi"},
"dwi": {"datatype": "dwi", "part": ["mag", None], "suffix": "dwi"},
}
bids_filters = filters or {}
for acq, entities in bids_filters.items():
Expand Down
1 change: 0 additions & 1 deletion qsiprep/utils/grouping.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ def group_dwi_scans(
A dict where the keys are the BIDS derivatives name of the output file after
concatenation. The values are lists of dwi files in that group.
"""

# Handle the grouping of multiple dwi files within a session
dwi_session_groups = get_session_groups(bids_layout, subject_data, combine_scans)

Expand Down
10 changes: 6 additions & 4 deletions qsiprep/workflows/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,8 +158,7 @@ def init_qsiprep_wf(
anatomical_contrst : str
Which contrast to use for the anatomical reference
ignore : list
Preprocessing steps to skip (may include "slicetiming",
"fieldmaps")
Preprocessing steps to skip (may include "slicetiming", "fieldmaps", "phase").
low_mem : bool
Write uncompressed .nii files in some cases to reduce memory usage
anat_only : bool
Expand Down Expand Up @@ -401,7 +400,7 @@ def init_single_subject_wf(
name : str
Name of workflow
ignore : list
Preprocessing steps to skip (may include "sbref", "fieldmaps")
Preprocessing steps to skip (may include "sbref", "fieldmaps", "phase")
debug : bool
Do inaccurate but fast normalization
low_mem : bool
Expand Down Expand Up @@ -496,7 +495,10 @@ def init_single_subject_wf(
LOGGER.warning("Building a test workflow")
else:
subject_data, layout = collect_data(
bids_dir, subject_id, filters=bids_filters, bids_validate=False
bids_dir,
subject_id,
filters=bids_filters,
bids_validate=False,
)

# Warn about --dwi-only and non-none --anat-modality
Expand Down
2 changes: 2 additions & 0 deletions qsiprep/workflows/dwi/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,8 @@ def init_dwi_preproc_wf(
source_file=source_file,
low_mem=low_mem,
denoise_before_combining=denoise_before_combining,
layout=layout,
ignore=ignore,
omp_nthreads=omp_nthreads,
)
test_pre_hmc_connect = pe.Node(TestInput(), name="test_pre_hmc_connect")
Expand Down
Loading