[ENH]: Support for multiple computed masks #175

Merged
fraimondo merged 10 commits from feat/compute_mask into main 2023-01-23 17:32:41 +00:00
17 changed files with 375 additions and 45 deletions

View file

@ -515,7 +515,7 @@ Available
* - Name * - Name
- Keys - Keys
- Version added - Version added
- Publication - Description - Publication
* - Vickery-Patil (Gray Matter) * - Vickery-Patil (Gray Matter)
- | ``GM_prob0.2`` - | ``GM_prob0.2``
- 0.0.1 - 0.0.1
@ -528,6 +528,29 @@ Available
- | Vickery, Sam, & Patil, Kaustubh. (2022). - | Vickery, Sam, & Patil, Kaustubh. (2022).
| Chimpanzee and Human Gray Matter Masks [Data set]. Zenodo. | Chimpanzee and Human Gray Matter Masks [Data set]. Zenodo.
| https://doi.org/10.5281/zenodo.6463123 | https://doi.org/10.5281/zenodo.6463123
* - Nilearn's MNI152 1mm-resolution mask
- | ``compute_brain_mask``
- 0.0.2
- | Compute the whole-brain mask. This mask is calculated using MNI152 1mm-resolution template mask onto the
| target image. See :func:`nilearn.masking.compute_brain_mask`
* - Nilearn's mask computed from FMRI data
- | ``compute_epi_mask``
- 0.0.2
- | Compute a brain mask from fMRI data. This is based on an heuristic proposed by T.Nichols: find the least
| dense point of the histogram, between fractions ``lower_cutoff`` and ``upper_cutoff`` of the total image
| histogram. See :func:`nilearn.masking.compute_epi_mask`
* - Nilearn's background mask
- | ``compute_background_mask``
- 0.0.2
- | Compute a brain mask for the images by guessing the value of the background from the border of the image.
| See :func:`nilearn.masking.compute_background_mask`
* - Nilearn's ICBM152 template gray-matter mask
- | ``fetch_icbm152_brain_gm_mask``
- 0.0.2
- | Compute a gray-matter mask from the asymmetrical ICBM152 2009 template, release a.
| See :func:`nilearn.datasets.fetch_icbm152_brain_gm_mask`
Planned Planned
~~~~~~~ ~~~~~~~

View file

@ -37,6 +37,9 @@ Enhancements
- Change HCP datagrabber tests to decrease CI running time (:gh:`155` by `Fede Raimondo`_). - Change HCP datagrabber tests to decrease CI running time (:gh:`155` by `Fede Raimondo`_).
- Add support for nilearn computed masks (``compute_epi_mask``, ``compute_brain_mask``, ``compute_background_mask``,
``fetch_icbm152_brain_gm_mask``) (:gh:`175` by `Fede Raimondo`_).
Bugs Bugs
~~~~ ~~~~

View file

@ -19,6 +19,7 @@ from .masks import (
list_masks, list_masks,
load_mask, load_mask,
register_mask, register_mask,
get_mask,
) )
from . import utils from . import utils

View file

@ -4,9 +4,27 @@
# License: AGPL # License: AGPL
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union from typing import (
TYPE_CHECKING,
Any,
Dict,
List,
Optional,
Tuple,
Union,
Callable,
)
import numpy as np
import nibabel as nib import nibabel as nib
from nilearn.masking import (
compute_brain_mask,
compute_background_mask,
compute_epi_mask,
)
from nilearn.datasets import fetch_icbm152_brain_gm_mask
from nilearn.image import resample_to_img
from ..utils.logging import logger, raise_error from ..utils.logging import logger, raise_error
from .utils import closest_resolution from .utils import closest_resolution
@ -18,6 +36,30 @@ if TYPE_CHECKING:
# Path to the VOIs # Path to the VOIs
synchon commented 2023-01-19 10:44:16 +00:00 (Migrated from github.com)

Missing type annotations.

