-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #3 from kaitj/enh/io_functions
Add IO functions
- Loading branch information
Showing
14 changed files
with
708 additions
and
17 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
# Directories | ||
__pycache__/ | ||
|
||
# Testing | ||
.hypothesis | ||
.ruff_cache | ||
.pytest_cache |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
"""Top-level for afids-utils.""" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
"""Custom exceptions""" | ||
|
||
|
||
class InvalidFiducialNumberError(Exception): | ||
"""Exception for invalid fiducial number""" | ||
|
||
def __init__(self, fid_num: int) -> None: | ||
super().__init__(f"Provided fiducial {fid_num} is not valid.") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
"""Methods for loading and saving nifti files""" | ||
from __future__ import annotations | ||
|
||
import csv | ||
from importlib import resources | ||
from os import PathLike | ||
|
||
import numpy as np | ||
import pandas as pd | ||
from numpy.typing import NDArray | ||
|
||
from afids_utils.exceptions import InvalidFiducialNumberError | ||
|
||
FCSV_FIELDNAMES = [ | ||
"# columns = id", | ||
"x", | ||
"y", | ||
"z", | ||
"ow", | ||
"ox", | ||
"oy", | ||
"oz", | ||
"vis", | ||
"sel", | ||
"lock", | ||
"label", | ||
"desc", | ||
"associatedNodeID", | ||
] | ||
|
||
|
||
def get_afid( | ||
fcsv_path: PathLike[str] | str, fid_num: int | ||
) -> NDArray[np.single]: | ||
"""Extract specific fiducial's spatial coordinates""" | ||
if fid_num < 1 or fid_num > 32: | ||
raise InvalidFiducialNumberError(fid_num) | ||
fcsv_df = pd.read_csv( | ||
fcsv_path, sep=",", header=2, usecols=FCSV_FIELDNAMES | ||
) | ||
|
||
return fcsv_df.loc[fid_num - 1, ["x", "y", "z"]].to_numpy(dtype="single") | ||
|
||
|
||
def afids_to_fcsv( | ||
afid_coords: NDArray[np.single], | ||
fcsv_output: PathLike[str] | str, | ||
) -> None: | ||
"""AFIDS to Slicer-compatible .fcsv file""" | ||
# Read in fcsv template | ||
with resources.open_text( | ||
"afids_utils.resources", "template.fcsv" | ||
) as template_fcsv_file: | ||
header = [template_fcsv_file.readline() for _ in range(3)] | ||
reader = csv.DictReader(template_fcsv_file, fieldnames=FCSV_FIELDNAMES) | ||
fcsv = list(reader) | ||
|
||
# Loop over fiducials and update with fiducial spatial coordinates | ||
for idx, row in enumerate(fcsv): | ||
row["x"] = afid_coords[idx][0] | ||
row["y"] = afid_coords[idx][1] | ||
row["z"] = afid_coords[idx][2] | ||
|
||
# Write output fcsv | ||
with open(fcsv_output, "w", encoding="utf-8", newline="") as out_fcsv_file: | ||
for line in header: | ||
out_fcsv_file.write(line) | ||
writer = csv.DictWriter(out_fcsv_file, fieldnames=FCSV_FIELDNAMES) | ||
for row in fcsv: | ||
writer.writerow(row) |
Empty file.
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
# Markups fiducial file version = 4.6 | ||
# CoordinateSystem = 0 | ||
# columns = id,x,y,z,ow,ox,oy,oz,vis,sel,lock,label,desc,associatedNodeID | ||
vtkMRMLMarkupsFiducialNode_1,0,0,0,0,0,0,1,1,1,1,1,AC,vtkMRMLScalarVolumeNode1 | ||
vtkMRMLMarkupsFiducialNode_2,0,0,0,0,0,0,1,1,1,1,2,PC,vtkMRMLScalarVolumeNode1 | ||
vtkMRMLMarkupsFiducialNode_3,0,0,0,0,0,0,1,1,1,1,3,infracollicular sulcus,vtkMRMLScalarVolumeNode1 | ||
vtkMRMLMarkupsFiducialNode_4,0,0,0,0,0,0,1,1,1,1,4,PMJ,vtkMRMLScalarVolumeNode1 | ||
vtkMRMLMarkupsFiducialNode_5,0,0,0,0,0,0,1,1,1,1,5,superior interpeduncular fossa,vtkMRMLScalarVolumeNode1 | ||
vtkMRMLMarkupsFiducialNode_6,0,0,0,0,0,0,1,1,1,1,6,R superior LMS,vtkMRMLScalarVolumeNode1 | ||
vtkMRMLMarkupsFiducialNode_7,0,0,0,0,0,0,1,1,1,1,7,L superior LMS,vtkMRMLScalarVolumeNode1 | ||
vtkMRMLMarkupsFiducialNode_8,0,0,0,0,0,0,1,1,1,1,8,R inferior LMS,vtkMRMLScalarVolumeNode1 | ||
vtkMRMLMarkupsFiducialNode_9,0,0,0,0,0,0,1,1,1,1,9,L inferior LMS,vtkMRMLScalarVolumeNode1 | ||
vtkMRMLMarkupsFiducialNode_10,0,0,0,0,0,0,1,1,1,1,10,culmen,vtkMRMLScalarVolumeNode1 | ||
vtkMRMLMarkupsFiducialNode_11,0,0,0,0,0,0,1,1,1,1,11,intermammillary sulcus,vtkMRMLScalarVolumeNode1 | ||
vtkMRMLMarkupsFiducialNode_12,0,0,0,0,0,0,1,1,1,1,12,R MB,vtkMRMLScalarVolumeNode1 | ||
vtkMRMLMarkupsFiducialNode_13,0,0,0,0,0,0,1,1,1,1,13,L MB,vtkMRMLScalarVolumeNode1 | ||
vtkMRMLMarkupsFiducialNode_14,0,0,0,0,0,0,1,1,1,1,14,pineal gland,vtkMRMLScalarVolumeNode1 | ||
vtkMRMLMarkupsFiducialNode_15,0,0,0,0,0,0,1,1,1,1,15,R LV at AC,vtkMRMLScalarVolumeNode1 | ||
vtkMRMLMarkupsFiducialNode_16,0,0,0,0,0,0,1,1,1,1,16,L LV at AC,vtkMRMLScalarVolumeNode1 | ||
vtkMRMLMarkupsFiducialNode_17,0,0,0,0,0,0,1,1,1,1,17,R LV at PC,vtkMRMLScalarVolumeNode1 | ||
vtkMRMLMarkupsFiducialNode_18,0,0,0,0,0,0,1,1,1,1,18,L LV at PC,vtkMRMLScalarVolumeNode1 | ||
vtkMRMLMarkupsFiducialNode_19,0,0,0,0,0,0,1,1,1,1,19,genu of CC,vtkMRMLScalarVolumeNode1 | ||
vtkMRMLMarkupsFiducialNode_20,0,0,0,0,0,0,1,1,1,1,20,splenium,vtkMRMLScalarVolumeNode1 | ||
vtkMRMLMarkupsFiducialNode_21,0,0,0,0,0,0,1,1,1,1,21,R AL temporal horn,vtkMRMLScalarVolumeNode1 | ||
vtkMRMLMarkupsFiducialNode_22,0,0,0,0,0,0,1,1,1,1,22,L AL temporal horn,vtkMRMLScalarVolumeNode1 | ||
vtkMRMLMarkupsFiducialNode_23,0,0,0,0,0,0,1,1,1,1,23,R superior AM temporal horn,vtkMRMLScalarVolumeNode1 | ||
vtkMRMLMarkupsFiducialNode_24,0,0,0,0,0,0,1,1,1,1,24,L superior AM temporal horn,vtkMRMLScalarVolumeNode1 | ||
vtkMRMLMarkupsFiducialNode_25,0,0,0,0,0,0,1,1,1,1,25,R inferior AM temporal horn,vtkMRMLScalarVolumeNode1 | ||
vtkMRMLMarkupsFiducialNode_26,0,0,0,0,0,0,1,1,1,1,26,L inferior AM temporal horn,vtkMRMLScalarVolumeNode1 | ||
vtkMRMLMarkupsFiducialNode_27,0,0,0,0,0,0,1,1,1,1,27,R indusium griseum origin,vtkMRMLScalarVolumeNode1 | ||
vtkMRMLMarkupsFiducialNode_28,0,0,0,0,0,0,1,1,1,1,28,L indusium griseum origin,vtkMRMLScalarVolumeNode1 | ||
vtkMRMLMarkupsFiducialNode_29,0,0,0,0,0,0,1,1,1,1,29,R ventral occipital horn,vtkMRMLScalarVolumeNode1 | ||
vtkMRMLMarkupsFiducialNode_30,0,0,0,0,0,0,1,1,1,1,30,L ventral occipital horn,vtkMRMLScalarVolumeNode1 | ||
vtkMRMLMarkupsFiducialNode_31,0,0,0,0,0,0,1,1,1,1,31,R olfactory sulcal fundus,vtkMRMLScalarVolumeNode1 | ||
vtkMRMLMarkupsFiducialNode_32,0,0,0,0,0,0,1,1,1,1,32,L olfactory sulcal fundus,vtkMRMLScalarVolumeNode1 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
# Markups fiducial file version = 4.6 | ||
# CoordinateSystem = 0 | ||
# columns = id,x,y,z,ow,ox,oy,oz,vis,sel,lock,label,desc,associatedNodeID | ||
vtkMRMLMarkupsFiducialNode_1,-0.23099999999999998,2.93275,-4.899,0,0,0,1,1,1,0,1,AC,vtkMRMLScalarVolumeNode1 | ||
vtkMRMLMarkupsFiducialNode_2,-0.24125000000000002,-25.074749999999998,-2.169,0,0,0,1,1,1,0,2,PC,vtkMRMLScalarVolumeNode1 | ||
vtkMRMLMarkupsFiducialNode_3,0.19949999999999998,-37.268249999999995,-10.9115,0,0,0,1,1,1,0,3,infracollicular sulcus,vtkMRMLScalarVolumeNode1 | ||
vtkMRMLMarkupsFiducialNode_4,-0.12725000000000003,-23.14825,-21.522,0,0,0,1,1,1,0,4,PMJ,vtkMRMLScalarVolumeNode1 | ||
vtkMRMLMarkupsFiducialNode_5,-0.06675,-14.81275,-11.32025,0,0,0,1,1,1,0,5,superior interpeduncular fossa,vtkMRMLScalarVolumeNode1 | ||
vtkMRMLMarkupsFiducialNode_6,12.67275,-26.960749999999997,-10.38225,0,0,0,1,1,1,0,6,R superior LMS,vtkMRMLScalarVolumeNode1 | ||
vtkMRMLMarkupsFiducialNode_7,-13.004999999999999,-27.190749999999998,-10.32375,0,0,0,1,1,1,0,7,L superior LMS,vtkMRMLScalarVolumeNode1 | ||
vtkMRMLMarkupsFiducialNode_8,10.873,-30.96475,-21.533,0,0,0,1,1,1,0,8,R inferior LMS,vtkMRMLScalarVolumeNode1 | ||
vtkMRMLMarkupsFiducialNode_9,-11.18,-30.437000000000005,-21.537,0,0,0,1,1,1,0,9,L inferior LMS,vtkMRMLScalarVolumeNode1 | ||
vtkMRMLMarkupsFiducialNode_10,-0.004750000000000004,-52.3695,2.06825,0,0,0,1,1,1,0,10,culmen,vtkMRMLScalarVolumeNode1 | ||
vtkMRMLMarkupsFiducialNode_11,-0.0665,-8.131499999999999,-14.7755,0,0,0,1,1,1,0,11,intermammillary sulcus,vtkMRMLScalarVolumeNode1 | ||
vtkMRMLMarkupsFiducialNode_12,2.0255,-8.143500000000001,-14.752749999999999,0,0,0,1,1,1,0,12,R MB,vtkMRMLScalarVolumeNode1 | ||
vtkMRMLMarkupsFiducialNode_13,-2.3954999999999997,-8.0565,-14.8675,0,0,0,1,1,1,0,13,L MB,vtkMRMLScalarVolumeNode1 | ||
vtkMRMLMarkupsFiducialNode_14,0.04425,-31.73675,0.76675,0,0,0,1,1,1,0,14,pineal gland,vtkMRMLScalarVolumeNode1 | ||
vtkMRMLMarkupsFiducialNode_15,15.141750000000002,5.3445,24.358249999999998,0,0,0,1,1,1,0,15,R LV at AC,vtkMRMLScalarVolumeNode1 | ||
vtkMRMLMarkupsFiducialNode_16,-15.6095,5.22575,24.63125,0,0,0,1,1,1,0,16,L LV at AC,vtkMRMLScalarVolumeNode1 | ||
vtkMRMLMarkupsFiducialNode_17,18.4955,-22.061999999999998,27.66,0,0,0,1,1,1,0,17,R LV at PC,vtkMRMLScalarVolumeNode1 | ||
vtkMRMLMarkupsFiducialNode_18,-18.999000000000002,-22.046,27.284,0,0,0,1,1,1,0,18,L LV at PC,vtkMRMLScalarVolumeNode1 | ||
vtkMRMLMarkupsFiducialNode_19,0.17799999999999996,33.423500000000004,2.6755,0,0,0,1,1,1,0,19,genu of CC,vtkMRMLScalarVolumeNode1 | ||
vtkMRMLMarkupsFiducialNode_20,0.037000000000000005,-37.6845,6.14675,0,0,0,1,1,1,0,20,splenium of CC,vtkMRMLScalarVolumeNode1 | ||
vtkMRMLMarkupsFiducialNode_21,34.312250000000006,-4.982,-26.87875,0,0,0,1,1,1,0,21,R AL temporal horn,vtkMRMLScalarVolumeNode1 | ||
vtkMRMLMarkupsFiducialNode_22,-34.72725,-7.2475,-25.203,0,0,0,1,1,1,0,22,L AL temporal horn,vtkMRMLScalarVolumeNode1 | ||
vtkMRMLMarkupsFiducialNode_23,18.69625,-10.56775,-17.7725,0,0,0,1,1,1,0,23,R superior AM temporal horn,vtkMRMLScalarVolumeNode1 | ||
vtkMRMLMarkupsFiducialNode_24,-20.12075,-11.62125,-17.1495,0,0,0,1,1,1,0,24,L superior AM temporal horn,vtkMRMLScalarVolumeNode1 | ||
vtkMRMLMarkupsFiducialNode_25,21.07325,-4.56975,-28.90925,0,0,0,1,1,1,0,25,R inferior AM temporal horn,vtkMRMLScalarVolumeNode1 | ||
vtkMRMLMarkupsFiducialNode_26,-21.966749999999998,-5.28875,-27.895,0,0,0,1,1,1,0,26,L inferior AM temporal horn,vtkMRMLScalarVolumeNode1 | ||
vtkMRMLMarkupsFiducialNode_27,14.40775,-39.697250000000004,3.8390000000000004,0,0,0,1,1,1,0,27,R indusium griseum origin,vtkMRMLScalarVolumeNode1 | ||
vtkMRMLMarkupsFiducialNode_28,-15.48075,-42.28375,3.9659999999999997,0,0,0,1,1,1,0,28,L indusium griseum origin,vtkMRMLScalarVolumeNode1 | ||
vtkMRMLMarkupsFiducialNode_29,20.730249999999998,-79.47475,4.85125,0,0,0,1,1,1,0,29,R ventral occipital horn,vtkMRMLScalarVolumeNode1 | ||
vtkMRMLMarkupsFiducialNode_30,-19.41825,-81.46925,3.785,0,0,0,1,1,1,0,30,L ventral occipital horn,vtkMRMLScalarVolumeNode1 | ||
vtkMRMLMarkupsFiducialNode_31,12.108500000000001,17.02725,-12.94,0,0,0,1,1,1,0,31,R olfactory sulcal fundus,vtkMRMLScalarVolumeNode1 | ||
vtkMRMLMarkupsFiducialNode_32,-13.34325,17.0885,-13.35225,0,0,0,1,1,1,0,32,L olfactory sulcal fundus,vtkMRMLScalarVolumeNode1 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
from __future__ import annotations | ||
|
||
import csv | ||
import tempfile | ||
from os import PathLike, remove | ||
from pathlib import Path | ||
|
||
import numpy as np | ||
import pytest | ||
from hypothesis import HealthCheck, assume, given, settings | ||
from hypothesis import strategies as st | ||
from hypothesis.extra.numpy import arrays | ||
from numpy.typing import NDArray | ||
|
||
from afids_utils.exceptions import InvalidFiducialNumberError | ||
from afids_utils.io import FCSV_FIELDNAMES, afids_to_fcsv, get_afid | ||
|
||
|
||
@pytest.fixture | ||
def valid_fcsv_file() -> PathLike[str]: | ||
return ( | ||
Path(__file__).parent / "data" / "tpl-MNI152NLin2009cAsym_afids.fcsv" | ||
) | ||
|
||
|
||
class TestGetAfid: | ||
@given(afid_num=st.integers(min_value=1, max_value=32)) | ||
@settings( | ||
suppress_health_check=[HealthCheck.function_scoped_fixture], | ||
) | ||
def test_valid_num_get_afid( | ||
self, valid_fcsv_file: PathLike[str], afid_num: int | ||
): | ||
afid = get_afid(valid_fcsv_file, afid_num) | ||
|
||
# Check array type | ||
assert isinstance(afid, np.ndarray) | ||
# Check array values | ||
assert afid.dtype == np.single | ||
|
||
@given(afid_num=st.integers(min_value=-1000, max_value=1000)) | ||
@settings( | ||
suppress_health_check=[HealthCheck.function_scoped_fixture], | ||
) | ||
def test_invalid_num_get_afid( | ||
self, | ||
valid_fcsv_file: PathLike[str], | ||
afid_num: int, | ||
): | ||
assume(afid_num < 1 or afid_num > 32) | ||
|
||
with pytest.raises( | ||
InvalidFiducialNumberError, match=".*is not valid." | ||
): | ||
get_afid(valid_fcsv_file, afid_num) | ||
|
||
|
||
@st.composite | ||
def afid_coords( | ||
draw: st.DrawFn, | ||
min_value: float = -50.0, | ||
max_value: float = 50.0, | ||
width: int = 16, | ||
) -> NDArray[np.single]: | ||
coords = draw( | ||
arrays( | ||
shape=(32, 3), | ||
dtype=np.single, | ||
elements=st.floats( | ||
min_value=min_value, max_value=max_value, width=width | ||
), | ||
) | ||
) | ||
|
||
return coords | ||
|
||
|
||
class TestAfidsToFcsv: | ||
@given(afids_coords=afid_coords()) | ||
@settings( | ||
suppress_health_check=[HealthCheck.function_scoped_fixture], | ||
) | ||
def test_write_fcsv( | ||
self, afids_coords: NDArray[np.single], valid_fcsv_file: PathLike[str] | ||
) -> None: | ||
out_fcsv_file = tempfile.NamedTemporaryFile( | ||
mode="w", delete=False, prefix="sub-test_afids.fcsv" | ||
) | ||
out_fcsv_path = Path(out_fcsv_file.name) | ||
|
||
afids_to_fcsv(afids_coords, out_fcsv_path) | ||
|
||
# Check file was created | ||
assert out_fcsv_path.exists() | ||
|
||
# Load files | ||
with open( | ||
valid_fcsv_file, "r", encoding="utf-8", newline="" | ||
) as template_fcsv_file: | ||
template_header = [template_fcsv_file.readline() for _ in range(3)] | ||
|
||
with open( | ||
out_fcsv_path, "r", encoding="utf-8", newline="" | ||
) as output_fcsv_file: | ||
output_header = [output_fcsv_file.readline() for _ in range(3)] | ||
reader = csv.DictReader( | ||
output_fcsv_file, fieldnames=FCSV_FIELDNAMES | ||
) | ||
output_fcsv = list(reader) | ||
|
||
# Check header | ||
assert output_header == template_header | ||
# Check contents | ||
for idx, row in enumerate(output_fcsv): | ||
assert (row["x"], row["y"], row["z"]) == ( | ||
str(afids_coords[idx][0]), | ||
str(afids_coords[idx][1]), | ||
str(afids_coords[idx][2]), | ||
) | ||
|
||
# Delete temporary file | ||
remove(out_fcsv_path) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
comment: | ||
layout: "header, diff, flags, components" | ||
|
||
coverage: | ||
status: | ||
project: | ||
default: | ||
target: 100% | ||
threshold: 5% | ||
|
||
component_management: | ||
individual_components: | ||
- component_id: afids-utils_io | ||
name: afids-utils_io | ||
paths: | ||
- afids_utils/io/ |
Oops, something went wrong.