From 5275c0016f4694e26249f8442edbf2a606024798 Mon Sep 17 00:00:00 2001 From: Fede Date: Tue, 17 Jan 2023 13:36:48 +0100 Subject: [PATCH 01/10] Masking functions (WIP) --- junifer/data/__init__.py | 1 + junifer/data/masks.py | 78 +++++++++++++++++++++++---- junifer/markers/parcel_aggregation.py | 11 ++-- junifer/markers/sphere_aggregation.py | 4 +- 4 files changed, 75 insertions(+), 19 deletions(-) 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..22d905a32 100644 --- a/junifer/data/masks.py +++ b/junifer/data/masks.py @@ -4,9 +4,26 @@ # 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.image import resample_to_img from ..utils.logging import logger, raise_error from .utils import closest_resolution @@ -28,6 +45,12 @@ 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": {"family": "Callable", "func": compute_brain_mask}, + "compute_background": { + "family": "Callable", + "func": compute_background_mask, + }, + "compute_epi": {"family": "Callable", "func": compute_epi_mask}, } @@ -88,11 +111,45 @@ def list_masks() -> List[str]: return sorted(_available_masks.keys()) +def get_mask( + name: str, + target_img: "Nifti1Image", +) -> "Nifti1Image": + """Get mask, tailored for the target image. + + Parameters + ---------- + name : str + The name of the mask. + target_img : Nifti1Image + The image 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 + resolution = np.min(target_img.header.get_zooms()[:3]) + mask_img, _ = load_mask(name, path_only=False, resolution=resolution) + if callable(mask_img): + mask_img = mask_img(target_img) + else: + mask_img = resample_to_img( + mask_img, + 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 +166,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 +184,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/markers/parcel_aggregation.py b/junifer/markers/parcel_aggregation.py index 8d56e4dfc..a908c91ec 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 @@ -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(name=self.mask, target_img=t_input) + parcellation_bin = math_img( "np.logical_and(img, mask)", img=parcellation_bin, diff --git a/junifer/markers/sphere_aggregation.py b/junifer/markers/sphere_aggregation.py index cf54b6ce7..88819b7c0 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 @@ -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(name=self.mask, target_img=t_input) # Get seeds and labels coords, out_labels = load_coordinates(name=self.coords) masker = JuniferNiftiSpheresMasker( -- 2.52.0 From a71aa8ed4134ce4c614f5de1817fe1d4dbbee8ce Mon Sep 17 00:00:00 2001 From: Fede Date: Tue, 17 Jan 2023 16:26:09 +0100 Subject: [PATCH 02/10] Masks callable WIP --- docs/builtin.rst | 18 ++++++- docs/changes/latest.inc | 3 ++ junifer/data/masks.py | 12 ++++- junifer/data/tests/test_masks.py | 87 +++++++++++++++++++++++++++++++- 4 files changed, 116 insertions(+), 4 deletions(-) diff --git a/docs/builtin.rst b/docs/builtin.rst index 3c6e0b758..49aea2f6b 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,22 @@ 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`` + - 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`` + - 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`` + - 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` Planned ~~~~~~~ diff --git a/docs/changes/latest.inc b/docs/changes/latest.inc index 6d46352eb..7cf65389a 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`) + (:gh:`175` by `Fede Raimondo`_). + Bugs ~~~~ diff --git a/junifer/data/masks.py b/junifer/data/masks.py index 22d905a32..0620d1243 100644 --- a/junifer/data/masks.py +++ b/junifer/data/masks.py @@ -114,6 +114,7 @@ def list_masks() -> List[str]: def get_mask( name: str, target_img: "Nifti1Image", + callable_params: Optional[Dict[str, Any]] = None, ) -> "Nifti1Image": """Get mask, tailored for the target image. @@ -123,7 +124,8 @@ def get_mask( The name of the mask. target_img : Nifti1Image The image to which the mask will be applied. - + callable_params : dict, optional + Parameters to pass to the callable mask function (default None). Returns ------- Nifti1Image @@ -133,8 +135,14 @@ def get_mask( resolution = np.min(target_img.header.get_zooms()[:3]) mask_img, _ = load_mask(name, path_only=False, resolution=resolution) if callable(mask_img): - mask_img = mask_img(target_img) + if callable_params is None: + callable_params = {} + mask_img = mask_img(target_img, **callable_params) else: + if callable_params is not None: + raise_error( + "Cannot pass callable_params to a non-callable mask." + ) mask_img = resample_to_img( mask_img, target_img, diff --git a/junifer/data/tests/test_masks.py b/junifer/data/tests/test_masks.py index b00a76df6..29125bf19 100644 --- a/junifer/data/tests/test_masks.py +++ b/junifer/data/tests/test_masks.py @@ -6,15 +6,34 @@ # License: AGPL from pathlib import Path +import operator 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 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 +170,69 @@ 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"]["data"] + mask = get_mask(name="GM_prob0.2", target_img=vbm_gm) + + assert mask.shape == vbm_gm.shape + assert_array_equal(mask.affine, vbm_gm.affine) + + raw_mask_img, _ = load_mask("GM_prob0.2", resolution=1.5) + res_mask_img = resample_to_img( + raw_mask_img, + vbm_gm, + 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"]["data"] + mask = get_mask(name="identity", target_img=vbm_gm) + + assert_array_equal(mask.get_fdata(), vbm_gm.get_fdata()) + + del _available_masks["identity"] + + +def test_nilearn_compute_masks() -> None: + """Test using nilearn compute mask functions.""" + reader = DefaultDataReader() + with SPMAuditoryTestingDatagrabber() as dg: + input = dg["sub001"] + input = reader.fit_transform(input) + bold_img = input["BOLD"]["data"] + + mask_1 = get_mask(name="compute_brain", target_img=bold_img) + mask_2 = get_mask(name="compute_epi", target_img=bold_img) + mask_3 = get_mask(name="compute_background", target_img=bold_img) + + assert_array_equal(mask_1.affine, bold_img.affine) + assert_array_equal(mask_2.affine, bold_img.affine) + assert_array_equal(mask_3.affine, bold_img.affine) + + ni_mask_1 = compute_brain_mask(bold_img) + ni_mask_2 = compute_epi_mask(bold_img) + ni_mask_3 = compute_background_mask(bold_img) + + assert_array_equal(mask_1.get_fdata(), ni_mask_1.get_fdata()) + assert_array_equal(mask_2.get_fdata(), ni_mask_2.get_fdata()) + assert_array_equal(mask_3.get_fdata(), ni_mask_3.get_fdata()) -- 2.52.0 From 802c2f36cc18d7f93a2d9d97390a2d5fc111a73e Mon Sep 17 00:00:00 2001 From: Fede Raimondo Date: Wed, 18 Jan 2023 12:43:14 +0100 Subject: [PATCH 03/10] Prepare for subject-specific masks + leo's comments --- junifer/data/masks.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/junifer/data/masks.py b/junifer/data/masks.py index 0620d1243..1856dbdfd 100644 --- a/junifer/data/masks.py +++ b/junifer/data/masks.py @@ -45,12 +45,12 @@ 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": {"family": "Callable", "func": compute_brain_mask}, - "compute_background": { + "compute_brain_mask": {"family": "Callable", "func": compute_brain_mask}, + "compute_backgroun_mask": { "family": "Callable", "func": compute_background_mask, }, - "compute_epi": {"family": "Callable", "func": compute_epi_mask}, + "compute_epi_mask": {"family": "Callable", "func": compute_epi_mask}, } @@ -113,7 +113,7 @@ def list_masks() -> List[str]: def get_mask( name: str, - target_img: "Nifti1Image", + target_data: Dict[str, Any], callable_params: Optional[Dict[str, Any]] = None, ) -> "Nifti1Image": """Get mask, tailored for the target image. @@ -122,8 +122,9 @@ def get_mask( ---------- name : str The name of the mask. - target_img : Nifti1Image - The image to which the mask will be applied. + target_data : Dict[str, Any] + The corresponding item of the data object to which the mask will be + applied. callable_params : dict, optional Parameters to pass to the callable mask function (default None). Returns @@ -132,6 +133,7 @@ def get_mask( 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]) mask_img, _ = load_mask(name, path_only=False, resolution=resolution) if callable(mask_img): -- 2.52.0 From 36fcc283b22ad8292d6f8da73c08919b7e718d47 Mon Sep 17 00:00:00 2001 From: Fede Raimondo Date: Wed, 18 Jan 2023 13:05:05 +0100 Subject: [PATCH 04/10] fix tests --- junifer/data/tests/test_masks.py | 27 +++++++++++++++------------ junifer/markers/parcel_aggregation.py | 12 ++++++------ junifer/markers/sphere_aggregation.py | 6 +++--- 3 files changed, 24 insertions(+), 21 deletions(-) diff --git a/junifer/data/tests/test_masks.py b/junifer/data/tests/test_masks.py index 29125bf19..f67d9f4ac 100644 --- a/junifer/data/tests/test_masks.py +++ b/junifer/data/tests/test_masks.py @@ -178,16 +178,17 @@ def test_get_mask() -> None: with OasisVBMTestingDatagrabber() as dg: input = dg["sub-01"] input = reader.fit_transform(input) - vbm_gm = input["VBM_GM"]["data"] - mask = get_mask(name="GM_prob0.2", target_img=vbm_gm) + vbm_gm = input["VBM_GM"] + vbm_gm_img = vbm_gm["data"] + mask = get_mask(name="GM_prob0.2", target_data=vbm_gm) - assert mask.shape == vbm_gm.shape - assert_array_equal(mask.affine, vbm_gm.affine) + 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, + vbm_gm_img, interpolation="nearest", copy=True, ) @@ -205,10 +206,11 @@ def test_mask_callable() -> None: with OasisVBMTestingDatagrabber() as dg: input = dg["sub-01"] input = reader.fit_transform(input) - vbm_gm = input["VBM_GM"]["data"] - mask = get_mask(name="identity", target_img=vbm_gm) + vbm_gm = input["VBM_GM"] + vbm_gm_img = vbm_gm["data"] + mask = get_mask(name="identity", target_data=vbm_gm) - assert_array_equal(mask.get_fdata(), vbm_gm.get_fdata()) + assert_array_equal(mask.get_fdata(), vbm_gm_img.get_fdata()) del _available_masks["identity"] @@ -219,11 +221,12 @@ def test_nilearn_compute_masks() -> None: with SPMAuditoryTestingDatagrabber() as dg: input = dg["sub001"] input = reader.fit_transform(input) - bold_img = input["BOLD"]["data"] + bold = input["BOLD"] + bold_img = bold["data"] - mask_1 = get_mask(name="compute_brain", target_img=bold_img) - mask_2 = get_mask(name="compute_epi", target_img=bold_img) - mask_3 = get_mask(name="compute_background", target_img=bold_img) + mask_1 = get_mask(name="compute_brain", target_data=bold) + mask_2 = get_mask(name="compute_epi", target_data=bold) + mask_3 = get_mask(name="compute_background", target_data=bold) assert_array_equal(mask_1.affine, bold_img.affine) assert_array_equal(mask_2.affine, bold_img.affine) diff --git a/junifer/markers/parcel_aggregation.py b/junifer/markers/parcel_aggregation.py index a908c91ec..41b63619d 100644 --- a/junifer/markers/parcel_aggregation.py +++ b/junifer/markers/parcel_aggregation.py @@ -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,7 +186,7 @@ class ParcelAggregation(BaseMarker): if self.mask is not None: logger.debug(f"Masking with {self.mask}") - mask_img = get_mask(name=self.mask, target_img=t_input) + mask_img = get_mask(name=self.mask, target_data=input) parcellation_bin = math_img( "np.logical_and(img, mask)", @@ -196,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/sphere_aggregation.py b/junifer/markers/sphere_aggregation.py index 88819b7c0..287a5cc14 100644 --- a/junifer/markers/sphere_aggregation.py +++ b/junifer/markers/sphere_aggregation.py @@ -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 = get_mask(name=self.mask, target_img=t_input) + mask_img = get_mask(name=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 -- 2.52.0 From e7be8d71ffdacf6314df1d5369a66c293d47ff0d Mon Sep 17 00:00:00 2001 From: Fede Raimondo Date: Wed, 18 Jan 2023 15:00:37 +0100 Subject: [PATCH 05/10] Added one more mask + ready for merge --- docs/builtin.rst | 13 +++-- docs/changes/latest.inc | 4 +- junifer/data/masks.py | 82 +++++++++++++++++++++++++------- junifer/data/tests/test_masks.py | 23 +++++++-- 4 files changed, 94 insertions(+), 28 deletions(-) diff --git a/docs/builtin.rst b/docs/builtin.rst index 49aea2f6b..2f12e04ae 100644 --- a/docs/builtin.rst +++ b/docs/builtin.rst @@ -529,22 +529,29 @@ Available | Chimpanzee and Human Gray Matter Masks [Data set]. Zenodo. | https://doi.org/10.5281/zenodo.6463123 * - Nilearn's MNI152 1mm-resolution mask - - | ``compute_brain`` + - | ``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`` + - | ``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`` + - | ``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 7cf65389a..ab07e54f9 100644 --- a/docs/changes/latest.inc +++ b/docs/changes/latest.inc @@ -37,8 +37,8 @@ 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`) - (:gh:`175` 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/masks.py b/junifer/data/masks.py index 1856dbdfd..c64712d18 100644 --- a/junifer/data/masks.py +++ b/junifer/data/masks.py @@ -23,6 +23,7 @@ from nilearn.masking import ( 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 @@ -35,6 +36,30 @@ if TYPE_CHECKING: # Path to the VOIs _masks_path = Path(__file__).parent / "masks" + +def _fetch_icbm152_brain_gm_mask(target_img, **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. @@ -45,12 +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_backgroun_mask": { + "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}, + "compute_epi_mask": { + "family": "Callable", + "func": compute_epi_mask, + }, + "fetch_icbm152_brain_gm_mask": { + "family": "Callable", + "func": _fetch_icbm152_brain_gm_mask, + }, } @@ -112,21 +147,19 @@ def list_masks() -> List[str]: def get_mask( - name: str, + mask: Union[str, Dict], target_data: Dict[str, Any], - callable_params: Optional[Dict[str, Any]] = None, ) -> "Nifti1Image": """Get mask, tailored for the target image. Parameters ---------- - name : str - The name of the mask. + 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[str, Any] The corresponding item of the data object to which the mask will be applied. - callable_params : dict, optional - Parameters to pass to the callable mask function (default None). Returns ------- Nifti1Image @@ -135,18 +168,31 @@ def get_mask( # 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]) - mask_img, _ = load_mask(name, path_only=False, resolution=resolution) - if callable(mask_img): - if callable_params is None: - callable_params = {} - mask_img = mask_img(target_img, **callable_params) - else: - if callable_params is not None: + + if isinstance(mask, dict): + if len(mask) != 1: raise_error( - "Cannot pass callable_params to a non-callable mask." + "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_img, + mask_object, target_img, interpolation="nearest", copy=True, diff --git a/junifer/data/tests/test_masks.py b/junifer/data/tests/test_masks.py index f67d9f4ac..489857bbf 100644 --- a/junifer/data/tests/test_masks.py +++ b/junifer/data/tests/test_masks.py @@ -20,6 +20,7 @@ from nilearn.masking import ( compute_background_mask, compute_epi_mask, ) +from nilearn.datasets import fetch_icbm152_brain_gm_mask from junifer.data.masks import ( _load_vickery_patil_mask, @@ -180,7 +181,7 @@ def test_get_mask() -> None: input = reader.fit_transform(input) vbm_gm = input["VBM_GM"] vbm_gm_img = vbm_gm["data"] - mask = get_mask(name="GM_prob0.2", target_data=vbm_gm) + 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) @@ -208,7 +209,7 @@ def test_mask_callable() -> None: input = reader.fit_transform(input) vbm_gm = input["VBM_GM"] vbm_gm_img = vbm_gm["data"] - mask = get_mask(name="identity", target_data=vbm_gm) + mask = get_mask(mask="identity", target_data=vbm_gm) assert_array_equal(mask.get_fdata(), vbm_gm_img.get_fdata()) @@ -224,18 +225,30 @@ def test_nilearn_compute_masks() -> None: bold = input["BOLD"] bold_img = bold["data"] - mask_1 = get_mask(name="compute_brain", target_data=bold) - mask_2 = get_mask(name="compute_epi", target_data=bold) - mask_3 = get_mask(name="compute_background", target_data=bold) + mask_1 = get_mask(mask="compute_brain_mask", target_data=bold) + mask_2 = get_mask(mask="compute_epi_mask", target_data=bold) + mask_3 = get_mask(mask="compute_background_mask", target_data=bold) + mask_4 = get_mask(mask="fetch_icbm152_brain_gm_mask", target_data=bold) assert_array_equal(mask_1.affine, bold_img.affine) assert_array_equal(mask_2.affine, bold_img.affine) assert_array_equal(mask_3.affine, bold_img.affine) + assert_array_equal(mask_4.affine, bold_img.affine) ni_mask_1 = compute_brain_mask(bold_img) ni_mask_2 = compute_epi_mask(bold_img) ni_mask_3 = compute_background_mask(bold_img) + ni_mask_4 = fetch_icbm152_brain_gm_mask() assert_array_equal(mask_1.get_fdata(), ni_mask_1.get_fdata()) assert_array_equal(mask_2.get_fdata(), ni_mask_2.get_fdata()) assert_array_equal(mask_3.get_fdata(), ni_mask_3.get_fdata()) + + # Mask 4 needs resample + ni_mask_4_res = resample_to_img( + ni_mask_4, + bold_img, + interpolation="nearest", + copy=True, + ) + assert_array_equal(mask_4.get_fdata(), ni_mask_4_res.get_fdata()) -- 2.52.0 From 66772651cbdaa79b898743d88897d36308ff9c50 Mon Sep 17 00:00:00 2001 From: Fede Raimondo Date: Wed, 18 Jan 2023 15:57:24 +0100 Subject: [PATCH 06/10] fix tests --- junifer/markers/parcel_aggregation.py | 2 +- junifer/markers/sphere_aggregation.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/junifer/markers/parcel_aggregation.py b/junifer/markers/parcel_aggregation.py index 41b63619d..99f22e9ea 100644 --- a/junifer/markers/parcel_aggregation.py +++ b/junifer/markers/parcel_aggregation.py @@ -186,7 +186,7 @@ class ParcelAggregation(BaseMarker): if self.mask is not None: logger.debug(f"Masking with {self.mask}") - mask_img = get_mask(name=self.mask, target_data=input) + mask_img = get_mask(mask=self.mask, target_data=input) parcellation_bin = math_img( "np.logical_and(img, mask)", diff --git a/junifer/markers/sphere_aggregation.py b/junifer/markers/sphere_aggregation.py index 287a5cc14..47b1c57ea 100644 --- a/junifer/markers/sphere_aggregation.py +++ b/junifer/markers/sphere_aggregation.py @@ -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 = get_mask(name=self.mask, target_data=input) + mask_img = get_mask(mask=self.mask, target_data=input) # Get seeds and labels coords, out_labels = load_coordinates(name=self.coords) masker = JuniferNiftiSpheresMasker( -- 2.52.0 From a06fc27dcb7066c0da727d74f46ca46ad384bc24 Mon Sep 17 00:00:00 2001 From: Fede Raimondo Date: Thu, 19 Jan 2023 14:16:06 +0100 Subject: [PATCH 07/10] Address comments + update type hints for masks --- docs/changes/latest.inc | 4 +- junifer/data/masks.py | 9 ++-- junifer/data/tests/test_masks.py | 20 +++++++- junifer/markers/ets_rss.py | 2 +- junifer/markers/falff/falff_parcels.py | 2 +- junifer/markers/falff/falff_spheres.py | 4 +- ...ossparcellation_functional_connectivity.py | 4 +- .../functional_connectivity_base.py | 4 +- .../functional_connectivity_parcels.py | 2 +- .../functional_connectivity_spheres.py | 4 +- junifer/markers/parcel_aggregation.py | 2 +- junifer/markers/reho/reho_parcels.py | 4 +- junifer/markers/reho/reho_spheres.py | 4 +- junifer/markers/sphere_aggregation.py | 2 +- .../markers/tests/test_parcel_aggregation.py | 51 +++++++++++++++++++ 15 files changed, 93 insertions(+), 25 deletions(-) diff --git a/docs/changes/latest.inc b/docs/changes/latest.inc index ab07e54f9..0207cf57d 100644 --- a/docs/changes/latest.inc +++ b/docs/changes/latest.inc @@ -37,8 +37,8 @@ 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`_). +- 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/masks.py b/junifer/data/masks.py index c64712d18..754089796 100644 --- a/junifer/data/masks.py +++ b/junifer/data/masks.py @@ -37,14 +37,14 @@ if TYPE_CHECKING: _masks_path = Path(__file__).parent / "masks" -def _fetch_icbm152_brain_gm_mask(target_img, **kwargs): +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 + **kwargs : dict Keyword arguments to be passed to :func:`nilearn.datasets.fetch_icbm152_brain_gm_mask`. @@ -157,9 +157,10 @@ def get_mask( 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[str, Any] + target_data : dict The corresponding item of the data object to which the mask will be applied. + Returns ------- Nifti1Image @@ -190,7 +191,7 @@ def get_mask( 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.") + raise_error("Cannot pass callable params to a non-callable mask.") mask_img = resample_to_img( mask_object, target_img, diff --git a/junifer/data/tests/test_masks.py b/junifer/data/tests/test_masks.py index 489857bbf..a040e559b 100644 --- a/junifer/data/tests/test_masks.py +++ b/junifer/data/tests/test_masks.py @@ -216,6 +216,20 @@ def test_mask_callable() -> None: 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) + + def test_nilearn_compute_masks() -> None: """Test using nilearn compute mask functions.""" reader = DefaultDataReader() @@ -225,7 +239,9 @@ def test_nilearn_compute_masks() -> None: bold = input["BOLD"] bold_img = bold["data"] - mask_1 = get_mask(mask="compute_brain_mask", target_data=bold) + mask_1 = get_mask( + mask={"compute_brain_mask": {"threshold": 0.2}}, target_data=bold + ) mask_2 = get_mask(mask="compute_epi_mask", target_data=bold) mask_3 = get_mask(mask="compute_background_mask", target_data=bold) mask_4 = get_mask(mask="fetch_icbm152_brain_gm_mask", target_data=bold) @@ -235,7 +251,7 @@ def test_nilearn_compute_masks() -> None: assert_array_equal(mask_3.affine, bold_img.affine) assert_array_equal(mask_4.affine, bold_img.affine) - ni_mask_1 = compute_brain_mask(bold_img) + ni_mask_1 = compute_brain_mask(bold_img, threshold=0.2) ni_mask_2 = compute_epi_mask(bold_img) ni_mask_3 = compute_background_mask(bold_img) ni_mask_4 = fetch_icbm152_brain_gm_mask() 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 99f22e9ea..6115d53bc 100644 --- a/junifer/markers/parcel_aggregation.py +++ b/junifer/markers/parcel_aggregation.py @@ -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: 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 47b1c57ea..d25dd5a9e 100644 --- a/junifer/markers/sphere_aggregation.py +++ b/junifer/markers/sphere_aggregation.py @@ -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: diff --git a/junifer/markers/tests/test_parcel_aggregation.py b/junifer/markers/tests/test_parcel_aggregation.py index 3274e0ad4..2378adddd 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,56 @@ 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 nilearn computed mask.""" + + # 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. -- 2.52.0 From ed1f26eb48669790cc24efc0ac4618f3c42df2c0 Mon Sep 17 00:00:00 2001 From: Fede Raimondo Date: Thu, 19 Jan 2023 14:43:26 +0100 Subject: [PATCH 08/10] Fix linter --- junifer/data/tests/test_masks.py | 1 - junifer/markers/tests/test_parcel_aggregation.py | 4 +--- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/junifer/data/tests/test_masks.py b/junifer/data/tests/test_masks.py index a040e559b..37fb83a91 100644 --- a/junifer/data/tests/test_masks.py +++ b/junifer/data/tests/test_masks.py @@ -6,7 +6,6 @@ # License: AGPL from pathlib import Path -import operator import pytest from numpy.testing import ( diff --git a/junifer/markers/tests/test_parcel_aggregation.py b/junifer/markers/tests/test_parcel_aggregation.py index 2378adddd..007eccfe1 100644 --- a/junifer/markers/tests/test_parcel_aggregation.py +++ b/junifer/markers/tests/test_parcel_aggregation.py @@ -242,12 +242,11 @@ def test_ParcelAggregation_3D_mask() -> None: def test_ParcelAggregation_3D_mask_computed() -> None: - """Test ParcelAggregation object on 3D images with nilearn computed mask.""" + """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] @@ -290,7 +289,6 @@ def test_ParcelAggregation_3D_mask_computed() -> None: 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. -- 2.52.0 From d61a20354aef07fb8f9fd70734d15f260316831e Mon Sep 17 00:00:00 2001 From: Fede Raimondo Date: Thu, 19 Jan 2023 14:54:58 +0100 Subject: [PATCH 09/10] update tests --- junifer/data/tests/test_masks.py | 77 ++++++++++++++++++++------------ 1 file changed, 49 insertions(+), 28 deletions(-) diff --git a/junifer/data/tests/test_masks.py b/junifer/data/tests/test_masks.py index 37fb83a91..16c21bdf0 100644 --- a/junifer/data/tests/test_masks.py +++ b/junifer/data/tests/test_masks.py @@ -5,6 +5,8 @@ # Synchon Mandal # License: AGPL +from typing import Callable, Dict, Union + from pathlib import Path import pytest @@ -229,8 +231,32 @@ def test_get_mask_errors() -> None: get_mask(mask={"GM_prob0.2": {"param": 1}}, target_data=vbm_gm) -def test_nilearn_compute_masks() -> None: - """Test using nilearn compute mask functions.""" +@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"] @@ -238,32 +264,27 @@ def test_nilearn_compute_masks() -> None: bold = input["BOLD"] bold_img = bold["data"] - mask_1 = get_mask( - mask={"compute_brain_mask": {"threshold": 0.2}}, target_data=bold + if params is None: + params = {} + mask_spec = mask_name + else: + mask_spec = {mask_name: params} + + mask = get_mask( + mask=mask_spec, target_data=bold ) - mask_2 = get_mask(mask="compute_epi_mask", target_data=bold) - mask_3 = get_mask(mask="compute_background_mask", target_data=bold) - mask_4 = get_mask(mask="fetch_icbm152_brain_gm_mask", target_data=bold) - assert_array_equal(mask_1.affine, bold_img.affine) - assert_array_equal(mask_2.affine, bold_img.affine) - assert_array_equal(mask_3.affine, bold_img.affine) - assert_array_equal(mask_4.affine, bold_img.affine) + assert_array_equal(mask.affine, bold_img.affine) - ni_mask_1 = compute_brain_mask(bold_img, threshold=0.2) - ni_mask_2 = compute_epi_mask(bold_img) - ni_mask_3 = compute_background_mask(bold_img) - ni_mask_4 = fetch_icbm152_brain_gm_mask() - - assert_array_equal(mask_1.get_fdata(), ni_mask_1.get_fdata()) - assert_array_equal(mask_2.get_fdata(), ni_mask_2.get_fdata()) - assert_array_equal(mask_3.get_fdata(), ni_mask_3.get_fdata()) - - # Mask 4 needs resample - ni_mask_4_res = resample_to_img( - ni_mask_4, - bold_img, - interpolation="nearest", - copy=True, - ) - assert_array_equal(mask_4.get_fdata(), ni_mask_4_res.get_fdata()) + 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()) -- 2.52.0 From 7ffc01f4eec43454d9d1e3102f5f8342723ed50d Mon Sep 17 00:00:00 2001 From: Fede Raimondo Date: Thu, 19 Jan 2023 15:03:18 +0100 Subject: [PATCH 10/10] Damn linter --- junifer/data/tests/test_masks.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/junifer/data/tests/test_masks.py b/junifer/data/tests/test_masks.py index 16c21bdf0..ec3739612 100644 --- a/junifer/data/tests/test_masks.py +++ b/junifer/data/tests/test_masks.py @@ -237,12 +237,19 @@ def test_get_mask_errors() -> None: ("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), + ( + "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 + mask_name: str, + function: Callable, + params: Union[Dict, None], + resample: bool, ) -> None: """Test using nilearn compute mask functions. @@ -270,9 +277,7 @@ def test_nilearn_compute_masks( else: mask_spec = {mask_name: params} - mask = get_mask( - mask=mask_spec, target_data=bold - ) + mask = get_mask(mask=mask_spec, target_data=bold) assert_array_equal(mask.affine, bold_img.affine) -- 2.52.0