Missing type annotations.
synchon commented 2023-01-19 10:44:42 +00:00 (Migrated from github.com)

**kwargs : dict

`**kwargs : dict`
_masks_path = Path(__file__).parent / "masks" _masks_path = Path(__file__).parent / "masks"
def _fetch_icbm152_brain_gm_mask(target_img: "Nifti1Image", **kwargs):
"""Fetch ICBM152 brain mask and resample.
Parameters
----------
target_img : nibabel.Nifti1Image
LeSasse commented 2023-01-18 07:57:34 +00:00 (Migrated from github.com)

I think it would be better to have the actual function name from nilearn (i.e. "compute_brain_mask") rather than an abbreviation of it.

I think it would be better to have the actual function name from nilearn (i.e. "compute_brain_mask") rather than an abbreviation of it.
The image to which the mask will be resampled.
**kwargs : dict
Keyword arguments to be passed to
:func:`nilearn.datasets.fetch_icbm152_brain_gm_mask`.
Returns
-------
nibabel.Nifti1Image
The resampled mask.
"""
mask = fetch_icbm152_brain_gm_mask(**kwargs)
mask = resample_to_img(
mask, target_img, interpolation="nearest", copy=True
)
return mask
""" """
A dictionary containing all supported masks and their respective file or A dictionary containing all supported masks and their respective file or
data. data.
@ -28,6 +70,22 @@ data/masks directory. The user can also register their own masks.
_available_masks: Dict[str, Dict[str, Any]] = { _available_masks: Dict[str, Dict[str, Any]] = {
"GM_prob0.2": {"family": "Vickery-Patil"}, "GM_prob0.2": {"family": "Vickery-Patil"},
"GM_prob0.2_cortex": {"family": "Vickery-Patil"}, "GM_prob0.2_cortex": {"family": "Vickery-Patil"},
"compute_brain_mask": {
"family": "Callable",
"func": compute_brain_mask,
},
"compute_background_mask": {
"family": "Callable",
"func": compute_background_mask,
},
"compute_epi_mask": {
"family": "Callable",
"func": compute_epi_mask,
},
"fetch_icbm152_brain_gm_mask": {
"family": "Callable",
"func": _fetch_icbm152_brain_gm_mask,
},
} }
@ -88,11 +146,67 @@ def list_masks() -> List[str]:
return sorted(_available_masks.keys()) return sorted(_available_masks.keys())
synchon commented 2023-01-19 10:53:31 +00:00 (Migrated from github.com)

Would prefer to have the type as dict and the key-value pair information in the description below.

Would prefer to have the type as `dict` and the key-value pair information in the description below.
synchon commented 2023-01-19 10:55:27 +00:00 (Migrated from github.com)

Missing coverage here.

