diff --git a/docs/builtin.rst b/docs/builtin.rst index 3c6e0b758..2f12e04ae 100644 --- a/docs/builtin.rst +++ b/docs/builtin.rst @@ -515,7 +515,7 @@ Available * - Name - Keys - Version added - - Publication + - Description - Publication * - Vickery-Patil (Gray Matter) - | ``GM_prob0.2`` - 0.0.1 @@ -528,6 +528,29 @@ Available - | Vickery, Sam, & Patil, Kaustubh. (2022). | Chimpanzee and Human Gray Matter Masks [Data set]. Zenodo. | 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 ~~~~~~~ diff --git a/docs/changes/latest.inc b/docs/changes/latest.inc index 6d46352eb..0207cf57d 100644 --- a/docs/changes/latest.inc +++ b/docs/changes/latest.inc @@ -37,6 +37,9 @@ Enhancements - 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 ~~~~ diff --git a/junifer/data/__init__.py b/junifer/data/__init__.py index f75220f6b..4aabf6a9e 100644 --- a/junifer/data/__init__.py +++ b/junifer/data/__init__.py @@ -19,6 +19,7 @@ from .masks import ( list_masks, load_mask, register_mask, + get_mask, ) from . import utils diff --git a/junifer/data/masks.py b/junifer/data/masks.py index 611176c2e..754089796 100644 --- a/junifer/data/masks.py +++ b/junifer/data/masks.py @@ -4,9 +4,27 @@ # License: AGPL 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 +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 import closest_resolution @@ -18,6 +36,30 @@ if TYPE_CHECKING: # Path to the VOIs _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 + 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 data. @@ -28,6 +70,22 @@ data/masks directory. The user can also register their own masks. _available_masks: Dict[str, Dict[str, Any]] = { "GM_prob0.2": {"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()) +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( name: str, resolution: Optional[float] = None, path_only: bool = False, -) -> Tuple[Optional["Nifti1Image"], Path]: +) -> Tuple[Optional[Union["Nifti1Image", Callable]], Optional[Path]]: """Load mask. Parameters @@ -109,11 +223,12 @@ def load_mask( Returns ------- - Nifti1Image or None + Nifti1Image, Callable or None Loaded mask image. - pathlib.Path + pathlib.Path or None File path to the mask image. """ + mask_img = None if name not in _available_masks: raise_error( f"Mask {name} not found. " f"Valid options are: {list_masks()}" @@ -126,14 +241,16 @@ def load_mask( mask_fname = Path(mask_definition["path"]) elif t_family == "Vickery-Patil": mask_fname = _load_vickery_patil_mask(name, resolution) + elif t_family == "Callable": + mask_img = mask_definition["func"] + mask_fname = None else: raise_error(f"I don't know about the {t_family} mask family.") - logger.info(f"Loading mask {mask_fname.absolute()}") - - mask_img = None - if path_only is False: - mask_img = nib.load(mask_fname) + if mask_fname is not None: + logger.info(f"Loading mask {mask_fname.absolute()}") + if path_only is False: + mask_img = nib.load(mask_fname) return mask_img, mask_fname diff --git a/junifer/data/tests/test_masks.py b/junifer/data/tests/test_masks.py index b00a76df6..ec3739612 100644 --- a/junifer/data/tests/test_masks.py +++ b/junifer/data/tests/test_masks.py @@ -5,16 +5,37 @@ # Synchon Mandal # License: AGPL +from typing import Callable, Dict, Union + from pathlib import Path 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 ( _load_vickery_patil_mask, list_masks, load_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 "): _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()) diff --git a/junifer/markers/ets_rss.py b/junifer/markers/ets_rss.py index 5605fa28a..f82c82343 100644 --- a/junifer/markers/ets_rss.py +++ b/junifer/markers/ets_rss.py @@ -49,7 +49,7 @@ class RSSETSMarker(BaseMarker): parcellation: Union[str, List[str]], agg_method: str = "mean", agg_method_params: Optional[Dict] = None, - mask: Optional[str] = None, + mask: Union[str, Dict, None] = None, name: Optional[str] = None, ) -> None: self.parcellation = parcellation diff --git a/junifer/markers/falff/falff_parcels.py b/junifer/markers/falff/falff_parcels.py index 4015540f8..25c3e8962 100644 --- a/junifer/markers/falff/falff_parcels.py +++ b/junifer/markers/falff/falff_parcels.py @@ -70,7 +70,7 @@ class AmplitudeLowFrequencyFluctuationParcels( lowpass: float = 0.1, tr: Optional[float] = None, use_afni: Optional[bool] = None, - mask: Optional[str] = None, + mask: Union[str, Dict, None] = None, method: str = "mean", method_params: Optional[Dict] = None, name: Optional[str] = None, diff --git a/junifer/markers/falff/falff_spheres.py b/junifer/markers/falff/falff_spheres.py index d9630b372..e614e5320 100644 --- a/junifer/markers/falff/falff_spheres.py +++ b/junifer/markers/falff/falff_spheres.py @@ -5,7 +5,7 @@ # Kaustubh R. Patil # License: AGPL -from typing import Dict, Optional +from typing import Dict, Optional, Union from ...api.decorators import register_marker from .. import SphereAggregation @@ -75,7 +75,7 @@ class AmplitudeLowFrequencyFluctuationSpheres( lowpass: float = 0.1, tr: Optional[float] = None, use_afni: Optional[bool] = None, - mask: Optional[str] = None, + mask: Union[str, Dict, None] = None, method: str = "mean", method_params: Optional[Dict] = None, name: Optional[str] = None, diff --git a/junifer/markers/functional_connectivity/crossparcellation_functional_connectivity.py b/junifer/markers/functional_connectivity/crossparcellation_functional_connectivity.py index 1f1ca429c..74baa08c9 100644 --- a/junifer/markers/functional_connectivity/crossparcellation_functional_connectivity.py +++ b/junifer/markers/functional_connectivity/crossparcellation_functional_connectivity.py @@ -4,7 +4,7 @@ # Kaustubh R. Patil # License: AGPL -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List, Optional, Union import pandas as pd @@ -47,7 +47,7 @@ class CrossParcellationFC(BaseMarker): parcellation_two: str, aggregation_method: str = "mean", correlation_method: str = "pearson", - mask: Optional[str] = None, + mask: Union[str, Dict, None] = None, name: Optional[str] = None, ) -> None: if parcellation_one == parcellation_two: diff --git a/junifer/markers/functional_connectivity/functional_connectivity_base.py b/junifer/markers/functional_connectivity/functional_connectivity_base.py index 94ffa2538..6de34ab07 100644 --- a/junifer/markers/functional_connectivity/functional_connectivity_base.py +++ b/junifer/markers/functional_connectivity/functional_connectivity_base.py @@ -5,7 +5,7 @@ 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 sklearn.covariance import EmpiricalCovariance @@ -50,7 +50,7 @@ class FunctionalConnectivityBase(BaseMarker): agg_method_params: Optional[Dict] = None, cor_method: str = "covariance", cor_method_params: Optional[Dict] = None, - mask: Optional[str] = None, + mask: Union[str, Dict, None] = None, name: Optional[str] = None, ) -> None: self.agg_method = agg_method diff --git a/junifer/markers/functional_connectivity/functional_connectivity_parcels.py b/junifer/markers/functional_connectivity/functional_connectivity_parcels.py index ef0147eaa..2742f0212 100644 --- a/junifer/markers/functional_connectivity/functional_connectivity_parcels.py +++ b/junifer/markers/functional_connectivity/functional_connectivity_parcels.py @@ -51,7 +51,7 @@ class FunctionalConnectivityParcels(FunctionalConnectivityBase): agg_method_params: Optional[Dict] = None, cor_method: str = "covariance", cor_method_params: Optional[Dict] = None, - mask: Optional[str] = None, + mask: Union[str, Dict, None] = None, name: Optional[str] = None, ) -> None: self.parcellation = parcellation diff --git a/junifer/markers/functional_connectivity/functional_connectivity_spheres.py b/junifer/markers/functional_connectivity/functional_connectivity_spheres.py index 9155df655..a43122ef5 100644 --- a/junifer/markers/functional_connectivity/functional_connectivity_spheres.py +++ b/junifer/markers/functional_connectivity/functional_connectivity_spheres.py @@ -5,7 +5,7 @@ # Synchon Mandal # License: AGPL -from typing import Any, Dict, Optional +from typing import Any, Dict, Optional, Union from ...api.decorators import register_marker from ..sphere_aggregation import SphereAggregation @@ -57,7 +57,7 @@ class FunctionalConnectivitySpheres(FunctionalConnectivityBase): agg_method_params: Optional[Dict] = None, cor_method: str = "covariance", cor_method_params: Optional[Dict] = None, - mask: Optional[str] = None, + mask: Union[str, Dict, None] = None, name: Optional[str] = None, ) -> None: self.coords = coords diff --git a/junifer/markers/parcel_aggregation.py b/junifer/markers/parcel_aggregation.py index 8d56e4dfc..6115d53bc 100644 --- a/junifer/markers/parcel_aggregation.py +++ b/junifer/markers/parcel_aggregation.py @@ -11,7 +11,7 @@ from nilearn.image import math_img, new_img_like, resample_to_img from nilearn.maskers import NiftiMasker 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 ..utils import logger from .base import BaseMarker @@ -52,7 +52,7 @@ class ParcelAggregation(BaseMarker): parcellation: Union[str, List[str]], method: str, method_params: Optional[Dict[str, Any]] = None, - mask: Optional[str] = None, + mask: Union[str, Dict, None] = None, on: Union[List[str], str, None] = None, name: Optional[str] = None, ) -> None: @@ -126,14 +126,14 @@ class ParcelAggregation(BaseMarker): * ``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}") agg_func = get_aggfunc_by_name( name=self.method, func_params=self.method_params, ) # 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 all_parcelations = [] @@ -146,7 +146,7 @@ class ParcelAggregation(BaseMarker): # Resample all of them to the image t_parcellation_img_res = resample_to_img( t_parcellation, - t_input, + t_input_img, interpolation="nearest", copy=True, ) @@ -186,13 +186,8 @@ class ParcelAggregation(BaseMarker): if self.mask is not None: logger.debug(f"Masking with {self.mask}") - mask_img, _ = load_mask(name=self.mask, resolution=resolution) - mask_img = resample_to_img( - mask_img, - t_input, - interpolation="nearest", - copy=True, - ) + mask_img = get_mask(mask=self.mask, target_data=input) + parcellation_bin = math_img( "np.logical_and(img, mask)", img=parcellation_bin, @@ -201,11 +196,11 @@ class ParcelAggregation(BaseMarker): logger.debug("Masking") masker = NiftiMasker( - parcellation_bin, target_affine=t_input.affine + parcellation_bin, target_affine=t_input_img.affine ) # type: ignore # 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 = np.squeeze(parcellation_values).astype(int) diff --git a/junifer/markers/reho/reho_parcels.py b/junifer/markers/reho/reho_parcels.py index 451ee01a5..c18814644 100644 --- a/junifer/markers/reho/reho_parcels.py +++ b/junifer/markers/reho/reho_parcels.py @@ -4,7 +4,7 @@ # License: AGPL -from typing import Any, Dict, Optional +from typing import Any, Dict, Optional, Union import numpy as np @@ -91,7 +91,7 @@ class ReHoParcels(ReHoBase): reho_params: Optional[Dict] = None, agg_method: str = "mean", agg_method_params: Optional[Dict] = None, - mask: Optional[str] = None, + mask: Union[str, Dict, None] = None, name: Optional[str] = None, ) -> None: self.parcellation = parcellation diff --git a/junifer/markers/reho/reho_spheres.py b/junifer/markers/reho/reho_spheres.py index 420c5e353..4871477a9 100644 --- a/junifer/markers/reho/reho_spheres.py +++ b/junifer/markers/reho/reho_spheres.py @@ -4,7 +4,7 @@ # License: AGPL -from typing import Any, Dict, Optional +from typing import Any, Dict, Optional, Union import numpy as np @@ -97,7 +97,7 @@ class ReHoSpheres(ReHoBase): reho_params: Optional[Dict] = None, agg_method: str = "mean", agg_method_params: Optional[Dict] = None, - mask: Optional[str] = None, + mask: Union[str, Dict, None] = None, name: Optional[str] = None, ) -> None: self.coords = coords diff --git a/junifer/markers/sphere_aggregation.py b/junifer/markers/sphere_aggregation.py index cf54b6ce7..d25dd5a9e 100644 --- a/junifer/markers/sphere_aggregation.py +++ b/junifer/markers/sphere_aggregation.py @@ -7,7 +7,7 @@ from typing import Any, Dict, List, Optional, Union 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 ..stats import get_aggfunc_by_name from ..utils import logger @@ -56,7 +56,7 @@ class SphereAggregation(BaseMarker): radius: Optional[float] = None, method: str = "mean", method_params: Optional[Dict[str, Any]] = None, - mask: Optional[str] = None, + mask: Union[str, Dict, None] = None, on: Union[List[str], str, None] = None, name: Optional[str] = None, ) -> None: @@ -129,7 +129,7 @@ class SphereAggregation(BaseMarker): * ``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}") # Get aggregation function agg_func = get_aggfunc_by_name( @@ -139,7 +139,7 @@ class SphereAggregation(BaseMarker): mask_img = None if self.mask is not None: 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 coords, out_labels = load_coordinates(name=self.coords) masker = JuniferNiftiSpheresMasker( @@ -149,7 +149,7 @@ class SphereAggregation(BaseMarker): agg_func=agg_func, ) # 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 out = {"data": out_values, "columns": out_labels} return out diff --git a/junifer/markers/tests/test_parcel_aggregation.py b/junifer/markers/tests/test_parcel_aggregation.py index 3274e0ad4..007eccfe1 100644 --- a/junifer/markers/tests/test_parcel_aggregation.py +++ b/junifer/markers/tests/test_parcel_aggregation.py @@ -12,6 +12,7 @@ import pytest from nilearn import datasets from nilearn.image import concat_imgs, math_img, new_img_like, resample_to_img 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 scipy.stats import trim_mean @@ -240,6 +241,54 @@ def test_ParcelAggregation_3D_mask() -> None: 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: """Test ParcelAggregation with multiple non-overlapping parcellations.