From 3db1f0eaf491e5f2ec9cfdc005d04648bb4e90f9 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Fri, 9 Feb 2024 10:39:04 +0100 Subject: [PATCH 01/10] update: add templateflow as dependency --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index bf82f834d..83eb025d1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,6 +48,7 @@ dependencies = [ "h5py>=3.8.0,<3.10", "httpx[http2]==0.26.0", "tqdm==4.66.1", + "templateflow>=23.0.0", ] dynamic = ["version"] -- 2.52.0 From 8528fb15747a23cd67bdfbb6e9237313cc567a3c Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Wed, 31 Jan 2024 09:52:24 +0100 Subject: [PATCH 02/10] feat: add get_template to get tailored templates from templateflow --- junifer/data/__init__.py | 2 +- junifer/data/template_spaces.py | 64 ++++++++++++++++++++++++++++++++- 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/junifer/data/__init__.py b/junifer/data/__init__.py index 263fbedc1..663ed1dbe 100644 --- a/junifer/data/__init__.py +++ b/junifer/data/__init__.py @@ -25,6 +25,6 @@ from .masks import ( get_mask, ) -from .template_spaces import get_xfm +from .template_spaces import get_template, get_xfm from . import utils diff --git a/junifer/data/template_spaces.py b/junifer/data/template_spaces.py index 4f10bb5ad..663a4f82a 100644 --- a/junifer/data/template_spaces.py +++ b/junifer/data/template_spaces.py @@ -4,9 +4,12 @@ # License: AGPL from pathlib import Path -from typing import Union +from typing import Any, Dict, Optional, Union import httpx +import nibabel as nib +import numpy as np +from templateflow import api as tflow from ..utils import logger, raise_error @@ -89,3 +92,62 @@ def get_xfm( f.write(chunk) return xfm_file_path + + +def get_template( + space: str, + target_data: Dict[str, Any], + extra_input: Optional[Dict[str, Any]] = None, +) -> nib.Nifti1Image: + """Get template for the space, tailored for the target image. + + Parameters + ---------- + space : str + The name of the template space. + target_data : dict + The corresponding item of the data object for which the template space + will be loaded. + extra_input : dict, optional + The other fields in the data object. Useful for accessing other data + types (default None). + + Returns + ------- + Nifti1Image + The template image. + + Raises + ------ + ValueError + If ``space`` is invalid. + RuntimeError + If template in the required resolution is not found. + + """ + # Check for invalid space; early check to raise proper error + if space not in tflow.templates(): + raise_error(f"Unknown template space: {space}") + + # 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]).astype(int) + + # Retrieve template + try: + template_path = tflow.get( + space, + raise_empty=True, + resolution=resolution, + suffix="T1w", + desc=None, + extension="nii.gz", + ) + except Exception: # noqa: BLE001 + raise_error( + f"Template {space} not found in the required resolution " + f"{resolution}", + klass=RuntimeError, + ) + else: + return nib.load(template_path) # type: ignore -- 2.52.0 From e645ff8daad4004692580c54023897457f4a7f02 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Wed, 31 Jan 2024 09:52:56 +0100 Subject: [PATCH 03/10] update: add tests for get_template --- junifer/data/tests/test_template_spaces.py | 44 +++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/junifer/data/tests/test_template_spaces.py b/junifer/data/tests/test_template_spaces.py index d61e36696..c44d4a24e 100644 --- a/junifer/data/tests/test_template_spaces.py +++ b/junifer/data/tests/test_template_spaces.py @@ -6,9 +6,12 @@ import socket from pathlib import Path +import nibabel as nib import pytest -from junifer.data import get_xfm +from junifer.data import get_template, get_xfm +from junifer.datareader import DefaultDataReader +from junifer.testing.datagrabbers import OasisVBMTestingDataGrabber @pytest.mark.skipif( @@ -28,3 +31,42 @@ def test_get_xfm(tmp_path: Path) -> None: src="MNI152NLin6Asym", dst="MNI152NLin2009cAsym", xfms_dir=tmp_path ) assert isinstance(xfm_path, Path) + + +def test_get_template() -> None: + """Test tailored template image fetch.""" + with OasisVBMTestingDataGrabber() as dg: + element = dg["sub-01"] + element_data = DefaultDataReader().fit_transform(element) + vbm_gm = element_data["VBM_GM"] + # Get tailored parcellation + tailored_template = get_template( + space=vbm_gm["space"], target_data=vbm_gm + ) + assert isinstance(tailored_template, nib.Nifti1Image) + + +def test_get_template_invalid_space() -> None: + """Test invalid space check for template fetch.""" + with OasisVBMTestingDataGrabber() as dg: + element = dg["sub-01"] + element_data = DefaultDataReader().fit_transform(element) + vbm_gm = element_data["VBM_GM"] + # Get tailored parcellation + with pytest.raises(ValueError, match="Unknown template space:"): + _ = get_template(space="andromeda", target_data=vbm_gm) + + +def test_get_template_invalid_resolution() -> None: + """Test invalid resolution check for template fetch.""" + with OasisVBMTestingDataGrabber() as dg: + element = dg["sub-01"] + element_data = DefaultDataReader().fit_transform(element) + vbm_gm = element_data["VBM_GM"] + # Change header resolution to raise error + element_data["VBM_GM"]["data"].header.set_zooms((3, 3, 3)) + # Get tailored parcellation + with pytest.raises( + RuntimeError, match="not found in the required resolution" + ): + _ = get_template(space=vbm_gm["space"], target_data=vbm_gm) -- 2.52.0 From 32d3153d98c0b8d43fb7280f3666bdbb73214506 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Fri, 9 Feb 2024 10:39:43 +0100 Subject: [PATCH 04/10] chore: update tests for api/utils.py --- junifer/api/tests/test_api_utils.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/junifer/api/tests/test_api_utils.py b/junifer/api/tests/test_api_utils.py index a2f68a41e..9bb54cd25 100644 --- a/junifer/api/tests/test_api_utils.py +++ b/junifer/api/tests/test_api_utils.py @@ -44,6 +44,7 @@ def test_get_dependency_information_short() -> None: "ruamel.yaml", "httpx", "tqdm", + "templateflow", ] if int(pl.python_version_tuple()[1]) < 10: dependency_list.append("importlib_metadata") @@ -68,6 +69,7 @@ def test_get_dependency_information_long() -> None: "ruamel.yaml", "httpx", "tqdm", + "templateflow", ] for key in dependency_list: assert key in dependency_information_keys -- 2.52.0 From 399f9e849999d994bce972d30930cb0e24984bda Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Fri, 9 Feb 2024 10:40:18 +0100 Subject: [PATCH 05/10] chore: update isort config for ruff in pyproject.toml --- pyproject.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 83eb025d1..37e8e864e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -189,8 +189,10 @@ known-third-party =[ "nilearn", "sqlalchemy", "yaml", + "importlib_metadata", "httpx", "tqdm", + "templateflow", "bct", "neurokit2", "pytest", -- 2.52.0 From a3af7da6ac4e6a0981acacdc979d65a4b6ded18e Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Wed, 31 Jan 2024 11:17:14 +0100 Subject: [PATCH 06/10] chore: add changelog 298.feature --- docs/changes/newsfragments/298.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 docs/changes/newsfragments/298.feature diff --git a/docs/changes/newsfragments/298.feature b/docs/changes/newsfragments/298.feature new file mode 100644 index 000000000..1b62ae002 --- /dev/null +++ b/docs/changes/newsfragments/298.feature @@ -0,0 +1 @@ +Introduce :func:`.get_template` to fetch template space image tailored to a target data by `Synchon Mandal`_ -- 2.52.0 From e829bd73fb02e605e3ae73db6f1b6522c4590dc7 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Fri, 2 Feb 2024 06:14:07 +0100 Subject: [PATCH 07/10] chore: improve logging for get_template() --- junifer/data/template_spaces.py | 1 + 1 file changed, 1 insertion(+) diff --git a/junifer/data/template_spaces.py b/junifer/data/template_spaces.py index 663a4f82a..424ec79b3 100644 --- a/junifer/data/template_spaces.py +++ b/junifer/data/template_spaces.py @@ -133,6 +133,7 @@ def get_template( target_img = target_data["data"] resolution = np.min(target_img.header.get_zooms()[:3]).astype(int) + logger.info(f"Downloading template {space} in resolution {resolution}") # Retrieve template try: template_path = tflow.get( -- 2.52.0 From a3cb71b4e3dafae0756ae38c6e50b35aec7beea4 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Fri, 9 Feb 2024 10:32:13 +0100 Subject: [PATCH 08/10] update: use closest_resolution() in get_template() --- junifer/data/template_spaces.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/junifer/data/template_spaces.py b/junifer/data/template_spaces.py index 424ec79b3..fb8b18122 100644 --- a/junifer/data/template_spaces.py +++ b/junifer/data/template_spaces.py @@ -12,6 +12,7 @@ import numpy as np from templateflow import api as tflow from ..utils import logger, raise_error +from .utils import closest_resolution def get_xfm( @@ -133,6 +134,14 @@ def get_template( target_img = target_data["data"] resolution = np.min(target_img.header.get_zooms()[:3]).astype(int) + # Fetch available resolutions for the template + available_resolutions = [ + int(min(val["zooms"])) + for val in tflow.get_metadata(space)["res"].values() + ] + # Use the closest resolution if desired resolution is not found + resolution = closest_resolution(resolution, available_resolutions) + logger.info(f"Downloading template {space} in resolution {resolution}") # Retrieve template try: -- 2.52.0 From 03d2ac4788f0039fbbd42249b81d23493769cbe6 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Fri, 9 Feb 2024 10:32:40 +0100 Subject: [PATCH 09/10] chore: update type annotation and docstring for closest_resolution() --- junifer/data/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/junifer/data/utils.py b/junifer/data/utils.py index 99306b3c1..291425c18 100644 --- a/junifer/data/utils.py +++ b/junifer/data/utils.py @@ -8,14 +8,14 @@ from ..utils.logging import logger def closest_resolution( - resolution: Optional[float], + resolution: Optional[Union[float, int]], valid_resolution: Union[List[float], List[int], np.ndarray], ) -> Union[float, int]: """Find the closest resolution. Parameters ---------- - resolution : float, optional + resolution : float or int, optional The given resolution. If None, will return the highest resolution (default None). valid_resolution : list of float or int, or np.ndarray -- 2.52.0 From 1d5d635cf4cf4dc91d761e5ebe8d8649cc94e2c1 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Fri, 9 Feb 2024 11:06:04 +0100 Subject: [PATCH 10/10] update: improve tests for get_template() --- junifer/data/tests/test_template_spaces.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/junifer/data/tests/test_template_spaces.py b/junifer/data/tests/test_template_spaces.py index c44d4a24e..cae6cf1be 100644 --- a/junifer/data/tests/test_template_spaces.py +++ b/junifer/data/tests/test_template_spaces.py @@ -57,16 +57,13 @@ def test_get_template_invalid_space() -> None: _ = get_template(space="andromeda", target_data=vbm_gm) -def test_get_template_invalid_resolution() -> None: - """Test invalid resolution check for template fetch.""" +def test_get_template_closest_resolution() -> None: + """Test closest resolution check for template fetch.""" with OasisVBMTestingDataGrabber() as dg: element = dg["sub-01"] element_data = DefaultDataReader().fit_transform(element) vbm_gm = element_data["VBM_GM"] - # Change header resolution to raise error + # Change header resolution to fetch closest resolution element_data["VBM_GM"]["data"].header.set_zooms((3, 3, 3)) - # Get tailored parcellation - with pytest.raises( - RuntimeError, match="not found in the required resolution" - ): - _ = get_template(space=vbm_gm["space"], target_data=vbm_gm) + template = get_template(space=vbm_gm["space"], target_data=vbm_gm) + assert isinstance(template, nib.Nifti1Image) -- 2.52.0