Missing coverage here.
def get_mask(
mask: Union[str, Dict],
target_data: Dict[str, Any],
) -> "Nifti1Image":
"""Get mask, tailored for the target image.
Parameters
----------
masks : str or dict
The name of the mask, or the name of a callable mask and the parameters
of the mask.
target_data : dict
The corresponding item of the data object to which the mask will be
applied.
Returns
-------
Nifti1Image
The mask image.
"""
# Get the min of the voxels sizes and use it as the resolution
target_img = target_data["data"]
resolution = np.min(target_img.header.get_zooms()[:3])
if isinstance(mask, dict):
if len(mask) != 1:
raise_error(
"The mask dictionary must have only one key, "
"the name of the mask."
)
mask_name = list(mask.keys())[0]
mask_params = mask[mask_name]
else:
mask_name = mask
mask_params = None
mask_object, _ = load_mask(
mask_name, path_only=False, resolution=resolution
)
if callable(mask_object):
if mask_params is None:
mask_params = {}
mask_img = mask_object(target_img, **mask_params)
else: # Mask is a Nifti1Image
if mask_params is not None:
raise_error("Cannot pass callable params to a non-callable mask.")
mask_img = resample_to_img(
mask_object,
target_img,
interpolation="nearest",
copy=True,
)
return mask_img
def load_mask( def load_mask(
name: str, name: str,
resolution: Optional[float] = None, resolution: Optional[float] = None,
path_only: bool = False, path_only: bool = False,
) -> Tuple[Optional["Nifti1Image"], Path]: ) -> Tuple[Optional[Union["Nifti1Image", Callable]], Optional[Path]]:
"""Load mask. """Load mask.
Parameters Parameters
@ -109,11 +223,12 @@ def load_mask(
Returns Returns
------- -------
Nifti1Image or None Nifti1Image, Callable or None
Loaded mask image. Loaded mask image.
pathlib.Path pathlib.Path or None
File path to the mask image. File path to the mask image.
""" """
mask_img = None
if name not in _available_masks: if name not in _available_masks:
raise_error( raise_error(
f"Mask {name} not found. " f"Valid options are: {list_masks()}" f"Mask {name} not found. " f"Valid options are: {list_masks()}"
@ -126,12 +241,14 @@ def load_mask(
mask_fname = Path(mask_definition["path"]) mask_fname = Path(mask_definition["path"])
elif t_family == "Vickery-Patil": elif t_family == "Vickery-Patil":
mask_fname = _load_vickery_patil_mask(name, resolution) mask_fname = _load_vickery_patil_mask(name, resolution)
elif t_family == "Callable":
mask_img = mask_definition["func"]
synchon commented 2023-01-19 10:54:00 +00:00 (Migrated from github.com)

Needs a newline after this line.

Needs a newline after this line.
mask_fname = None
else: else:
raise_error(f"I don't know about the {t_family} mask family.") raise_error(f"I don't know about the {t_family} mask family.")
if mask_fname is not None:
logger.info(f"Loading mask {mask_fname.absolute()}") logger.info(f"Loading mask {mask_fname.absolute()}")
mask_img = None
if path_only is False: if path_only is False:
mask_img = nib.load(mask_fname) mask_img = nib.load(mask_fname)

View file

@ -5,16 +5,37 @@
# Synchon Mandal <s.mandal@fz-juelich.de> # Synchon Mandal <s.mandal@fz-juelich.de>
# License: AGPL # License: AGPL
from typing import Callable, Dict, Union
from pathlib import Path from pathlib import Path
import pytest import pytest
from numpy.testing import assert_array_almost_equal from numpy.testing import (
assert_array_almost_equal,
assert_array_equal,
)
from nilearn.image import resample_to_img
from nilearn.masking import (
compute_brain_mask,
compute_background_mask,
compute_epi_mask,
)
from nilearn.datasets import fetch_icbm152_brain_gm_mask
from junifer.data.masks import ( from junifer.data.masks import (
_load_vickery_patil_mask, _load_vickery_patil_mask,
list_masks, list_masks,
load_mask, load_mask,
register_mask, register_mask,
get_mask,
_available_masks,
)
from junifer.datareader import DefaultDataReader
from junifer.testing.datagrabbers import (
OasisVBMTestingDatagrabber,
SPMAuditoryTestingDatagrabber,
) )
@ -151,3 +172,124 @@ def test_vickery_patil() -> None:
with pytest.raises(ValueError, match=r"find a Vickery-Patil mask "): with pytest.raises(ValueError, match=r"find a Vickery-Patil mask "):
_load_vickery_patil_mask("wrong", resolution=2) _load_vickery_patil_mask("wrong", resolution=2)
def test_get_mask() -> None:
"""Test the get_mask function."""
reader = DefaultDataReader()
with OasisVBMTestingDatagrabber() as dg:
input = dg["sub-01"]
input = reader.fit_transform(input)
vbm_gm = input["VBM_GM"]
vbm_gm_img = vbm_gm["data"]
mask = get_mask(mask="GM_prob0.2", target_data=vbm_gm)
assert mask.shape == vbm_gm_img.shape
assert_array_equal(mask.affine, vbm_gm_img.affine)
raw_mask_img, _ = load_mask("GM_prob0.2", resolution=1.5)
res_mask_img = resample_to_img(
raw_mask_img,
vbm_gm_img,
interpolation="nearest",
copy=True,
)
assert_array_equal(mask.get_fdata(), res_mask_img.get_fdata())
def test_mask_callable() -> None:
"""Test using a callable mask."""
def ident(x):
return x
_available_masks["identity"] = {"family": "Callable", "func": ident}
reader = DefaultDataReader()
with OasisVBMTestingDatagrabber() as dg:
input = dg["sub-01"]
input = reader.fit_transform(input)
vbm_gm = input["VBM_GM"]
vbm_gm_img = vbm_gm["data"]
mask = get_mask(mask="identity", target_data=vbm_gm)
assert_array_equal(mask.get_fdata(), vbm_gm_img.get_fdata())
del _available_masks["identity"]
def test_get_mask_errors() -> None:
"""Test passing wrong parameters to get_mask."""
reader = DefaultDataReader()
with OasisVBMTestingDatagrabber() as dg:
input = dg["sub-01"]
input = reader.fit_transform(input)
vbm_gm = input["VBM_GM"]
with pytest.raises(ValueError, match=r"only one key"):
get_mask(mask={"GM_prob0.2": {}, "Other": {}}, target_data=vbm_gm)
with pytest.raises(ValueError, match=r"callable params"):
get_mask(mask={"GM_prob0.2": {"param": 1}}, target_data=vbm_gm)
@pytest.mark.parametrize(
"mask_name,function,params,resample",
[
("compute_brain_mask", compute_brain_mask, {"threshold": 0.2}, False),
("compute_background_mask", compute_background_mask, None, False),
("compute_epi_mask", compute_epi_mask, None, False),
(
"fetch_icbm152_brain_gm_mask",
fetch_icbm152_brain_gm_mask,
None,
True,
),
],
)
def test_nilearn_compute_masks(
mask_name: str,
function: Callable,
params: Union[Dict, None],
resample: bool,
) -> None:
"""Test using nilearn compute mask functions.
Parameters
----------
mask_name : str
Name of the mask.
function : callable
Function to call.
params : dict, optional
Parameters to pass to the function.
resample : bool
Whether to resample the mask to the target data.
"""
reader = DefaultDataReader()
with SPMAuditoryTestingDatagrabber() as dg:
input = dg["sub001"]
input = reader.fit_transform(input)
bold = input["BOLD"]
bold_img = bold["data"]
if params is None:
params = {}
mask_spec = mask_name
else:
mask_spec = {mask_name: params}
mask = get_mask(mask=mask_spec, target_data=bold)
assert_array_equal(mask.affine, bold_img.affine)
if resample is False:
ni_mask = function(bold_img, **params)
else:
ni_mask = function(**params)
# Mask needs resample
ni_mask = resample_to_img(
ni_mask,
bold_img,
interpolation="nearest",
copy=True,
)
assert_array_equal(mask.get_fdata(), ni_mask.get_fdata())

View file

@ -49,7 +49,7 @@ class RSSETSMarker(BaseMarker):
parcellation: Union[str, List[str]], parcellation: Union[str, List[str]],
agg_method: str = "mean", agg_method: str = "mean",
agg_method_params: Optional[Dict] = None, agg_method_params: Optional[Dict] = None,
mask: Optional[str] = None, mask: Union[str, Dict, None] = None,
name: Optional[str] = None, name: Optional[str] = None,
) -> None: ) -> None:
self.parcellation = parcellation self.parcellation = parcellation

View file

@ -70,7 +70,7 @@ class AmplitudeLowFrequencyFluctuationParcels(
lowpass: float = 0.1, lowpass: float = 0.1,
tr: Optional[float] = None, tr: Optional[float] = None,
use_afni: Optional[bool] = None, use_afni: Optional[bool] = None,
mask: Optional[str] = None, mask: Union[str, Dict, None] = None,
method: str = "mean", method: str = "mean",
method_params: Optional[Dict] = None, method_params: Optional[Dict] = None,
name: Optional[str] = None, name: Optional[str] = None,

View file

@ -5,7 +5,7 @@
# Kaustubh R. Patil <k.patil@fz-juelich.de> # Kaustubh R. Patil <k.patil@fz-juelich.de>
# License: AGPL # License: AGPL
from typing import Dict, Optional from typing import Dict, Optional, Union
from ...api.decorators import register_marker from ...api.decorators import register_marker
from .. import SphereAggregation from .. import SphereAggregation
@ -75,7 +75,7 @@ class AmplitudeLowFrequencyFluctuationSpheres(
lowpass: float = 0.1, lowpass: float = 0.1,
tr: Optional[float] = None, tr: Optional[float] = None,
use_afni: Optional[bool] = None, use_afni: Optional[bool] = None,
mask: Optional[str] = None, mask: Union[str, Dict, None] = None,
method: str = "mean", method: str = "mean",
method_params: Optional[Dict] = None, method_params: Optional[Dict] = None,
name: Optional[str] = None, name: Optional[str] = None,

View file

@ -4,7 +4,7 @@
# Kaustubh R. Patil <k.patil@fz-juelich.de> # Kaustubh R. Patil <k.patil@fz-juelich.de>
# License: AGPL # License: AGPL
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional, Union
import pandas as pd import pandas as pd
@ -47,7 +47,7 @@ class CrossParcellationFC(BaseMarker):
parcellation_two: str, parcellation_two: str,
aggregation_method: str = "mean", aggregation_method: str = "mean",
correlation_method: str = "pearson", correlation_method: str = "pearson",
mask: Optional[str] = None, mask: Union[str, Dict, None] = None,
name: Optional[str] = None, name: Optional[str] = None,
) -> None: ) -> None:
if parcellation_one == parcellation_two: if parcellation_one == parcellation_two:

View file

@ -5,7 +5,7 @@
from abc import abstractmethod from abc import abstractmethod
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional, Union
from nilearn.connectome import ConnectivityMeasure from nilearn.connectome import ConnectivityMeasure
from sklearn.covariance import EmpiricalCovariance from sklearn.covariance import EmpiricalCovariance
@ -50,7 +50,7 @@ class FunctionalConnectivityBase(BaseMarker):
agg_method_params: Optional[Dict] = None, agg_method_params: Optional[Dict] = None,
cor_method: str = "covariance", cor_method: str = "covariance",
cor_method_params: Optional[Dict] = None, cor_method_params: Optional[Dict] = None,
mask: Optional[str] = None, mask: Union[str, Dict, None] = None,
name: Optional[str] = None, name: Optional[str] = None,
) -> None: ) -> None:
self.agg_method = agg_method self.agg_method = agg_method

