Skip to content

Commit da1e81f

Browse files
authored
Merge pull request #2130 from oesteban/fix/2129-compcor-masks-reliability
FIX: Revise the reproducibility of CompCor masks
2 parents 1830b25 + bafb714 commit da1e81f

File tree

5 files changed

+252
-128
lines changed

5 files changed

+252
-128
lines changed

fmriprep/data/reports-spec.yml

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -85,11 +85,14 @@ sections:
8585
subtitle: Alignment of functional and anatomical MRI data (surface driven)
8686
- bids: {datatype: figures, desc: rois, suffix: bold}
8787
caption: Brain mask calculated on the BOLD signal (red contour), along with the
88-
masks used for a/tCompCor.<br />The aCompCor mask (magenta contour) is a conservative
89-
CSF and white-matter mask for extracting physiological and movement confounds.
90-
<br />The fCompCor mask (blue contour) contains the top 5% most variable voxels
91-
within a heavily-eroded brain-mask.
92-
subtitle: Brain mask and (temporal/anatomical) CompCor ROIs
88+
regions of interest (ROIs) used in <em>a/tCompCor</em> for extracting
89+
physiological and movement confounding components.<br />
90+
The <em>anatomical CompCor</em> ROI (magenta contour) is a mask combining
91+
CSF and WM (white-matter), where voxels containing a minimal partial volume
92+
of GM have been removed.<br />
93+
The <em>temporal CompCor</em> ROI (blue contour) contains the top 2% most
94+
variable voxels within the brain mask.
95+
subtitle: Brain mask and (anatomical/temporal) CompCor ROIs
9396
- bids:
9497
datatype: figures
9598
desc: '[at]compcor'
@@ -107,10 +110,11 @@ sections:
107110
in the BOLD data. Global signals calculated within the whole-brain (GS), within
108111
the white-matter (WM) and within cerebro-spinal fluid (CSF) show the mean BOLD
109112
signal in their corresponding masks. DVARS and FD show the standardized DVARS
110-
and framewise-displacement measures for each time point.<br />A carpet plot
111-
shows the time series for all voxels within the brain mask, or if ``--cifti-output``
112-
was enabled, all grayordinates. Voxels are grouped into cortical (dark/light blue),
113-
and subcortical (orange) gray matter, cerebellum (green) and white matter and CSF
113+
and framewise-displacement measures for each time point.<br />
114+
A carpet plot shows the time series for all voxels within the brain mask,
115+
or if <code>--cifti-output</code> was enabled, all grayordinates.
116+
Voxels are grouped into cortical (dark/light blue), and subcortical (orange)
117+
gray matter, cerebellum (green) and white matter and CSF
114118
(red), indicated by the color map on the left-hand side.
115119
subtitle: BOLD Summary
116120
- bids: {datatype: figures, desc: 'confoundcorr', suffix: bold}

fmriprep/interfaces/confounds.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,41 @@
1818
from nipype.utils.filemanip import fname_presuffix
1919
from nipype.interfaces.base import (
2020
traits, TraitedSpec, BaseInterfaceInputSpec, File, Directory, isdefined,
21-
SimpleInterface
21+
SimpleInterface, InputMultiObject, OutputMultiObject
2222
)
2323

2424
LOGGER = logging.getLogger('nipype.interface')
2525

2626

27+
class _aCompCorMasksInputSpec(BaseInterfaceInputSpec):
28+
in_vfs = InputMultiObject(File(exists=True), desc="Input volume fractions.")
29+
is_aseg = traits.Bool(False, usedefault=True,
30+
desc="Whether the input volume fractions come from FS' aseg.")
31+
bold_zooms = traits.Tuple(traits.Float, traits.Float, traits.Float, mandatory=True,
32+
desc="BOLD series zooms")
33+
34+
35+
class _aCompCorMasksOutputSpec(TraitedSpec):
36+
out_masks = OutputMultiObject(File(exists=True),
37+
desc="CSF, WM and combined masks, respectively")
38+
39+
40+
class aCompCorMasks(SimpleInterface):
41+
"""Generate masks in T1w space for aCompCor."""
42+
43+
input_spec = _aCompCorMasksInputSpec
44+
output_spec = _aCompCorMasksOutputSpec
45+
46+
def _run_interface(self, runtime):
47+
from ..utils.confounds import acompcor_masks
48+
self._results["out_masks"] = acompcor_masks(
49+
self.inputs.in_vfs,
50+
self.inputs.is_aseg,
51+
self.inputs.bold_zooms,
52+
)
53+
return runtime
54+
55+
2756
class GatherConfoundsInputSpec(BaseInterfaceInputSpec):
2857
signals = File(exists=True, desc='input signals')
2958
dvars = File(exists=True, desc='file containing DVARS')

