[ENH]: Further Aggregation methods for SphereAggregation Marker #102
|
|
@ -85,6 +85,8 @@ Enhancements
|
|||
|
||||
- Refactor the :class:`junifer.datagrabber.BaseDataGrabber` class to allow for easier subclassing (:gh:`123` by `Fede Raimondo`_).
|
||||
|
||||
- Allow custom aggregation method for :class:`junifer.markers.SphereAggregation` (:gh:`102` by `Synchon Mandal`_).
|
||||
|
||||
Bugs
|
||||
~~~~
|
||||
|
||||
|
|
@ -96,8 +98,4 @@ Bugs
|
|||
- Fix a bug in which a datalad dataset could remove a user-cloned dataset (:gh:`53` by `Fede Raimondo`_)
|
||||
|
||||
API changes
|
||||
~~~~~~~~~~~
|
||||
|
||||
- Add new argument ``check_file_existence`` for PatternDataGrabber which allows skipping actual file existence check.
|
||||
This is needed for datagrabbers which extend from PatternDataGrabber and DataladDataGrabber and have one or more
|
||||
Datalad subdatasets inside the dataset, for example, DataladHCP1200.
|
||||
~~~~~~~~~~~
|
||||
|
|
@ -16,5 +16,6 @@ from . import (
|
|||
stats,
|
||||
storage,
|
||||
utils,
|
||||
external,
|
||||
)
|
||||
from ._version import __version__
|
||||
|
|
|
|||
4
junifer/external/__init__.py
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
"""Provide imports for external sub-package."""
|
||||
|
||||
# Authors: Synchon Mandal <s.mandal@fz-juelich.de>
|
||||
# License: AGPL
|
||||
6
junifer/external/nilearn/__init__.py
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
"""Provide imports for custom nilearn objects sub-package."""
|
||||
|
||||
# Authors: Synchon Mandal <s.mandal@fz-juelich.de>
|
||||
# License: AGPL
|
||||
|
||||
from .junifer_nifti_spheres_masker import JuniferNiftiSpheresMasker
|
||||
290
junifer/external/nilearn/junifer_nifti_spheres_masker.py
vendored
Normal file
|
|
@ -0,0 +1,290 @@
|
|||
"""Provide JuniferNiftiSpheresMasker class."""
|
||||
|
||||
# Authors: Synchon Mandal <s.mandal@fz-juelich.de>
|
||||
|
Yeah from nilearn, okay will add the information. Yeah from nilearn, okay will add the information.
Okay two doubts: Okay two doubts:
- nilearn has a different [style](https://github.com/nilearn/nilearn/blob/c3a9390af2349e3263fd0b2991c17a927a142f6c/nilearn/maskers/nifti_spheres_masker.py#L1) of module docstrings, so it will look way different when it renders.
- According to BSD license (which nilearn uses), needs the [license](https://github.com/nilearn/nilearn/blob/main/LICENSE) to be there in anything that redistributes the code.
1) Change the docstrings
2) Keep the header of the file where it says MIT License, including the authors and stuff
https://github.com/nilearn/nilearn/blob/98a3ee060b55aa073118885d11cc6a1cecd95059/LICENSE#L4-L35
You mean make it nilearn style?
They have BSD but I get your point. > * Change the docstrings
You mean make it nilearn style?
> * Keep the header of the file where it says MIT License, including the authors and stuff
They have BSD but I get your point.
Shouldn't it be the latest license? Licensing is always tricky. > https://github.com/nilearn/nilearn/blob/98a3ee060b55aa073118885d11cc6a1cecd95059/LICENSE#L4-L35
Shouldn't it be the latest license? Licensing is always tricky.
Make them our style
No, the first is nilearn license. The rest are software incorporated in nilearn. > > * Change the docstrings
>
> You mean make it nilearn style?
Make them our style
> > https://github.com/nilearn/nilearn/blob/98a3ee060b55aa073118885d11cc6a1cecd95059/LICENSE#L4-L35
>
> Shouldn't it be the latest license? Licensing is always tricky.
No, the first is nilearn license. The rest are software incorporated in nilearn.
I meant that the link you referenced has I meant that the link you referenced has `2007-2015` and the one I referenced has `2007-2022`. So, should we go for the new one?
check the main branch: https://github.com/nilearn/nilearn/blob/main/LICENSE check the main branch: https://github.com/nilearn/nilearn/blob/main/LICENSE
|
||||
# License: AGPL
|
||||
|
||||
from typing import TYPE_CHECKING, Callable, List, Optional, Tuple, Union
|
||||
|
||||
import numpy as np
|
||||
from nilearn._utils.class_inspect import get_params
|
||||
from nilearn._utils.niimg import img_data_dtype
|
||||
from nilearn._utils.niimg_conversions import check_niimg_4d
|
||||
from nilearn.maskers import NiftiSpheresMasker
|
||||
from nilearn.maskers.base_masker import _filter_and_extract
|
||||
from nilearn.maskers.nifti_spheres_masker import _iter_signals_from_spheres
|
||||
|
||||
from ...utils import raise_error
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pathlib import Path
|
||||
|
||||
from nibabel import Nifti1Image, Nifti2Image
|
||||
from numpy.typing import ArrayLike, DTypeLike
|
||||
from pandas import DataFrame
|
||||
|
||||
|
||||
"""
|
||||
New BSD License
|
||||
|
||||
Copyright (c) 2007 - 2022 The nilearn developers.
|
||||
All rights reserved.
|
||||
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
a. Redistributions of source code must retain the above copyright notice,
|
||||
this list of conditions and the following disclaimer.
|
||||
b. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
c. Neither the name of the nilearn developers nor the names of
|
||||
its contributors may be used to endorse or promote products
|
||||
derived from this software without specific prior written
|
||||
permission.
|
||||
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR
|
||||
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
|
||||
OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
|
||||
DAMAGE.
|
||||
"""
|
||||
|
||||
|
||||
class _JuniferExtractionFunctor:
|
||||
"""Functor to extract signals from spheres.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
seeds_ : list of triple of coordinates in native space
|
||||
Seed definitions. List of coordinates of the seeds in the same space
|
||||
as the images (typically MNI or TAL).
|
||||
radius : float
|
||||
Indicates, in millimeters, the radius for the sphere around the seed.
|
||||
mask_img : Niimg-like object
|
||||
Mask to apply to regions before extracting signals.
|
||||
agg_func : callable
|
||||
The function to aggregate signals using.
|
||||
allow_overlap : bool
|
||||
If False, an error is raised if the maps overlap.
|
||||
dtype : any type that can be coerced into a numpy dtype or "auto"
|
||||
The dtype for the extraction. If "auto", the data will be converted to
|
||||
int32 if dtype is discrete and float32 if it is continuous.
|
||||
|
||||
"""
|
||||
|
||||
func_name = "junifer_nifti_spheres_masker_extractor"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
seeds_: "ArrayLike",
|
||||
radius: Optional[float],
|
||||
mask_img: Union["Nifti1Image", "Nifti2Image", None],
|
||||
agg_func: Callable,
|
||||
allow_overlap: bool,
|
||||
dtype: Union["DTypeLike", str, None],
|
||||
) -> None:
|
||||
self.seeds_ = seeds_
|
||||
self.radius = radius
|
||||
self.mask_img = mask_img
|
||||
self.agg_func = agg_func
|
||||
self.allow_overlap = allow_overlap
|
||||
self.dtype = dtype
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
imgs: Union["Nifti1Image", "Nifti2Image"],
|
||||
) -> Tuple["ArrayLike", None]:
|
||||
"""Implement function call overloading.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
imgs : 4D Niimg-like object
|
||||
If ``imgs`` is an iterable, checks if data is really 4D. Then,
|
||||
considering that it is a list of ``img``, load them one by one.
|
||||
If ``img`` is a string, consider it as a path to Nifti image and
|
||||
call :func:`nibabel.load` on it.
|
||||
If it is an object, check if the affine attribute is present and
|
||||
that :func:`nilearn.image.get_data` returns a result, eval raise
|
||||
TypeError.
|
||||
|
||||
Raises
|
||||
------
|
||||
TypeError
|
||||
If ``img`` in ``imgs`` is an object without the affine attribute.
|
||||
|
||||
Returns
|
||||
-------
|
||||
tuple of numpy.ndarray and None
|
||||
The numpy.ndarray returned is the array of extracted signals.
|
||||
|
||||
"""
|
||||
n_seeds = len(self.seeds_)
|
||||
imgs = check_niimg_4d(imgs, dtype=self.dtype)
|
||||
|
||||
signals = np.empty(
|
||||
(imgs.shape[3], n_seeds), dtype=img_data_dtype(imgs)
|
||||
)
|
||||
for i, sphere in enumerate(
|
||||
_iter_signals_from_spheres(
|
||||
seeds=self.seeds_,
|
||||
niimg=imgs,
|
||||
radius=self.radius,
|
||||
allow_overlap=self.allow_overlap,
|
||||
mask_img=self.mask_img,
|
||||
)
|
||||
):
|
||||
signals[:, i] = self.agg_func(sphere, axis=1)
|
||||
return signals, None
|
||||
|
||||
|
||||
class JuniferNiftiSpheresMasker(NiftiSpheresMasker):
|
||||
"""Class for custom NiftiSpheresMasker.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
seeds : list of triplet of coordinates in native space
|
||||
Seed definitions. List of coordinates of the seeds in the same space
|
||||
as the images (typically MNI or TAL).
|
||||
radius : float, optional
|
||||
Indicates, in millimeters, the radius for the sphere around the seed.
|
||||
If None, signal is extracted on a single voxel (default None).
|
||||
mask_img : Niimg-like object, optional
|
||||
Mask to apply to regions before extracting signals (default None).
|
||||
agg_func : callable, optional
|
||||
The function to aggregate signals using (default numpy.mean).
|
||||
allow_overlap : bool, optional
|
||||
If False, an error is raised if the maps overlap (default None).
|
||||
dtype : any type that can be coerced into a numpy dtype or "auto", optional
|
||||
The dtype for the extraction. If "auto", the data will be converted to
|
||||
int32 if dtype is discrete and float32 if it is continuous
|
||||
(default None).
|
||||
**kwargs
|
||||
Keyword arguments are passed to the
|
||||
:func:`nilearn.maskers.NiftiSpheresMasker`.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
seeds: "ArrayLike",
|
||||
radius: Optional[float] = None,
|
||||
mask_img: Union["Nifti1Image", "Nifti2Image", None] = None,
|
||||
agg_func: Callable = np.mean,
|
||||
allow_overlap: bool = False,
|
||||
dtype: Union["DTypeLike", str, None] = None,
|
||||
**kwargs, # TODO: to keep or not?
|
||||
) -> None:
|
||||
self.agg_func = agg_func
|
||||
super().__init__(
|
||||
seeds=seeds,
|
||||
radius=radius,
|
||||
mask_img=mask_img,
|
||||
allow_overlap=allow_overlap,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def transform_single_imgs(
|
||||
self,
|
||||
imgs: Union["Nifti1Image", "Nifti2Image"],
|
||||
confounds: Union[
|
||||
str, "Path", "ArrayLike", "DataFrame", List, None
|
||||
] = None,
|
||||
sample_mask: Union["ArrayLike", List, Tuple, None] = None,
|
||||
) -> "ArrayLike":
|
||||
"""Extract signals from a single 4D niimg.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
imgs : 3D/4D Niimg-like object
|
||||
Images to process.
|
||||
If a 3D niimg is provided, a singleton dimension will be added to
|
||||
the output to represent the single scan in the niimg.
|
||||
confounds : CSV file or array-like or pandas.DataFrame, optional
|
||||
This parameter is passed to :func:`nilearn.signal.clean`.
|
||||
Please see the related documentation for details.
|
||||
shape: (number of scans, number of confounds)
|
||||
sample_mask : Any type compatible with numpy-array indexing, optional
|
||||
Masks the niimgs along time/fourth dimension to perform scrubbing
|
||||
(remove volumes with high motion) and/or non-steady-state volumes.
|
||||
This parameter is passed to :func:`nilearn.signal.clean`.
|
||||
shape: (number of scans - number of volumes removed, )
|
||||
|
||||
Returns
|
||||
-------
|
||||
region_signals : 2D numpy.ndarray
|
||||
Signal for each sphere.
|
||||
shape: (number of scans, number of spheres)
|
||||
|
||||
Warns
|
||||
-----
|
||||
DeprecationWarning
|
||||
If a 3D niimg input is provided, the current behavior
|
||||
(adding a singleton dimension to produce a 2D array) is deprecated.
|
||||
Starting in version 0.12, a 1D array will be returned for 3D
|
||||
inputs.
|
||||
|
||||
"""
|
||||
self._check_fitted()
|
||||
|
||||
params = get_params(NiftiSpheresMasker, self)
|
||||
|
||||
signals, _ = self._cache(
|
||||
_filter_and_extract, ignore=["verbose", "memory", "memory_level"]
|
||||
)(
|
||||
imgs,
|
||||
_JuniferExtractionFunctor(
|
||||
seeds_=self.seeds_,
|
||||
radius=self.radius,
|
||||
mask_img=self.mask_img,
|
||||
agg_func=self.agg_func,
|
||||
allow_overlap=self.allow_overlap,
|
||||
dtype=self.dtype,
|
||||
),
|
||||
# Pre-processing
|
||||
params,
|
||||
confounds=confounds,
|
||||
sample_mask=sample_mask,
|
||||
dtype=self.dtype,
|
||||
# Caching
|
||||
memory=self.memory,
|
||||
memory_level=self.memory_level,
|
||||
# kwargs
|
||||
verbose=self.verbose,
|
||||
)
|
||||
return signals
|
||||
|
||||
def inverse_transform(self, region_signals: "ArrayLike") -> "Nifti1Image":
|
||||
"""Compute voxel signals from spheres signals.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
region_signals : 1D/2D numpy.ndarray
|
||||
Signal for each region.
|
||||
If a 1D array is provided, then the shape should be
|
||||
(number of elements,), and a 3D img will be returned.
|
||||
If a 2D array is provided, then the shape should be
|
||||
(number of scans, number of elements), and a 4D img will be
|
||||
returned.
|
||||
|
||||
Returns
|
||||
-------
|
||||
voxel_signals : nibabel.nifti1.Nifti1Image
|
||||
Signal for each sphere.
|
||||
shape: (mask_img, number of scans).
|
||||
|
||||
"""
|
||||
raise_error(
|
||||
msg="As this class allows multiple methods of aggregation while "
|
||||
"transforming, some of which are non-reversible, "
|
||||
"inverse_transform() should not be implemented.",
|
||||
klass=NotImplementedError,
|
||||
)
|
||||
334
junifer/external/nilearn/tests/test_junifer_nifti_spheres_masker.py
vendored
Normal file
|
|
@ -0,0 +1,334 @@
|
|||
"""Provide tests for JuniferNiftiSpheresMasker class."""
|
||||
|
||||
# Authors: Synchon Mandal <s.mandal@fz-juelich.de>
|
||||
|
Same here, how much is it nilearn's code? Same here, how much is it nilearn's code?
The whole of it. I removed the tests for The whole of it. I removed the tests for `inverse_transform`.
then keep nilearn's header then. then keep nilearn's header then.
This will also need the license, I believe? This will also need the license, I believe?
Yes. Yes.
|
||||
# License: AGPL
|
||||
|
||||
import warnings
|
||||
|
||||
import nibabel
|
||||
import numpy as np
|
||||
import pytest
|
||||
from nilearn._utils import data_gen
|
||||
from nilearn.image import get_data
|
||||
from numpy.testing import assert_array_equal
|
||||
|
||||
from junifer.external.nilearn import JuniferNiftiSpheresMasker
|
||||
|
||||
|
||||
"""
|
||||
New BSD License
|
||||
|
||||
Copyright (c) 2007 - 2022 The nilearn developers.
|
||||
All rights reserved.
|
||||
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
a. Redistributions of source code must retain the above copyright notice,
|
||||
this list of conditions and the following disclaimer.
|
||||
b. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
c. Neither the name of the nilearn developers nor the names of
|
||||
its contributors may be used to endorse or promote products
|
||||
derived from this software without specific prior written
|
||||
permission.
|
||||
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR
|
||||
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
|
||||
OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
|
||||
DAMAGE.
|
||||
"""
|
||||
|
||||
|
||||
def test_seed_extraction() -> None:
|
||||
"""Test seed extraction."""
|
||||
data = np.random.RandomState(42).random_sample((3, 3, 3, 5))
|
||||
img = nibabel.Nifti1Image(data, np.eye(4))
|
||||
masker = JuniferNiftiSpheresMasker(seeds=[(1, 1, 1)])
|
||||
# Test the fit
|
||||
masker.fit()
|
||||
# Test the transform
|
||||
s = masker.transform(img)
|
||||
assert_array_equal(s[:, 0], data[1, 1, 1])
|
||||
|
||||
|
||||
def test_sphere_extraction() -> None:
|
||||
"""Test sphere extraction."""
|
||||
data = np.random.RandomState(42).random_sample((3, 3, 3, 5))
|
||||
img = nibabel.Nifti1Image(data, np.eye(4))
|
||||
masker = JuniferNiftiSpheresMasker(seeds=[(1, 1, 1)], radius=1)
|
||||
|
||||
# Check attributes defined at fit
|
||||
assert not hasattr(masker, "seeds_")
|
||||
assert not hasattr(masker, "n_elements_")
|
||||
|
||||
masker.fit()
|
||||
|
||||
# Check attributes defined at fit
|
||||
assert hasattr(masker, "seeds_")
|
||||
assert hasattr(masker, "n_elements_")
|
||||
assert masker.n_elements_ == 1
|
||||
|
||||
# Test the fit
|
||||
masker.fit()
|
||||
|
||||
# Test the transform
|
||||
s = masker.transform(img)
|
||||
mask = np.zeros((3, 3, 3), dtype=bool)
|
||||
mask[:, 1, 1] = True
|
||||
mask[1, :, 1] = True
|
||||
mask[1, 1, :] = True
|
||||
assert_array_equal(s[:, 0], np.mean(data[mask], axis=0))
|
||||
|
||||
# Now with a mask
|
||||
mask_img = np.zeros((3, 3, 3))
|
||||
mask_img[1, :, :] = 1
|
||||
mask_img = nibabel.Nifti1Image(mask_img, np.eye(4))
|
||||
masker = JuniferNiftiSpheresMasker(
|
||||
seeds=[(1, 1, 1)],
|
||||
radius=1,
|
||||
mask_img=mask_img,
|
||||
)
|
||||
masker.fit()
|
||||
s = masker.transform(img)
|
||||
assert_array_equal(
|
||||
s[:, 0],
|
||||
np.mean(
|
||||
data[np.logical_and(mask, get_data(mask_img))],
|
||||
axis=0,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def test_anisotropic_sphere_extraction() -> None:
|
||||
"""Test anisotropic sphere extraction."""
|
||||
data = np.random.RandomState(42).random_sample((3, 3, 3, 5))
|
||||
affine = np.eye(4)
|
||||
affine[0, 0] = 2
|
||||
affine[2, 2] = 2
|
||||
img = nibabel.Nifti1Image(data, affine)
|
||||
masker = JuniferNiftiSpheresMasker(seeds=[(2, 1, 2)], radius=1)
|
||||
# Test the fit
|
||||
masker.fit()
|
||||
# Test the transform
|
||||
s = masker.transform(img)
|
||||
mask = np.zeros((3, 3, 3), dtype=bool)
|
||||
mask[1, :, 1] = True
|
||||
assert_array_equal(s[:, 0], np.mean(data[mask], axis=0))
|
||||
# Now with a mask
|
||||
mask_img = np.zeros((3, 2, 3))
|
||||
mask_img[1, 0, 1] = 1
|
||||
affine_2 = affine.copy()
|
||||
affine_2[0, 0] = 4
|
||||
mask_img = nibabel.Nifti1Image(mask_img, affine=affine_2)
|
||||
masker = JuniferNiftiSpheresMasker(
|
||||
seeds=[(2, 1, 2)],
|
||||
radius=1,
|
||||
mask_img=mask_img,
|
||||
)
|
||||
|
||||
masker.fit()
|
||||
s = masker.transform(img)
|
||||
assert_array_equal(s[:, 0], data[1, 0, 1])
|
||||
|
||||
|
||||
def test_errors() -> None:
|
||||
"""Test errors."""
|
||||
masker = JuniferNiftiSpheresMasker(seeds=([1, 2]), radius=0.2)
|
||||
with pytest.raises(ValueError, match="Seeds must be a list .+"):
|
||||
masker.fit()
|
||||
|
||||
|
||||
def test_nifti_spheres_masker_overlap() -> None:
|
||||
"""Test overlapping sphere extraction."""
|
||||
# Test resampling in NiftiMapsMasker
|
||||
affine = np.eye(4)
|
||||
shape = (5, 5, 5)
|
||||
|
||||
data = np.random.RandomState(42).random_sample(shape + (5,))
|
||||
fmri_img = nibabel.Nifti1Image(data, affine)
|
||||
|
||||
seeds = [(0, 0, 0), (2, 2, 2)]
|
||||
|
||||
overlapping_masker = JuniferNiftiSpheresMasker(
|
||||
seeds,
|
||||
radius=1,
|
||||
allow_overlap=True,
|
||||
)
|
||||
overlapping_masker.fit_transform(fmri_img)
|
||||
overlapping_masker = JuniferNiftiSpheresMasker(
|
||||
seeds,
|
||||
radius=2,
|
||||
allow_overlap=True,
|
||||
)
|
||||
overlapping_masker.fit_transform(fmri_img)
|
||||
|
||||
noverlapping_masker = JuniferNiftiSpheresMasker(
|
||||
seeds,
|
||||
radius=1,
|
||||
allow_overlap=False,
|
||||
)
|
||||
noverlapping_masker.fit_transform(fmri_img)
|
||||
noverlapping_masker = JuniferNiftiSpheresMasker(
|
||||
seeds,
|
||||
radius=2,
|
||||
allow_overlap=False,
|
||||
)
|
||||
with pytest.raises(ValueError, match="Overlap detected"):
|
||||
noverlapping_masker.fit_transform(fmri_img)
|
||||
|
||||
|
||||
def test_small_radius() -> None:
|
||||
"""Test sphere extraction with small radius."""
|
||||
affine = np.eye(4)
|
||||
shape = (3, 3, 3)
|
||||
|
||||
data = np.random.RandomState(42).random_sample(shape)
|
||||
mask = np.zeros(shape)
|
||||
mask[1, 1, 1] = 1
|
||||
mask[2, 2, 2] = 1
|
||||
affine = np.eye(4) * 1.2
|
||||
seed = (1.4, 1.4, 1.4)
|
||||
|
||||
masker = JuniferNiftiSpheresMasker(
|
||||
seeds=[seed],
|
||||
radius=0.1,
|
||||
mask_img=nibabel.Nifti1Image(mask, affine),
|
||||
)
|
||||
masker.fit_transform(nibabel.Nifti1Image(data, affine))
|
||||
|
||||
# Test if masking is taken into account
|
||||
mask[1, 1, 1] = 0
|
||||
mask[1, 1, 0] = 1
|
||||
|
||||
masker = JuniferNiftiSpheresMasker(
|
||||
seeds=[seed],
|
||||
radius=0.1,
|
||||
mask_img=nibabel.Nifti1Image(mask, affine),
|
||||
)
|
||||
with pytest.raises(ValueError, match="These spheres are empty"):
|
||||
masker.fit_transform(nibabel.Nifti1Image(data, affine))
|
||||
|
||||
masker = JuniferNiftiSpheresMasker(
|
||||
seeds=[seed],
|
||||
radius=1.6,
|
||||
mask_img=nibabel.Nifti1Image(mask, affine),
|
||||
)
|
||||
masker.fit_transform(nibabel.Nifti1Image(data, affine))
|
||||
|
||||
|
||||
def test_is_nifti_spheres_masker_give_nans() -> None:
|
||||
"""Test NaN interaction with masker."""
|
||||
affine = np.eye(4)
|
||||
|
||||
data_with_nans = np.zeros((10, 10, 10), dtype=np.float32)
|
||||
data_with_nans[:, :, :] = np.nan
|
||||
|
||||
data_without_nans = np.random.RandomState(42).random_sample((9, 9, 9))
|
||||
indices = np.nonzero(data_without_nans)
|
||||
|
||||
# Leaving nans outside of some data
|
||||
data_with_nans[indices] = data_without_nans[indices]
|
||||
img = nibabel.Nifti1Image(data_with_nans, affine)
|
||||
seed = [(7, 7, 7)]
|
||||
|
||||
# Interaction of seed with nans
|
||||
masker = JuniferNiftiSpheresMasker(seeds=seed, radius=2.0)
|
||||
assert not np.isnan(np.sum(masker.fit_transform(img)))
|
||||
|
||||
mask = np.ones((9, 9, 9))
|
||||
mask_img = nibabel.Nifti1Image(mask, affine)
|
||||
# When mask_img is provided, the seed interacts within the brain, so no nan
|
||||
masker = JuniferNiftiSpheresMasker(
|
||||
seeds=seed,
|
||||
radius=2.0,
|
||||
mask_img=mask_img,
|
||||
)
|
||||
assert not np.isnan(np.sum(masker.fit_transform(img)))
|
||||
|
||||
|
||||
def test_standardization() -> None:
|
||||
"""Test standardization."""
|
||||
data = np.random.RandomState(42).random_sample((3, 3, 3, 5))
|
||||
img = nibabel.Nifti1Image(data, np.eye(4))
|
||||
|
||||
# test zscore
|
||||
masker = JuniferNiftiSpheresMasker(seeds=[(1, 1, 1)], standardize="zscore")
|
||||
# Test the fit
|
||||
s = masker.fit_transform(img)
|
||||
|
||||
np.testing.assert_almost_equal(s.mean(), 0)
|
||||
np.testing.assert_almost_equal(s.std(), 1)
|
||||
|
||||
# test psc
|
||||
masker = JuniferNiftiSpheresMasker(seeds=[(1, 1, 1)], standardize="psc")
|
||||
# Test the fit
|
||||
s = masker.fit_transform(img)
|
||||
|
||||
np.testing.assert_almost_equal(s.mean(), 0)
|
||||
np.testing.assert_almost_equal(
|
||||
s.ravel(),
|
||||
data[1, 1, 1] / data[1, 1, 1].mean() * 100 - 100,
|
||||
)
|
||||
|
||||
|
||||
def test_nifti_spheres_masker_inverse_transform() -> None:
|
||||
"""Test inverse transform."""
|
||||
data = np.random.RandomState(42).random_sample((3, 3, 3, 5))
|
||||
masker = JuniferNiftiSpheresMasker(seeds=[(1, 1, 1)], radius=1)
|
||||
with pytest.raises(
|
||||
NotImplementedError,
|
||||
match="some of which are non-reversible",
|
||||
):
|
||||
masker.inverse_transform(data[0, 0, 0, :])
|
||||
|
||||
|
||||
def test_nifti_spheres_masker_io_shapes() -> None:
|
||||
"""Ensure that masker handles 1D/2D/3D/4D data appropriately.
|
||||
|
||||
transform(4D image) --> 2D output, no warning
|
||||
transform(3D image) --> 2D output, DeprecationWarning
|
||||
|
||||
"""
|
||||
n_regions, n_volumes = 2, 5
|
||||
shape_3d = (10, 11, 12)
|
||||
shape_4d = (10, 11, 12, n_volumes)
|
||||
affine = np.eye(4)
|
||||
|
||||
img_4d, mask_img = data_gen.generate_random_img(
|
||||
shape_4d,
|
||||
affine=affine,
|
||||
)
|
||||
img_3d, _ = data_gen.generate_random_img(shape_3d, affine=affine)
|
||||
|
||||
masker = JuniferNiftiSpheresMasker(
|
||||
seeds=[(1, 1, 1), (4, 4, 4)], # number of tuples equal to n_regions
|
||||
radius=1,
|
||||
mask_img=mask_img,
|
||||
)
|
||||
masker.fit()
|
||||
|
||||
# DeprecationWarning *should* be raised for 3D inputs
|
||||
with pytest.warns(DeprecationWarning, match="Starting in version 0.12"):
|
||||
test_data = masker.transform(img_3d)
|
||||
assert test_data.shape == (1, n_regions)
|
||||
|
||||
# DeprecationWarning should *not* be raised for 4D inputs
|
||||
with warnings.catch_warnings():
|
||||
warnings.filterwarnings(
|
||||
"error",
|
||||
message="Starting in version 0.12",
|
||||
category=DeprecationWarning,
|
||||
)
|
||||
test_data = masker.transform(img_4d)
|
||||
assert test_data.shape == (n_volumes, n_regions)
|
||||
|
|
@ -6,11 +6,11 @@
|
|||
|
||||
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
|
||||
|
||||
from nilearn.maskers import NiftiSpheresMasker
|
||||
|
||||
from ..api.decorators import register_marker
|
||||
from ..data import load_coordinates
|
||||
from ..utils import logger, raise_error
|
||||
from ..external.nilearn import JuniferNiftiSpheresMasker
|
||||
from ..stats import get_aggfunc_by_name
|
||||
from ..utils import logger
|
||||
from .base import BaseMarker
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
|
@ -27,21 +27,24 @@ class SphereAggregation(BaseMarker):
|
|||
The name of the coordinates list to use. See
|
||||
:func:`junifer.data.coordinates.list_coordinates` for options.
|
||||
radius : float, optional
|
||||
The radius of the sphere in mm. If None, the signal will be extracted
|
||||
from a single voxel. See :class:`nilearn.maskers.NiftiSpheresMasker`
|
||||
for more information (default None).
|
||||
The radius of the sphere in millimeters. If None, the signal will be
|
||||
extracted from a single voxel. See
|
||||
:class:`nilearn.maskers.NiftiSpheresMasker` for more information
|
||||
(default None).
|
||||
method : str, optional
|
||||
The aggregation method to use.
|
||||
See :func:`junifer.stats.get_aggfunc_by_name` for more information
|
||||
(default "mean").
|
||||
method_params : dict, optional
|
||||
The parameters to pass to the aggregation method (default None).
|
||||
on : list of str, optional
|
||||
The kind of data to apply the marker to. By default, will work on all
|
||||
on : {"T1w", "BOLD", "VBM_GM", "VBM_WM", "fALFF", "GCOR", "LCOR"} or \
|
||||
list of the options, optional
|
||||
The data types to apply the marker to. If None, will work on all
|
||||
available data (default None).
|
||||
name : str, optional
|
||||
The name of the marker. By default, it will use KIND_SphereAggregation
|
||||
where KIND is the kind of data it was applied to (default None).
|
||||
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
|
|
@ -49,21 +52,12 @@ class SphereAggregation(BaseMarker):
|
|||
coords: str,
|
||||
radius: Optional[float] = None,
|
||||
method: str = "mean",
|
||||
method_params: Optional[Dict] = None,
|
||||
on: Optional[Union[List[str], str]] = None,
|
||||
method_params: Optional[Dict[str, Any]] = None,
|
||||
on: Union[List[str], str, None] = None,
|
||||
name: Optional[str] = None,
|
||||
) -> None:
|
||||
self.coords = coords
|
||||
self.radius = radius
|
||||
|
||||
if method != "mean":
|
||||
raise_error(
|
||||
"Only mean aggregation is supported for sphere aggregation. "
|
||||
"If you need other aggregation methods, please open an issue "
|
||||
"on `junifer github`_.",
|
||||
NotImplementedError,
|
||||
)
|
||||
|
||||
self.method = method
|
||||
self.method_params = {} if method_params is None else method_params
|
||||
super().__init__(on=on, name=name)
|
||||
|
|
@ -90,7 +84,7 @@ class SphereAggregation(BaseMarker):
|
|||
Returns
|
||||
-------
|
||||
list of str
|
||||
The kind of output.
|
||||
The list of storage kinds.
|
||||
|
||||
"""
|
||||
outputs = []
|
||||
|
|
@ -159,17 +153,21 @@ class SphereAggregation(BaseMarker):
|
|||
"""
|
||||
t_input = input["data"]
|
||||
logger.debug(f"Sphere aggregation using {self.method}")
|
||||
# agg_func = get_aggfunc_by_name(
|
||||
# self.method, func_params=self.method_params
|
||||
# )
|
||||
coords, out_labels = load_coordinates(self.coords)
|
||||
masker = NiftiSpheresMasker(
|
||||
# Get aggregation function
|
||||
agg_func = get_aggfunc_by_name(
|
||||
self.method, func_params=self.method_params
|
||||
)
|
||||
# Get seeds and labels
|
||||
coords, out_labels = load_coordinates(name=self.coords)
|
||||
masker = JuniferNiftiSpheresMasker(
|
||||
seeds=coords,
|
||||
radius=self.radius,
|
||||
mask_img=None, # TODO: support this (needs #79)
|
||||
agg_func=agg_func,
|
||||
)
|
||||
|
||||
# Fit and transform the marker on the data
|
||||
out_values = masker.fit_transform(t_input)
|
||||
# Format the output
|
||||
out = {"data": out_values, "columns": out_labels}
|
||||
if out_values.shape[0] > 1:
|
||||
out["row_names"] = "scan"
|
||||
|
|
|
|||
|
|
@ -16,11 +16,15 @@ from junifer.data import load_coordinates
|
|||
from junifer.markers.sphere_aggregation import SphereAggregation
|
||||
from junifer.storage import SQLiteFeatureStorage
|
||||
|
||||
# Define common variables
|
||||
COORDS = "DMNBuckner"
|
||||
RADIUS = 8
|
||||
|
||||
|
||||
def test_SphereAggregation_input_output() -> None:
|
||||
"""Test SphereAggregation input and output types."""
|
||||
marker = SphereAggregation(
|
||||
coords="DMNBuckner", method="mean", radius=8, on="VBM_GM"
|
||||
coords=COORDS, method="mean", radius=RADIUS, on="VBM_GM"
|
||||
)
|
||||
|
||||
output = marker.get_output_kind(["VBM_GM", "BOLD"])
|
||||
|
|
@ -33,9 +37,7 @@ def test_SphereAggregation_input_output() -> None:
|
|||
def test_SphereAggregation_3D() -> None:
|
||||
"""Test SphereAggregation object on 3D images."""
|
||||
# Get the testing coordinates (for nilearn)
|
||||
coord_names = "DMNBuckner"
|
||||
radius = 8
|
||||
coordinates, labels = load_coordinates(coord_names)
|
||||
coordinates, labels = load_coordinates(COORDS)
|
||||
|
||||
# Get the oasis VBM data
|
||||
oasis_dataset = datasets.fetch_oasis_vbm(n_subjects=1)
|
||||
|
|
@ -43,12 +45,12 @@ def test_SphereAggregation_3D() -> None:
|
|||
img = nib.load(vbm)
|
||||
|
||||
# Create NiftiLabelsMasker
|
||||
nifti_masker = NiftiSpheresMasker(seeds=coordinates, radius=radius)
|
||||
nifti_masker = NiftiSpheresMasker(seeds=coordinates, radius=RADIUS)
|
||||
auto4d = nifti_masker.fit_transform(img)
|
||||
|
||||
# Create SphereAggregation object
|
||||
marker = SphereAggregation(
|
||||
coords=coord_names, method="mean", radius=radius, on="VBM_GM"
|
||||
coords=COORDS, method="mean", radius=RADIUS, on="VBM_GM"
|
||||
)
|
||||
input = {"VBM_GM": {"data": img}}
|
||||
jun_values4d = marker.fit_transform(input)["VBM_GM"]["data"]
|
||||
|
|
@ -59,37 +61,30 @@ def test_SphereAggregation_3D() -> None:
|
|||
|
||||
meta = marker.get_meta("VBM_GM")["marker"]
|
||||
assert meta["method"] == "mean"
|
||||
assert meta["coords"] == coord_names
|
||||
assert meta["radius"] == radius
|
||||
assert meta["coords"] == COORDS
|
||||
assert meta["radius"] == RADIUS
|
||||
assert meta["name"] == "VBM_GM_SphereAggregation"
|
||||
assert meta["class"] == "SphereAggregation"
|
||||
assert meta["kind"] == "VBM_GM"
|
||||
assert meta["method_params"] == {}
|
||||
|
||||
with pytest.raises(NotImplementedError, match="mean aggregation"):
|
||||
marker = SphereAggregation(
|
||||
coords=coord_names, method="std", radius=radius, on="BOLD"
|
||||
)
|
||||
|
||||
|
||||
def test_SphereAggregation_4D() -> None:
|
||||
"""Test SphereAggregation object on 4D images."""
|
||||
# Get the testing coordinates (for nilearn)
|
||||
coord_names = "DMNBuckner"
|
||||
radius = 8
|
||||
coordinates, labels = load_coordinates(coord_names)
|
||||
coordinates, labels = load_coordinates(COORDS)
|
||||
|
||||
# Get the SPM auditory data:
|
||||
# Get the SPM auditory data
|
||||
subject_data = datasets.fetch_spm_auditory()
|
||||
fmri_img = concat_imgs(subject_data.func) # type: ignore
|
||||
|
||||
# Create NiftiLabelsMasker
|
||||
nifti_masker = NiftiSpheresMasker(seeds=coordinates, radius=radius)
|
||||
nifti_masker = NiftiSpheresMasker(seeds=coordinates, radius=RADIUS)
|
||||
auto4d = nifti_masker.fit_transform(fmri_img)
|
||||
|
||||
# Create SphereAggregation object
|
||||
marker = SphereAggregation(
|
||||
coords=coord_names, method="mean", radius=radius
|
||||
coords=COORDS, method="mean", radius=RADIUS
|
||||
)
|
||||
input = {"BOLD": {"data": fmri_img}}
|
||||
jun_values4d = marker.fit_transform(input)["BOLD"]["data"]
|
||||
|
|
@ -100,8 +95,8 @@ def test_SphereAggregation_4D() -> None:
|
|||
|
||||
meta = marker.get_meta("BOLD")["marker"]
|
||||
assert meta["method"] == "mean"
|
||||
assert meta["coords"] == coord_names
|
||||
assert meta["radius"] == radius
|
||||
assert meta["coords"] == COORDS
|
||||
assert meta["radius"] == RADIUS
|
||||
assert meta["name"] == "BOLD_SphereAggregation"
|
||||
assert meta["class"] == "SphereAggregation"
|
||||
assert meta["kind"] == "BOLD"
|
||||
|
|
@ -133,7 +128,7 @@ def test_SphereAggregation_storage(tmp_path: Path) -> None:
|
|||
}
|
||||
input = {"VBM_GM": {"data": img}, "meta": meta}
|
||||
marker = SphereAggregation(
|
||||
coords="DMNBuckner", method="mean", radius=8, on="VBM_GM"
|
||||
coords=COORDS, method="mean", radius=RADIUS, on="VBM_GM"
|
||||
)
|
||||
|
||||
marker.fit_transform(input, storage=storage)
|
||||
|
|
@ -143,12 +138,12 @@ def test_SphereAggregation_storage(tmp_path: Path) -> None:
|
|||
"version": "0.0.1",
|
||||
"marker": {"name": "BOLD_fcname"},
|
||||
}
|
||||
# Get the SPM auditory data:
|
||||
# Get the SPM auditory data
|
||||
subject_data = datasets.fetch_spm_auditory()
|
||||
fmri_img = concat_imgs(subject_data.func) # type: ignore
|
||||
input = {"BOLD": {"data": fmri_img}, "meta": meta}
|
||||
marker = SphereAggregation(
|
||||
coords="DMNBuckner", method="mean", radius=8, on="BOLD"
|
||||
coords=COORDS, method="mean", radius=RADIUS, on="BOLD"
|
||||
)
|
||||
|
||||
marker.fit_transform(input, storage=storage)
|
||||
|
|
|
|||
Is this code from Nilearn? If so, can you adjust the license/info on this file to reflect that?