View file

@ -51,7 +51,7 @@ class FunctionalConnectivityParcels(FunctionalConnectivityBase):
agg_method_params: Optional[Dict] = None, agg_method_params: Optional[Dict] = None,
cor_method: str = "covariance", cor_method: str = "covariance",
cor_method_params: Optional[Dict] = None, cor_method_params: Optional[Dict] = None,
mask: Optional[str] = None, mask: Union[str, Dict, None] = None,
name: Optional[str] = None, name: Optional[str] = None,
) -> None: ) -> None:
self.parcellation = parcellation self.parcellation = parcellation

View file

@ -5,7 +5,7 @@
# Synchon Mandal <s.mandal@fz-juelich.de> # Synchon Mandal <s.mandal@fz-juelich.de>
# License: AGPL # License: AGPL
from typing import Any, Dict, Optional from typing import Any, Dict, Optional, Union
from ...api.decorators import register_marker from ...api.decorators import register_marker
from ..sphere_aggregation import SphereAggregation from ..sphere_aggregation import SphereAggregation
@ -57,7 +57,7 @@ class FunctionalConnectivitySpheres(FunctionalConnectivityBase):
agg_method_params: Optional[Dict] = None, agg_method_params: Optional[Dict] = None,
cor_method: str = "covariance", cor_method: str = "covariance",
cor_method_params: Optional[Dict] = None, cor_method_params: Optional[Dict] = None,
mask: Optional[str] = None, mask: Union[str, Dict, None] = None,
name: Optional[str] = None, name: Optional[str] = None,
) -> None: ) -> None:
self.coords = coords self.coords = coords