fmriprep/utils/confounds.py

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
"""Utilities for confounds manipulation."""
2+
3+
4+
def mask2vf(in_file, zooms=None, out_file=None):
5+
"""
6+
Convert a binary mask on a volume fraction map.
7+
8+
The algorithm simply applies a Gaussian filter with the kernel size scaled
9+
by the zooms given as argument.
10+
11+
"""
12+
import numpy as np
13+
import nibabel as nb
14+
from scipy.ndimage import gaussian_filter
15+
16+
img = nb.load(in_file)
17+
imgzooms = np.array(img.header.get_zooms()[:3], dtype=float)
18+
if zooms is None:
19+
zooms = imgzooms
20+
21+
zooms = np.array(zooms, dtype=float)
22+
sigma = 0.5 * (zooms / imgzooms)
23+
24+
data = gaussian_filter(img.get_fdata(dtype=np.float32), sigma=sigma)
25+
26+
max_data = np.percentile(data[data > 0], 99)
27+
data = np.clip(data / max_data, a_min=0, a_max=1)
28+
29+
if out_file is None:
30+
return data
31+
32+
hdr = img.header.copy()
33+
hdr.set_data_dtype(np.float32)
34+
nb.Nifti1Image(data.astype(np.float32), img.affine, hdr).to_filename(out_file)
35+
return out_file
36+
37+
38+
def acompcor_masks(in_files, is_aseg=False, zooms=None):
39+
"""
40+
Generate aCompCor masks.
41+
42+
This function selects the CSF partial volume map from the input,
43+
and generates the WM and combined CSF+WM masks for aCompCor.
44+
45+
The implementation deviates from Behzadi et al.
46+
Their original implementation thresholded the CSF and the WM partial-volume
47+
masks at 0.99 (i.e., 99% of the voxel volume is filled with a particular tissue),
48+
and then binary eroded that 2 voxels:
49+
50+
> Anatomical data were segmented into gray matter, white matter,
51+
> and CSF partial volume maps using the FAST algorithm available
52+
> in the FSL software package (Smith et al., 2004). Tissue partial
53+
> volume maps were linearly interpolated to the resolution of the
54+
> functional data series using AFNI (Cox, 1996). In order to form
55+
> white matter ROIs, the white matter partial volume maps were
56+
> thresholded at a partial volume fraction of 0.99 and then eroded by
57+
> two voxels in each direction to further minimize partial voluming
58+
> with gray matter. CSF voxels were determined by first thresholding
59+
> the CSF partial volume maps at 0.99 and then applying a threedimensional
60+
> nearest neighbor criteria to minimize multiple tissue
61+
> partial voluming. Since CSF regions are typically small compared
62+
> to white matter regions mask, erosion was not applied.
63+
64+
This particular procedure is not generalizable to BOLD data with different voxel zooms
65+
as the mathematical morphology operations will be scaled by those.
66+
Also, from reading the excerpt above and the tCompCor description, I (@oesteban)
67+
believe that they always operated slice-wise given the large slice-thickness of
68+
their functional data.
69+
70+
Instead, *fMRIPrep*'s implementation deviates from Behzadi's implementation on two
71+
aspects:
72+
73+
* the masks are prepared in high-resolution, anatomical space and then
74+
projected into BOLD space; and,
75+
* instead of using binary erosion, a dilated GM map is generated -- thresholding
76+
the corresponding PV map at 0.05 (i.e., pixels containing at least 5% of GM tissue)
77+
and then subtracting that map from the CSF, WM and CSF+WM (combined) masks.
78+
This should be equivalent to eroding the masks, except that the erosion
79+
only happens at direct interfaces with GM.
80+
81+
When the probseg maps provene from FreeSurfer's ``recon-all`` (i.e., they are
82+
discrete), binary maps are *transformed* into some sort of partial volume maps
83+
by means of a Gaussian smoothing filter with sigma adjusted by the size of the
84+
BOLD data.
85+
86+
"""
87+
from pathlib import Path
88+
import numpy as np
89+
import nibabel as nb
90+
from scipy.ndimage import binary_dilation
91+
from skimage.morphology import ball
92+
93+
csf_file = in_files[2] # BIDS labeling (CSF=2; last of list)
94+
# Load PV maps (fast) or segments (recon-all)
95+
gm_vf = nb.load(in_files[0])
96+
wm_vf = nb.load(in_files[1])
97+
csf_vf = nb.load(csf_file)
98+
99+
# Prepare target zooms
100+
imgzooms = np.array(gm_vf.header.get_zooms()[:3], dtype=float)
101+
if zooms is None:
102+
zooms = imgzooms
103+
zooms = np.array(zooms, dtype=float)
104+
105+
if not is_aseg:
106+
gm_data = gm_vf.get_fdata() > 0.05
107+
wm_data = wm_vf.get_fdata()
108+
csf_data = csf_vf.get_fdata()
109+
else:
110+
csf_file = mask2vf(
111+
csf_file,
112+
zooms=zooms,
113+
out_file=str(Path("acompcor_csf.nii.gz").absolute()),
114+
)
115+
csf_data = nb.load(csf_file).get_fdata()
116+
wm_data = mask2vf(in_files[1], zooms=zooms)
117+
118+
# We do not have partial volume maps (recon-all route)
119+
gm_data = np.asanyarray(gm_vf.dataobj, np.uint8) > 0
120+
121+
# Dilate the GM mask
122+
gm_data = binary_dilation(gm_data, structure=ball(3))
123+
124+
# Output filenames
125+
wm_file = str(Path("acompcor_wm.nii.gz").absolute())
126+
combined_file = str(Path("acompcor_wmcsf.nii.gz").absolute())
127+
128+
# Prepare WM mask
129+
wm_data[gm_data] = 0 # Make sure voxel does not contain GM
130+
nb.Nifti1Image(wm_data, gm_vf.affine, gm_vf.header).to_filename(wm_file)
131+
132+
# Prepare combined CSF+WM mask
133+
comb_data = csf_data + wm_data
134+
comb_data[gm_data] = 0 # Make sure voxel does not contain GM
135+
nb.Nifti1Image(comb_data, gm_vf.affine, gm_vf.header).to_filename(combined_file)
136+
return [csf_file, wm_file, combined_file]

fmriprep/workflows/bold/base.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,7 @@ def init_func_preproc_wf(bold_file):
349349
bold_confounds_wf = init_bold_confs_wf(
350350
mem_gb=mem_gb['largemem'],
351351
metadata=metadata,
352+
freesurfer=freesurfer,
352353
regressors_all_comps=config.workflow.regressors_all_comps,
353354
regressors_fd_th=config.workflow.regressors_fd_th,
354355
regressors_dvars_th=config.workflow.regressors_dvars_th,

0 commit comments

Comments
 (0)