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

Add methods for handling JSON #33

Merged
merged 7 commits into from
Sep 15, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
14 changes: 10 additions & 4 deletions afids_utils/afids.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,8 +178,12 @@ def load(cls, afids_fpath: PathLike[str] | str) -> AfidSet:
afids_fpath
)
# Loading json
# if afids_fpath_ext = ".json":
# load_json(afids_path)
elif afids_fpath_ext == ".json":
from afids_utils.ext.json import load_json

slicer_version, coord_system, afids_positions = load_json(
afids_fpath
)
else:
raise ValueError("Unsupported file extension")

Expand Down Expand Up @@ -229,8 +233,10 @@ def save(self, out_fpath: PathLike[str] | str) -> None:

save_fcsv(self, out_fpath)
# Saving json
# if out_fpath_ext = ".json":
# save_json(afids_coords, out_fpath)
elif out_fpath_ext == ".json":
from afids_utils.ext.json import save_json

save_json(self, out_fpath)
else:
raise ValueError("Unsupported file extension")

Expand Down
17 changes: 4 additions & 13 deletions afids_utils/ext/fcsv.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,7 @@


def _get_metadata(in_fcsv: list[str]) -> tuple[str, str]:
"""
Internal function to extract metadata from header of fcsv files
"""Internal function to extract metadata from header of fcsv files

Parameters
----------
Expand Down Expand Up @@ -73,8 +72,7 @@ def _get_metadata(in_fcsv: list[str]) -> tuple[str, str]:


def _get_afids(in_fcsv: list[str]) -> list[AfidPosition]:
"""
Internal function for converting .fcsv file to a pl.DataFrame
"""Internal function for grabbing AFID positions from .fcsv file

Parameters
----------
Expand Down Expand Up @@ -109,8 +107,7 @@ def _get_afids(in_fcsv: list[str]) -> list[AfidPosition]:
def load_fcsv(
fcsv_path: PathLike[str] | str,
) -> tuple[str, str, list[AfidPosition]]:
"""
Read in fcsv to an AfidSet
"""Read in fcsv and extract relevant information for an AfidSet

Parameters
----------
Expand Down Expand Up @@ -143,8 +140,7 @@ def save_fcsv(
afid_set: AfidSet,
out_fcsv: PathLike[str] | str,
) -> None:
"""
Save fiducials to output fcsv file
"""Save fiducials to output fcsv file

Parameters
----------
Expand All @@ -153,11 +149,6 @@ def save_fcsv(

out_fcsv
Path of fcsv file to save AFIDs to

Raises
------
TypeError
If number of fiducials to write does not match expected number
"""
# Read in fcsv template
with resources.open_text(
Expand Down
163 changes: 163 additions & 0 deletions afids_utils/ext/json.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
"""Methods for handling .json files associated with AFIDS"""
from __future__ import annotations

import json
from importlib import resources
from os import PathLike

from typing_extensions import TypedDict

from afids_utils.afids import AfidPosition, AfidSet
from afids_utils.exceptions import InvalidFileError


class ControlPoint(TypedDict):
id: str
label: str
description: str
associatedNodeID: str
position: list[float]
orientation: list[float]
selected: bool
locked: bool
visibility: bool
positionStatus: str


def _get_metadata(coord_system: str) -> tuple[str, str]:
"""Internal function to extract metadata from json files

Note: Slicer version is not currently included in the json file
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: This is perhaps another argument for dropping slicer_version from AfidSet

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I agree - if we end up dropping this, I would prefer to do it in a separate PR


Parameters:
-----------
coord_system
Coordinate system parsed from json_file

Returns
-------
parsed_version
Slicer version associated with fiducial file

parsed_coord
Coordinate system of fiducials

Raises
------
InvalidFileError
If header is invalid from .json file
"""

# Update if future json versions include Slicer version
parsed_version = "Unknown"
parsed_coord = coord_system

# Transform coordinate system so human-understandable
if parsed_coord == "0":
parsed_coord = "RAS"
elif parsed_coord == "1":
parsed_coord = "LPS"

if parsed_coord not in ["RAS", "LPS"]:
raise InvalidFileError("Invalid coordinate system")

return parsed_version, parsed_coord


def _get_afids(control_points: list[ControlPoint]) -> list[AfidPosition]:
"""Internal function to parse fiducial information from json file

Parameters
----------
ctrl_points
List of dicts containing fiducial information from parsed json file

Returns
-------
afid_positions
List containing spatial position of afids
"""
afids_positions = [
AfidPosition(
label=int(afid["label"]),
x=float(afid["position"][0]),
y=float(afid["position"][1]),
z=float(afid["position"][2]),
desc=afid["description"],
)
for afid in control_points
]

return afids_positions


def load_json(
json_path: PathLike[str] | str,
) -> tuple[str, str, list[AfidPosition]]:
"""Read in json and extract relevant information for an AfidSet

Parameters
----------
json_path
Path to .json file containing AFIDs coordinates

Returns
-------
slicer_version
Slicer version associated with fiducial file

coord_system
Coordinate system of fiducials

afids_positions
List containing spatial position of afids
"""
with open(json_path) as json_file:
afids_json = json.load(json_file)

# Grab metadata
slicer_version, coord_system = _get_metadata(
afids_json["markups"][0]["coordinateSystem"]
)
# Grab afids
afids_positions = _get_afids(afids_json["markups"][0]["controlPoints"])

return slicer_version, coord_system, afids_positions


def save_json(
afid_set: AfidSet,
out_json: PathLike[str] | str,
) -> None:
"""Save fiducials to output json file

Parameters
----------
afid_set
A complete AfidSet containing metadata and positions of AFIDs

out_json
Path of json file to save AFIDs to
"""
# Read in json template
with resources.open_text(
"afids_utils.resources", "template.json"
) as template_json_file:
template_content = json.load(template_json_file)

# Update header
template_content["markups"][0][
"coordinateSystem"
] = afid_set.coord_system

# Loop and update with fiducial coordinates
for idx in range(len(template_content["markups"][0]["controlPoints"])):
template_content["markups"][0]["controlPoints"][idx]["position"] = [
afid_set.afids[idx].x,
afid_set.afids[idx].y,
afid_set.afids[idx].z,
]

# Write output json
with open(out_json, "w") as out_json_file:
json.dump(template_content, out_json_file, indent=4)
Loading