View file

@ -11,7 +11,7 @@ from nilearn.image import math_img, new_img_like, resample_to_img
from nilearn.maskers import NiftiMasker from nilearn.maskers import NiftiMasker
LeSasse commented 2023-01-18 14:25:51 +00:00 (Migrated from github.com)

same here: in the ParcelAggregation init method should masks not be this should be str or list of (str or dict) now?

same here: in the ParcelAggregation init method should masks not be this should be str or list of (str or dict) now?
fraimondo commented 2023-01-18 14:55:49 +00:00 (Migrated from github.com)

same as before. This is for computing masks.

same as before. This is for computing masks.
LeSasse commented 2023-01-18 14:59:47 +00:00 (Migrated from github.com)

i see makes sense!

i see makes sense!
from ..api.decorators import register_marker from ..api.decorators import register_marker
from ..data import load_mask, load_parcellation from ..data import get_mask, load_parcellation
from ..stats import get_aggfunc_by_name from ..stats import get_aggfunc_by_name
from ..utils import logger from ..utils import logger
from .base import BaseMarker from .base import BaseMarker
@ -52,7 +52,7 @@ class ParcelAggregation(BaseMarker):
parcellation: Union[str, List[str]], parcellation: Union[str, List[str]],
method: str, method: str,
method_params: Optional[Dict[str, Any]] = None, method_params: Optional[Dict[str, Any]] = None,
mask: Optional[str] = None, mask: Union[str, Dict, None] = None,
on: Union[List[str], str, None] = None, on: Union[List[str], str, None] = None,
name: Optional[str] = None, name: Optional[str] = None,
) -> None: ) -> None:
@ -126,14 +126,14 @@ class ParcelAggregation(BaseMarker):
* ``columns`` : the column labels for the computed values as a list * ``columns`` : the column labels for the computed values as a list
""" """
t_input = input["data"] t_input_img = input["data"]
logger.debug(f"Parcel aggregation using {self.method}") logger.debug(f"Parcel aggregation using {self.method}")
agg_func = get_aggfunc_by_name( agg_func = get_aggfunc_by_name(
name=self.method, name=self.method,
func_params=self.method_params, func_params=self.method_params,
) )
# Get the min of the voxels sizes and use it as the resolution # Get the min of the voxels sizes and use it as the resolution
resolution = np.min(t_input.header.get_zooms()[:3]) resolution = np.min(t_input_img.header.get_zooms()[:3])
# Load the parcellations # Load the parcellations
all_parcelations = [] all_parcelations = []
@ -146,7 +146,7 @@ class ParcelAggregation(BaseMarker):
# Resample all of them to the image # Resample all of them to the image
t_parcellation_img_res = resample_to_img( t_parcellation_img_res = resample_to_img(
t_parcellation, t_parcellation,
t_input, t_input_img,
interpolation="nearest", interpolation="nearest",
copy=True, copy=True,
) )
@ -186,13 +186,8 @@ class ParcelAggregation(BaseMarker):
if self.mask is not None: if self.mask is not None:
logger.debug(f"Masking with {self.mask}") logger.debug(f"Masking with {self.mask}")
mask_img, _ = load_mask(name=self.mask, resolution=resolution) mask_img = get_mask(mask=self.mask, target_data=input)
mask_img = resample_to_img(
mask_img,
t_input,
interpolation="nearest",
copy=True,
)
parcellation_bin = math_img( parcellation_bin = math_img(
"np.logical_and(img, mask)", "np.logical_and(img, mask)",
img=parcellation_bin, img=parcellation_bin,
@ -201,11 +196,11 @@ class ParcelAggregation(BaseMarker):
logger.debug("Masking") logger.debug("Masking")
masker = NiftiMasker( masker = NiftiMasker(
parcellation_bin, target_affine=t_input.affine parcellation_bin, target_affine=t_input_img.affine
) # type: ignore ) # type: ignore
# Mask the input data and the parcellation # Mask the input data and the parcellation
data = masker.fit_transform(t_input) data = masker.fit_transform(t_input_img)
parcellation_values = masker.transform(parcellation_img_res) parcellation_values = masker.transform(parcellation_img_res)
parcellation_values = np.squeeze(parcellation_values).astype(int) parcellation_values = np.squeeze(parcellation_values).astype(int)

View file

@ -4,7 +4,7 @@
# License: AGPL # License: AGPL
from typing import Any, Dict, Optional from typing import Any, Dict, Optional, Union
import numpy as np import numpy as np
@ -91,7 +91,7 @@ class ReHoParcels(ReHoBase):
reho_params: Optional[Dict] = None, reho_params: Optional[Dict] = None,
agg_method: str = "mean", agg_method: str = "mean",
agg_method_params: Optional[Dict] = None, agg_method_params: Optional[Dict] = None,
mask: Optional[str] = None, mask: Union[str, Dict, None] = None,
name: Optional[str] = None, name: Optional[str] = None,
) -> None: ) -> None:
self.parcellation = parcellation self.parcellation = parcellation

View file

@ -4,7 +4,7 @@
# License: AGPL # License: AGPL
from typing import Any, Dict, Optional from typing import Any, Dict, Optional, Union
import numpy as np import numpy as np
@ -97,7 +97,7 @@ class ReHoSpheres(ReHoBase):
reho_params: Optional[Dict] = None, reho_params: Optional[Dict] = None,
agg_method: str = "mean", agg_method: str = "mean",
agg_method_params: Optional[Dict] = None, agg_method_params: Optional[Dict] = None,
mask: Optional[str] = None, mask: Union[str, Dict, None] = None,
name: Optional[str] = None, name: Optional[str] = None,
) -> None: ) -> None:
self.coords = coords self.coords = coords

