-
Notifications
You must be signed in to change notification settings - Fork 307
FIX: Revise the reproducibility of CompCor masks #2130
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
Merged
effigies
merged 3 commits into
nipreps:master
from
oesteban:fix/2129-compcor-masks-reliability
Sep 2, 2020
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,136 @@ | ||
| """Utilities for confounds manipulation.""" | ||
|
|
||
|
|
||
| def mask2vf(in_file, zooms=None, out_file=None): | ||
| """ | ||
| Convert a binary mask on a volume fraction map. | ||
|
|
||
| The algorithm simply applies a Gaussian filter with the kernel size scaled | ||
| by the zooms given as argument. | ||
|
|
||
| """ | ||
| import numpy as np | ||
| import nibabel as nb | ||
| from scipy.ndimage import gaussian_filter | ||
|
|
||
| img = nb.load(in_file) | ||
| imgzooms = np.array(img.header.get_zooms()[:3], dtype=float) | ||
| if zooms is None: | ||
| zooms = imgzooms | ||
|
|
||
| zooms = np.array(zooms, dtype=float) | ||
| sigma = 0.5 * (zooms / imgzooms) | ||
|
|
||
| data = gaussian_filter(img.get_fdata(dtype=np.float32), sigma=sigma) | ||
|
|
||
| max_data = np.percentile(data[data > 0], 99) | ||
| data = np.clip(data / max_data, a_min=0, a_max=1) | ||
|
|
||
| if out_file is None: | ||
| return data | ||
|
|
||
| hdr = img.header.copy() | ||
| hdr.set_data_dtype(np.float32) | ||
| nb.Nifti1Image(data.astype(np.float32), img.affine, hdr).to_filename(out_file) | ||
| return out_file | ||
|
|
||
|
|
||
| def acompcor_masks(in_files, is_aseg=False, zooms=None): | ||
| """ | ||
| Generate aCompCor masks. | ||
|
|
||
| This function selects the CSF partial volume map from the input, | ||
| and generates the WM and combined CSF+WM masks for aCompCor. | ||
|
|
||
| The implementation deviates from Behzadi et al. | ||
| Their original implementation thresholded the CSF and the WM partial-volume | ||
| masks at 0.99 (i.e., 99% of the voxel volume is filled with a particular tissue), | ||
| and then binary eroded that 2 voxels: | ||
|
|
||
| > Anatomical data were segmented into gray matter, white matter, | ||
| > and CSF partial volume maps using the FAST algorithm available | ||
| > in the FSL software package (Smith et al., 2004). Tissue partial | ||
| > volume maps were linearly interpolated to the resolution of the | ||
| > functional data series using AFNI (Cox, 1996). In order to form | ||
| > white matter ROIs, the white matter partial volume maps were | ||
| > thresholded at a partial volume fraction of 0.99 and then eroded by | ||
| > two voxels in each direction to further minimize partial voluming | ||
| > with gray matter. CSF voxels were determined by first thresholding | ||
| > the CSF partial volume maps at 0.99 and then applying a threedimensional | ||
| > nearest neighbor criteria to minimize multiple tissue | ||
| > partial voluming. Since CSF regions are typically small compared | ||
| > to white matter regions mask, erosion was not applied. | ||
|
|
||
| This particular procedure is not generalizable to BOLD data with different voxel zooms | ||
| as the mathematical morphology operations will be scaled by those. | ||
| Also, from reading the excerpt above and the tCompCor description, I (@oesteban) | ||
| believe that they always operated slice-wise given the large slice-thickness of | ||
| their functional data. | ||
|
|
||
| Instead, *fMRIPrep*'s implementation deviates from Behzadi's implementation on two | ||
| aspects: | ||
|
|
||
| * the masks are prepared in high-resolution, anatomical space and then | ||
| projected into BOLD space; and, | ||
| * instead of using binary erosion, a dilated GM map is generated -- thresholding | ||
| the corresponding PV map at 0.05 (i.e., pixels containing at least 5% of GM tissue) | ||
| and then subtracting that map from the CSF, WM and CSF+WM (combined) masks. | ||
| This should be equivalent to eroding the masks, except that the erosion | ||
| only happens at direct interfaces with GM. | ||
|
|
||
| When the probseg maps provene from FreeSurfer's ``recon-all`` (i.e., they are | ||
| discrete), binary maps are *transformed* into some sort of partial volume maps | ||
| by means of a Gaussian smoothing filter with sigma adjusted by the size of the | ||
| BOLD data. | ||
|
|
||
| """ | ||
| from pathlib import Path | ||
| import numpy as np | ||
| import nibabel as nb | ||
| from scipy.ndimage import binary_dilation | ||
| from skimage.morphology import ball | ||
|
|
||
| csf_file = in_files[2] # BIDS labeling (CSF=2; last of list) | ||
| # Load PV maps (fast) or segments (recon-all) | ||
| gm_vf = nb.load(in_files[0]) | ||
| wm_vf = nb.load(in_files[1]) | ||
| csf_vf = nb.load(csf_file) | ||
|
|
||
| # Prepare target zooms | ||
| imgzooms = np.array(gm_vf.header.get_zooms()[:3], dtype=float) | ||
| if zooms is None: | ||
| zooms = imgzooms | ||
| zooms = np.array(zooms, dtype=float) | ||
|
|
||
| if not is_aseg: | ||
| gm_data = gm_vf.get_fdata() > 0.05 | ||
| wm_data = wm_vf.get_fdata() | ||
| csf_data = csf_vf.get_fdata() | ||
| else: | ||
| csf_file = mask2vf( | ||
| csf_file, | ||
| zooms=zooms, | ||
| out_file=str(Path("acompcor_csf.nii.gz").absolute()), | ||
| ) | ||
| csf_data = nb.load(csf_file).get_fdata() | ||
| wm_data = mask2vf(in_files[1], zooms=zooms) | ||
|
|
||
| # We do not have partial volume maps (recon-all route) | ||
| gm_data = np.asanyarray(gm_vf.dataobj, np.uint8) > 0 | ||
|
|
||
| # Dilate the GM mask | ||
| gm_data = binary_dilation(gm_data, structure=ball(3)) | ||
|
|
||
| # Output filenames | ||
| wm_file = str(Path("acompcor_wm.nii.gz").absolute()) | ||
| combined_file = str(Path("acompcor_wmcsf.nii.gz").absolute()) | ||
|
|
||
| # Prepare WM mask | ||
| wm_data[gm_data] = 0 # Make sure voxel does not contain GM | ||
| nb.Nifti1Image(wm_data, gm_vf.affine, gm_vf.header).to_filename(wm_file) | ||
|
|
||
| # Prepare combined CSF+WM mask | ||
| comb_data = csf_data + wm_data | ||
| comb_data[gm_data] = 0 # Make sure voxel does not contain GM | ||
| nb.Nifti1Image(comb_data, gm_vf.affine, gm_vf.header).to_filename(combined_file) | ||
| return [csf_file, wm_file, combined_file] | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't really like sticking major pieces of machinery into a
utilsmodule and having the interfaces be thin wrappers on a hidden function. I think at one point we were doing something like:And the justification was that it would be easier to test a function than an interface. We're not testing this, and now it's being hidden away into an unassuming corner of the code.
I don't want to hold up the PR, but I would suggest we think through our organization and apply it consistently.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yup, this would be good. Let's make sure we find a time after the LTS. At first my only comment would be that sphinx does not take private functions by default (that's why this is public). That said, if when we meet we decide to have everything together, fine by me - this was kind of an arbitrary decision.