Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Convert between LPS/RAS coordinate systems #32

Merged
merged 4 commits into from
Sep 7, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion afids_utils/tests/test_afids.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,6 @@ class TestAfidsIO:
)
def test_valid_load(
self,
human_mappings: list[dict[str, str]],
valid_fcsv_file: PathLike[str],
label: int,
):
Expand Down
64 changes: 54 additions & 10 deletions afids_utils/tests/test_transforms.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
from numpy.typing import NDArray

import afids_utils.tests.strategies as af_st
from afids_utils.afids import AfidPosition, AfidVoxel
from afids_utils.transforms import voxel_to_world, world_to_voxel
import afids_utils.transforms as af_xfm
from afids_utils.afids import AfidPosition, AfidSet, AfidVoxel


class TestAfidWorld2Voxel:
Expand All @@ -21,7 +21,7 @@ class TestAfidWorld2Voxel:
def test_world_to_voxel_xfm(
self, afid_position: AfidPosition, nii_affine: NDArray[np.float_]
):
afid_voxel = world_to_voxel(afid_position, nii_affine)
afid_voxel = af_xfm.world_to_voxel(afid_position, nii_affine)

assert isinstance(afid_voxel, AfidVoxel)
# Have to assert specific int dtype
Expand All @@ -38,7 +38,7 @@ def test_invalid_world_type(
self, afid_voxel: AfidVoxel, nii_affine: NDArray[np.float_]
):
with pytest.raises(TypeError, match="Not an AfidPosition.*"):
world_to_voxel(afid_voxel, nii_affine)
af_xfm.world_to_voxel(afid_voxel, nii_affine)


class TestAfidVoxel2World:
Expand All @@ -50,7 +50,7 @@ class TestAfidVoxel2World:
def test_voxel_to_world_xfm(
self, afid_voxel: AfidVoxel, nii_affine: NDArray[np.float_]
):
afid_position = voxel_to_world(afid_voxel, nii_affine)
afid_position = af_xfm.voxel_to_world(afid_voxel, nii_affine)

assert isinstance(afid_position, AfidPosition)
# Have to assert specific float dtype
Expand All @@ -69,7 +69,7 @@ def test_invalid_voxel_type(
self, afid_position: AfidPosition, nii_affine: NDArray[np.float_]
):
with pytest.raises(TypeError, match="Not an AfidVoxel.*"):
voxel_to_world(afid_position, nii_affine)
af_xfm.voxel_to_world(afid_position, nii_affine)


class TestAfidRoundTripConvert:
Expand All @@ -83,8 +83,8 @@ class TestAfidRoundTripConvert:
def test_round_trip_world(
self, afid_position: AfidPosition, nii_affine: NDArray[np.float_]
):
afid_voxel = world_to_voxel(afid_position, nii_affine)
afid_position_approx = voxel_to_world(afid_voxel, nii_affine)
afid_voxel = af_xfm.world_to_voxel(afid_position, nii_affine)
afid_position_approx = af_xfm.voxel_to_world(afid_voxel, nii_affine)
print(afid_position, afid_position_approx, nii_affine)

