diff --git a/docs/builtin.rst b/docs/builtin.rst index 2388c997c..4e3fbd097 100644 --- a/docs/builtin.rst +++ b/docs/builtin.rst @@ -365,6 +365,16 @@ Available | resting-state functional connectivity. | NeuroImage, Volume 273 (2023). | https://doi.org/10.1016/j.neuroimage.2023.120010 + * - Brainnetome + - ``threshold`` + - ``Brainnetome_thr0``, ``Brainnetome_thr25``, ``Brainnetome_thr50`` + - ``MNI152NLin6Asym`` + - 0.0.4 + - | Fan, L., Li, H., Zhuo, J., et al. + | The Human Brainnetome Atlas: A New Brain Atlas Based on Connectional + | Architecture + | Cerebral Cortex, Volume 26(8), Pages 3508–3526 (2016). + | https://doi.org/10.1093/cercor/bhw157 Planned diff --git a/docs/changes/newsfragments/275.feature b/docs/changes/newsfragments/275.feature new file mode 100644 index 000000000..38faed47c --- /dev/null +++ b/docs/changes/newsfragments/275.feature @@ -0,0 +1 @@ +Add ``Brainnetome 246`` parcellation to ``junifer.data`` by `Synchon Mandal`_ diff --git a/junifer/data/parcellations.py b/junifer/data/parcellations.py index 02f01d238..9c2b5d62b 100644 --- a/junifer/data/parcellations.py +++ b/junifer/data/parcellations.py @@ -127,6 +127,13 @@ for n_rois in range(100, 1001, 100): "kong_networks": 17, "space": "MNI152NLin6Asym", } +# Add Brainnetome parcellation info +for threshold in [0, 25, 50]: + _available_parcellations[f"Brainnetome_thr{threshold}"] = { + "family": "Brainnetome", + "threshold": threshold, + "space": "MNI152NLin6Asym", + } def register_parcellation( @@ -523,6 +530,9 @@ def _retrieve_parcellation( Number of Yeo networks to use (default None). ``kong_networks`` : {17}, optional Number of Kong networks to use (default None). + * Brainnetome : + ``threshold`` : {0, 25, 50} + Threshold for the probabilistic maps of subregion. Returns ------- @@ -586,6 +596,12 @@ def _retrieve_parcellation( resolution=resolution, **kwargs, ) + elif family == "Brainnetome": + parcellation_fname, parcellation_labels = _retrieve_brainnetome( + parcellations_dir=parcellations_dir, + resolution=resolution, + **kwargs, + ) else: raise_error( f"The provided parcellation name {family} cannot be retrieved." @@ -1555,6 +1571,122 @@ def _retrieve_yan( return parcellation_fname, labels +def _retrieve_brainnetome( + parcellations_dir: Path, + resolution: Optional[float] = None, + threshold: Optional[int] = None, +) -> Tuple[Path, List[str]]: + """Retrieve Brainnetome parcellation. + + Parameters + ---------- + parcellations_dir : pathlib.Path + The path to the parcellation data directory. + resolution : {1.0, 1.25, 2.0}, optional + The desired resolution of the parcellation to load. If it is not + available, the closest resolution will be loaded. Preferably, use a + resolution higher than the desired one. By default, will load the + highest one (default None). Available resolutions for this + parcellation are 1mm, 1.25mm and 2mm. + threshold : {0, 25, 50}, optional + The threshold for the probabilistic maps of subregion (default None). + + Returns + ------- + pathlib.Path + File path to the parcellation image. + list of str + Parcellation labels. + + Raises + ------ + RuntimeError + If there is a problem fetching files. + ValueError + If invalid value is provided for ``threshold``. + + """ + logger.info("Parcellation parameters:") + logger.info(f"\tresolution: {resolution}") + logger.info(f"\tthreshold: {threshold}") + + # Check resolution + _valid_resolutions = [1.0, 1.25, 2.0] + resolution = closest_resolution(resolution, _valid_resolutions) + + # Check threshold value + _valid_threshold = [0, 25, 50] + if threshold not in _valid_threshold: + raise_error( + f"The parameter `threshold` ({threshold}) needs to be one of the " + f"following: {_valid_threshold}" + ) + # Correct resolution for further stuff + if resolution in [1.0, 2.0]: + resolution = int(resolution) + + parcellation_fname = ( + parcellations_dir + / "BNA246" + / f"BNA-maxprob-thr{threshold}-{resolution}mm.nii.gz" + ) + + # Check for existence of parcellation + if not parcellation_fname.exists(): + # Set URL + url = f"http://neurovault.org/media/images/1625/BNA-maxprob-thr{threshold}-{resolution}mm.nii.gz" + + logger.info(f"Downloading Brainnetome from {url}") + # Make HTTP request + try: + resp = httpx.get(url, follow_redirects=True) + resp.raise_for_status() + except httpx.HTTPError as exc: + raise_error( + f"Error response {exc.response.status_code} while " + f"requesting {exc.request.url!r}", + klass=RuntimeError, + ) + else: + # Create local directory if not present + parcellation_fname.parent.mkdir(parents=True, exist_ok=True) + # Create file if not present + parcellation_fname.touch(exist_ok=True) + # Open file and write bytes + parcellation_fname.write_bytes(resp.content) + + # Load labels + labels = ( + sorted([f"SFG_L(R)_7_{i}" for i in range(1, 8)] * 2) + + sorted([f"MFG_L(R)_7_{i}" for i in range(1, 8)] * 2) + + sorted([f"IFG_L(R)_6_{i}" for i in range(1, 7)] * 2) + + sorted([f"OrG_L(R)_6_{i}" for i in range(1, 7)] * 2) + + sorted([f"PrG_L(R)_6_{i}" for i in range(1, 7)] * 2) + + sorted([f"PCL_L(R)_2_{i}" for i in range(1, 3)] * 2) + + sorted([f"STG_L(R)_6_{i}" for i in range(1, 7)] * 2) + + sorted([f"MTG_L(R)_4_{i}" for i in range(1, 5)] * 2) + + sorted([f"ITG_L(R)_7_{i}" for i in range(1, 8)] * 2) + + sorted([f"FuG_L(R)_3_{i}" for i in range(1, 4)] * 2) + + sorted([f"PhG_L(R)_6_{i}" for i in range(1, 7)] * 2) + + sorted([f"pSTS_L(R)_2_{i}" for i in range(1, 3)] * 2) + + sorted([f"SPL_L(R)_5_{i}" for i in range(1, 6)] * 2) + + sorted([f"IPL_L(R)_6_{i}" for i in range(1, 7)] * 2) + + sorted([f"PCun_L(R)_4_{i}" for i in range(1, 5)] * 2) + + sorted([f"PoG_L(R)_4_{i}" for i in range(1, 5)] * 2) + + sorted([f"INS_L(R)_6_{i}" for i in range(1, 7)] * 2) + + sorted([f"CG_L(R)_7_{i}" for i in range(1, 8)] * 2) + + sorted([f"MVOcC _L(R)_5_{i}" for i in range(1, 6)] * 2) + + sorted([f"LOcC_L(R)_4_{i}" for i in range(1, 5)] * 2) + + sorted([f"LOcC_L(R)_2_{i}" for i in range(1, 3)] * 2) + + sorted([f"Amyg_L(R)_2_{i}" for i in range(1, 3)] * 2) + + sorted([f"Hipp_L(R)_2_{i}" for i in range(1, 3)] * 2) + + sorted([f"BG_L(R)_6_{i}" for i in range(1, 7)] * 2) + + sorted([f"Tha_L(R)_8_{i}" for i in range(1, 9)] * 2) + ) + + return parcellation_fname, labels + + def merge_parcellations( parcellations_list: List["Nifti1Image"], parcellations_names: List[str], diff --git a/junifer/data/tests/test_parcellations.py b/junifer/data/tests/test_parcellations.py index 044dc2023..a19cac8f6 100644 --- a/junifer/data/tests/test_parcellations.py +++ b/junifer/data/tests/test_parcellations.py @@ -16,6 +16,7 @@ from numpy.testing import assert_array_almost_equal, assert_array_equal from junifer.data.parcellations import ( _retrieve_aicha, + _retrieve_brainnetome, _retrieve_parcellation, _retrieve_schaefer, _retrieve_shen, @@ -955,6 +956,77 @@ def test_retrieve_yan_incorrect_kong_networks(tmp_path: Path) -> None: ) +@pytest.mark.parametrize( + "resolution, threshold", + [ + (1.0, 0), + (1.0, 25), + (1.0, 50), + (1.25, 0), + (1.25, 25), + (1.25, 50), + (2, 0), + (2, 25), + (2, 50), + ], +) +def test_brainnetome( + tmp_path: Path, + resolution: float, + threshold: int, +) -> None: + """Test Brainnetome parcellation. + + Parameters + ---------- + tmp_path : pathlib.Path + The path to the test directory. + resolution : float + The parametrized resolution values. + threshold : int + The parametrized threshold values. + + """ + parcellations = list_parcellations() + parcellation_name = f"Brainnetome_thr{threshold}" + assert parcellation_name in parcellations + + # Fix resolution + if resolution in [1.0, 2.0]: + resolution = int(resolution) + + parcellation_file = f"BNA-maxprob-thr{threshold}-{resolution}mm.nii.gz" + # Load parcellation + img, label, img_path, space = load_parcellation( + name=parcellation_name, + parcellations_dir=tmp_path, + resolution=resolution, + ) + assert img is not None + assert img_path.name == parcellation_file + assert space == "MNI152NLin6Asym" + assert len(label) == 246 + assert_array_equal( + img.header["pixdim"][1:4], 3 * [resolution] # type: ignore + ) + + +def test_retrieve_brainnetome_incorrect_threshold(tmp_path: Path) -> None: + """Test retrieve Brainnetome with incorrect threshold. + + Parameters + ---------- + tmp_path : pathlib.Path + The path to the test directory. + + """ + with pytest.raises(ValueError, match="The parameter `threshold`"): + _retrieve_brainnetome( + parcellations_dir=tmp_path, + threshold=100, + ) + + def test_merge_parcellations() -> None: """Test merging parcellations.""" # load some parcellations for testing