View file

@ -7,7 +7,7 @@
from typing import Any, Dict, List, Optional, Union from typing import Any, Dict, List, Optional, Union
from ..api.decorators import register_marker from ..api.decorators import register_marker
from ..data import load_coordinates, load_mask from ..data import load_coordinates, get_mask
from ..external.nilearn import JuniferNiftiSpheresMasker from ..external.nilearn import JuniferNiftiSpheresMasker
from ..stats import get_aggfunc_by_name from ..stats import get_aggfunc_by_name
from ..utils import logger from ..utils import logger
@ -56,7 +56,7 @@ class SphereAggregation(BaseMarker):
radius: Optional[float] = None, radius: Optional[float] = None,
method: str = "mean", method: str = "mean",
method_params: Optional[Dict[str, Any]] = None, method_params: Optional[Dict[str, Any]] = None,
mask: Optional[str] = None, mask: Union[str, Dict, None] = None,
on: Union[List[str], str, None] = None, on: Union[List[str], str, None] = None,
name: Optional[str] = None, name: Optional[str] = None,
) -> None: ) -> None:
@ -129,7 +129,7 @@ class SphereAggregation(BaseMarker):
* ``columns`` : the column labels for the computed values as a list * ``columns`` : the column labels for the computed values as a list
""" """
t_input = input["data"] t_input_img = input["data"]
logger.debug(f"Sphere aggregation using {self.method}") logger.debug(f"Sphere aggregation using {self.method}")
# Get aggregation function # Get aggregation function
agg_func = get_aggfunc_by_name( agg_func = get_aggfunc_by_name(
@ -139,7 +139,7 @@ class SphereAggregation(BaseMarker):
mask_img = None mask_img = None
if self.mask is not None: if self.mask is not None:
logger.debug(f"Masking with {self.mask}") logger.debug(f"Masking with {self.mask}")
mask_img, _ = load_mask(self.mask) mask_img = get_mask(mask=self.mask, target_data=input)
# Get seeds and labels # Get seeds and labels
coords, out_labels = load_coordinates(name=self.coords) coords, out_labels = load_coordinates(name=self.coords)
masker = JuniferNiftiSpheresMasker( masker = JuniferNiftiSpheresMasker(
@ -149,7 +149,7 @@ class SphereAggregation(BaseMarker):
agg_func=agg_func, agg_func=agg_func,
) )
# Fit and transform the marker on the data # Fit and transform the marker on the data
out_values = masker.fit_transform(t_input) out_values = masker.fit_transform(t_input_img)
# Format the output # Format the output
out = {"data": out_values, "columns": out_labels} out = {"data": out_values, "columns": out_labels}
return out return out

View file

@ -12,6 +12,7 @@ import pytest
from nilearn import datasets from nilearn import datasets
from nilearn.image import concat_imgs, math_img, new_img_like, resample_to_img from nilearn.image import concat_imgs, math_img, new_img_like, resample_to_img
from nilearn.maskers import NiftiLabelsMasker, NiftiMasker from nilearn.maskers import NiftiLabelsMasker, NiftiMasker
from nilearn.masking import compute_brain_mask
from numpy.testing import assert_array_almost_equal, assert_array_equal from numpy.testing import assert_array_almost_equal, assert_array_equal
from scipy.stats import trim_mean from scipy.stats import trim_mean
@ -240,6 +241,54 @@ def test_ParcelAggregation_3D_mask() -> None:
assert_array_almost_equal(auto, jun_values3d_mean) assert_array_almost_equal(auto, jun_values3d_mean)
def test_ParcelAggregation_3D_mask_computed() -> None:
"""Test ParcelAggregation object on 3D images with computed masks."""
# Get the testing parcellation (for nilearn)
parcellation = datasets.fetch_atlas_schaefer_2018(n_rois=100)
# Get the oasis VBM data
oasis_dataset = datasets.fetch_oasis_vbm(n_subjects=1)
vbm = oasis_dataset.gray_matter_maps[0]
img = nib.load(vbm)
# Get one mask
mask_img = compute_brain_mask(img, threshold=0.2)
# Create NiftiLabelsMasker
nifti_masker = NiftiLabelsMasker(
labels_img=parcellation.maps, mask_img=mask_img
)
auto = nifti_masker.fit_transform(img)
# Get one mask
mask_img = compute_brain_mask(img, threshold=0.5)
# Create NiftiLabelsMasker
nifti_masker = NiftiLabelsMasker(
labels_img=parcellation.maps, mask_img=mask_img
)
auto_bad = nifti_masker.fit_transform(img)
# Use the ParcelAggregation object
marker = ParcelAggregation(
parcellation="Schaefer100x7",
method="mean",
mask={"compute_brain_mask": {"threshold": 0.2}},
name="gmd_schaefer100x7_mean",
on="VBM_GM",
) # Test passing "on" as a keyword argument
input = {"VBM_GM": {"data": img, "meta": {}}}
jun_values3d_mean = marker.fit_transform(input)["VBM_GM"]["data"]
assert jun_values3d_mean.ndim == 2
assert jun_values3d_mean.shape[0] == 1
assert_array_almost_equal(auto, jun_values3d_mean)
with pytest.raises(AssertionError):
assert_array_almost_equal(jun_values3d_mean, auto_bad)
def test_ParcelAggregation_3D_multiple_non_overlapping(tmp_path: Path) -> None: def test_ParcelAggregation_3D_multiple_non_overlapping(tmp_path: Path) -> None:
"""Test ParcelAggregation with multiple non-overlapping parcellations. """Test ParcelAggregation with multiple non-overlapping parcellations.