# Check to see if round-trip approximates to within 10mm
Expand All @@ -101,10 +101,54 @@ def test_round_trip_world(
def test_round_trip_voxel(
self, afid_voxel: AfidVoxel, nii_affine: NDArray[np.float_]
):
afid_world = voxel_to_world(afid_voxel, nii_affine)
afid_voxel_approx = world_to_voxel(afid_world, nii_affine)
afid_world = af_xfm.voxel_to_world(afid_voxel, nii_affine)
afid_voxel_approx = af_xfm.world_to_voxel(afid_world, nii_affine)

# Check to see if round-trip approximates to within 2 voxels
assert afid_voxel_approx.i == pytest.approx(afid_voxel.i, abs=2)
assert afid_voxel_approx.j == pytest.approx(afid_voxel.j, abs=2)
assert afid_voxel_approx.k == pytest.approx(afid_voxel.k, abs=2)


class TestCoordSystemXfm:
@given(afid_set=af_st.afid_sets())
@settings(
deadline=400,
suppress_health_check=[HealthCheck.function_scoped_fixture],
)
def test_invalid_new_coord_system(self, afid_set: AfidSet):
with pytest.raises(
ValueError, match=r"Unrecognized coordinate system.*"
):
af_xfm.coord_system_xfm(afid_set, new_coord_system="invalid")

@given(afid_set=af_st.afid_sets(randomize_header=False))
@settings(
deadline=400,
suppress_health_check=[HealthCheck.function_scoped_fixture],
)
def test_same_coord_system(self, afid_set: AfidSet):
with pytest.raises(ValueError, match=r"Already saved in.*"):
af_xfm.coord_system_xfm(afid_set, new_coord_system="LPS")

@given(afid_set=af_st.afid_sets(randomize_header=False))
@settings(
deadline=400,
suppress_health_check=[HealthCheck.function_scoped_fixture],
)
def test_valid_new_coord_system(self, afid_set):
new_afid_set = af_xfm.coord_system_xfm(afid_set)

assert isinstance(new_afid_set, AfidSet)
assert new_afid_set.coord_system == "RAS"

for idx in range(len(new_afid_set.afids)):
kaitj marked this conversation as resolved.
Show resolved Hide resolved
assert (
new_afid_set.afids[idx].x,
new_afid_set.afids[idx].y,
new_afid_set.afids[idx].z,
) == (
-afid_set.afids[idx].x,
-afid_set.afids[idx].y,
afid_set.afids[idx].z,
)
54 changes: 49 additions & 5 deletions afids_utils/transforms.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
"""Methods for transforming between different coordinate systems"""
from __future__ import annotations

from copy import deepcopy

import numpy as np
from numpy.typing import NDArray

from afids_utils.afids import AfidPosition, AfidVoxel
from afids_utils.afids import AfidPosition, AfidSet, AfidVoxel


def world_to_voxel(
afid_world: AfidPosition,
nii_affine: NDArray[np.float_],
) -> AfidVoxel:
"""
Transform fiducials from world coordinates to voxel coordinates
"""Transform fiducials from world coordinates to voxel coordinates

Parameters
----------
Expand Down Expand Up @@ -54,8 +55,7 @@ def voxel_to_world(
afid_voxel: AfidVoxel,
nii_affine: NDArray[np.float_],
) -> AfidPosition:
"""
Transform fiducials from world coordinates to voxel coordinates
"""Transform fiducials from world coordinates to voxel coordinates

Parameters
----------
Expand Down Expand Up @@ -88,3 +88,47 @@ def voxel_to_world(
z=world_pos[2],
desc=afid_voxel.desc,
)


def coord_system_xfm(
kaitj marked this conversation as resolved.
Show resolved Hide resolved
afid_set: AfidSet, new_coord_system: str = "RAS"
) -> AfidSet:
"""Convert AFID set between LPS and RAS coordinates

Parameters
----------
afid_set
Object containing valid AfidSet

new_coord_sys
Convert AFID set to defined coordinate system (default: 'RAS')

Returns
-------
AfidSet
Object containing AFIDs stored in defined coordinate system

Raises
------
ValueError
If invalid coordinate system, or if already in defined
cordinate system
"""
if new_coord_system not in ["RAS", "LPS"]:
raise ValueError(
"Unrecognized coordinate system - please select RAS or LPS"
)

if afid_set.coord_system == new_coord_system:
kaitj marked this conversation as resolved.
Show resolved Hide resolved
raise ValueError(f"Already saved in {new_coord_system}")

# Create copy and update coordinate system
kaitj marked this conversation as resolved.
Show resolved Hide resolved
new_afid_set = deepcopy(afid_set)
new_afid_set.coord_system = new_coord_system

# Update afid positions
for idx in range(len(new_afid_set.afids)):
new_afid_set.afids[idx].x = -new_afid_set.afids[idx].x
new_afid_set.afids[idx].y = -new_afid_set.afids[idx].y

return new_afid_set