Skip to content

Commit

Permalink
[ENH] Continue dev for v1.1.0
Browse files Browse the repository at this point in the history
  • Loading branch information
tsbinns committed Dec 15, 2023
1 parent feef1d6 commit f60bdfd
Show file tree
Hide file tree
Showing 10 changed files with 155 additions and 58 deletions.
4 changes: 4 additions & 0 deletions .flake8
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[flake8]
per-file-ignores =
__init__.py: F401,
*.py: E203, E741
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
# mni_to_atlas
# mni-to-atlas
A simple Python-based tool for finding brain atlas regions based on MNI coordinates, with basic plotting abilities to show the sagittal, coronal, and axial views of the coordinates on the atlas.

The following atlases are currently supported:
- Automated anatomical labelling atlas [[1]](#References)
- Automated anatomical labelling 3 atlas (1mm<sup>3</sup> voxel version) [[2]](#References)
- Human Connectome Project extended parcellation atlas [[3]](#References)

If there is an atlas you would like to see added, please open an [issue](https://github.com/tsbinns/mni_to_atlas/issues).

Example screenshot of the plotting:
![image](https://user-images.githubusercontent.com/56922019/178039475-998e077b-482f-4fbe-94af-88e1891b493b.png)
![image](docs/_static/example_plot.png)

## Requirements:
[See here for the list of requirements](requirements.txt).
Expand Down
12 changes: 12 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# mni-to-atlas changelog

## [Version 1.1.0](https://github.com/tsbinns/mni_to_atlas/tree/1.1.0)

##### Enhancements
- Added support for the Human Connectome Project extended parcellation atlas.
- Vectorised computations to improve performance.


## [Version 1.0.0](https://github.com/tsbinns/mni_to_atlas/tree/1.0.0)

- Initial release.
Binary file added docs/_static/example_plot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
31 changes: 27 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "mni_to_atlas"
name = "mni-to-atlas"
version = "1.1.0"
authors = [
{ name="Thomas Samuel Binns", email="t.s.binns@outlook.com" },
]
description = "A simple Python-based tool for finding brain atlas regions based on MNI coordinates."
readme = "README.md"
requires-python = ">=3.11"
requires-python = ">=3.10"
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
Expand All @@ -24,8 +24,31 @@ dependencies = [
]

[project.optional-dependencies]
dev = ["pytest"]
dev = [
"pytest",
"coverage",
"flake8",
"black",
"codespell",
"pycodestyle",
"pydocstyle"
]

[project.urls]
"Homepage" = "https://github.com/tsbinns/mni_to_atlas"
"Bug Tracker" = "https://github.com/tsbinns/mni_to_atlas/issues"
"Bug Tracker" = "https://github.com/tsbinns/mni_to_atlas/issues"

[tool.pytest.ini_options]
filterwarnings = [
# Ignore warnings about matplotlib figures not being shown
'ignore:FigureCanvasAgg is non-interactive, and thus cannot be shown:UserWarning',
]

[tool.coverage.run]
omit = [
"tests/*",
"__init__.py"
]

[tool.pydocstyle]
match-dir = "^(?!(tests)).*"
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
python>=3.11
python>=3.10
numpy>=1.22
matplotlib>=3.5
nibabel>=3.2
2 changes: 2 additions & 0 deletions src/mni_to_atlas/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"""Initialisation of the mni-to-atlas package."""

__version__ = "1.1.0"

from .atlas_browser import AtlasBrowser
10 changes: 5 additions & 5 deletions src/mni_to_atlas/atlas_browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from mni_to_atlas.atlases import _ATLASES_PATH, _SUPPORTED_ATLASES


class AtlasBrowser:
class AtlasBrowser: # noqa: D414
"""Class for converting MNI coordinates to brain atlas regions.
Parameters
Expand Down Expand Up @@ -41,15 +41,14 @@ class AtlasBrowser:
_plotting_image: np.ndarray = None
_affine: np.ndarray = None
_region_names: dict = None
_region_colours: dict = None

def __init__(self, atlas: str) -> None:
def __init__(self, atlas: str) -> None: # noqa: D107
self.atlas_name = atlas

self._check_atlas()
self._load_data()

def _check_atlas(self) -> None: # noqa: D107
def _check_atlas(self) -> None:
"""Check that the requested atlas is supported."""
if self.atlas_name not in _SUPPORTED_ATLASES:
raise ValueError(
Expand Down Expand Up @@ -146,7 +145,7 @@ def _sort_coordinates(self, coordinates: np.ndarray) -> np.ndarray:

if coordinates.shape[1] != 3:
raise ValueError(
"'coordinates' should have shape (n, 3), but it has shape (n, "
"'coordinates' must have shape (n, 3), but it has shape (n, "
f"{coordinates.shape[1]})."
)

Expand Down Expand Up @@ -267,6 +266,7 @@ def _convert_mni_to_atlas_space(
extended_mni_coords = np.hstack(
(mni_coords, np.ones((mni_coords.shape[0], 1), dtype=np.int32))
)
# np.linalg.solve faster than taking inverse of affine and multiplying
atlas_coords = np.linalg.solve(self._affine, extended_mni_coords.T)

return atlas_coords[:3, :].astype(np.int32).T
Expand Down
2 changes: 2 additions & 0 deletions src/mni_to_atlas/atlases/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"""Initialisation of the atlases."""

__version__ = "1.1.0"

_ATLASES_PATH = __path__[0]
Expand Down
144 changes: 98 additions & 46 deletions tests/atlas_browser_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,52 +2,104 @@

import pytest
import numpy as np
import matplotlib.pyplot as plt

from mni_to_atlas import AtlasBrowser
from mni_to_atlas.atlases import _SUPPORTED_ATLASES

# Do not show plots when testing
plt.switch_backend("Agg")


@pytest.mark.parametrize("name", _SUPPORTED_ATLASES)
def test_init_runs(name: str):
"""Test AtlasBrowser initialisation."""
AtlasBrowser(name)


def test_init_error_catch():
"""Test AtlasBrowser initialisation errors caught."""
incorrect_name = "Not an atlas"
with pytest.raises(
ValueError,
match=f"The requested atlas '{incorrect_name}' is not recognised.",
):
AtlasBrowser(incorrect_name)


@pytest.mark.parametrize(
"inputs",
[
["AAL", np.array([40, 0, 60]), ["Frontal_Mid_R"]],
["AAL3", np.array([40, 0, 60]), ["Frontal_Mid_2_R"]],
["HCPEx", np.array([40, 0, 60]), ["Inferior_6-8_Transitional_Area_R"]],
[
"AAL",
np.array([[40, 0, 60], [0, 0, 0]]),
["Frontal_Mid_R", "Undefined"],
],
[
"AAL3",
np.array([[40, 0, 60], [0, 0, 0]]),
["Frontal_Mid_2_R", "Undefined"],
],
[
"HCPEx",
np.array([[40, 0, 60], [0, 0, 0]]),
["Inferior_6-8_Transitional_Area_R", "Undefined"],
],
],
)
@pytest.mark.parametrize("plot", [False, True])
def test_find_regions_runs(
inputs: tuple[str, np.ndarray, list[str]], plot: bool
):
"""Test that `find_regions` returns the correct region(s).
Parameters
----------
inputs : tuple
The atlas name, coordinates, and expected region(s), respectively.
plot : bool
Whether to plot the results.
Notes
-----
Correct regions were found using MRIcron
(https://www.nitrc.org/projects/mricron).
"""
atlas = AtlasBrowser(inputs[0])
assert atlas.find_regions(inputs[1], plot=plot) == inputs[2]


def test_find_regions_error_cach():
"""Test that errors are caught for `find_regions`."""
atlas = AtlasBrowser("AAL") # atlas type is irrelevant

coords_list = [[40, 0, 60]]
with pytest.raises(
TypeError,
match="`coordinates` must be a NumPy array.",
):
atlas.find_regions(coords_list)

coords_3d = np.array([[40, 0, 60]])[:, np.newaxis]
with pytest.raises(
ValueError,
match=(
"`coordinates` must have two dimensions, but it has "
f"{coords_3d.ndim} dimensions."
),
):
atlas.find_regions(coords_3d)

class TestAtlasBrowser:
def test_invalid_atlas(self):
with pytest.raises(ValueError, match="The requested atlas"):
AtlasBrowser("Not An Atlas")

def test_invalid_coordinates_type(self):
with pytest.raises(TypeError, match="'coordinates' should be a numpy"):
AtlasBrowser("AAL").find_regions([44, 44, 44])

def test_invalid_coordinates_ndim(self):
with pytest.raises(ValueError, match="'coordinates' should have two"):
AtlasBrowser("AAL").find_regions(np.zeros((1, 3, 1)))

def test_invalid_coordinates_shape(self):
with pytest.raises(
ValueError, match=r"'coordinates' should be an \[n x 3\]"
):
AtlasBrowser("AAL").find_regions(np.full((1, 4), 44))

def test_vector_coordinates(self):
AtlasBrowser("AAL").find_regions(np.full((3,), 44))

def test_multiple_coordinates(self):
coordinates = np.full((2, 3), 44)
regions = AtlasBrowser("AAL").find_regions(coordinates)
assert len(regions) == coordinates.shape[0], (
"The number of returned regions does not match the number of "
"supplied coordinates"
)

def test_defined_region(self):
regions = AtlasBrowser("AAL").find_regions(np.array([40, 0, 60]))
# Correct region found using MRIcron
# (https://www.nitrc.org/projects/mricron)
assert regions == ["Frontal_Mid_R"], "The region is not correct."

def test_undefined_region(self):
regions = AtlasBrowser("AAL").find_regions(np.full((1, 3), 0))
assert regions == ["undefined"], "The region is not undefined."

def test_supported_atlases(self):
for atlas_name in AtlasBrowser.supported_atlases:
AtlasBrowser(atlas_name).find_regions(np.full((1, 3), 44))

def test_plotting(self):
AtlasBrowser("AAL").find_regions(np.full((1, 3), 44), plot=True)
coords_n_by_4 = np.array([[40, 0, 60, 0]])
with pytest.raises(
ValueError,
match=(
r"'coordinates' must have shape \(n, 3\), but it has shape \(n, "
rf"{coords_n_by_4.shape[1]}\)."
),
):
atlas.find_regions(coords_n_by_4)

0 comments on commit f60bdfd

Please sign in to comment.