Skip to content

Commit

Permalink
adding roi from coordinates
Browse files Browse the repository at this point in the history
  • Loading branch information
gacou54 committed Feb 21, 2024
1 parent fd11e3e commit ce89810
Show file tree
Hide file tree
Showing 4 changed files with 130 additions and 3 deletions.
60 changes: 59 additions & 1 deletion rt_utils/ds_helper.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import datetime
from typing import List

from rt_utils.image_helper import get_contours_coords
from rt_utils.utils import ROIData, SOPClassUID
import numpy as np
Expand Down Expand Up @@ -157,7 +159,7 @@ def create_roi_contour(roi_data: ROIData, series_data) -> Dataset:
return roi_contour


def create_contour_sequence(roi_data: ROIData, series_data) -> Sequence:
def create_contour_sequence(roi_data: ROIData, series_data: List[Dataset]) -> Sequence:
"""
Iterate through each slice of the mask
For each connected segment within a slice, create a contour
Expand All @@ -175,6 +177,62 @@ def create_contour_sequence(roi_data: ROIData, series_data) -> Sequence:
return contour_sequence


def create_roi_contour_from_coordinates(coordinates: List[List[float]], roi_data: ROIData, series_data) -> Dataset:
roi_contour = Dataset()
roi_contour.ROIDisplayColor = roi_data.color
roi_contour.ContourSequence = create_contour_sequence_from_coordinates(coordinates, series_data)
roi_contour.ReferencedROINumber = str(roi_data.number)

return roi_contour


def _find_closest_slice(series_slices: List[Dataset], z_coord: float) -> Dataset:
return min(series_slices, key=lambda series_slice: abs(series_slice.SliceLocation - z_coord))


def _flatten_lists(lists: List[List[float]]) -> List[float]:
"""Flatten the list [[1, 2, 3], [1, 2, 3] -> [1, 2, 3, 1, 2, 3]"""
flatten_list = []
for l in lists:
flatten_list += l

return flatten_list


def create_contour_sequence_from_coordinates(coordinates: List[List[List[float]]], series_data: List[Dataset]) -> Sequence:
"""
Iterate through each slice of the mask
For each connected segment within a slice, create a contour
"""
slice_position_to_contours_coords = {}

for contours_coords in coordinates:
# Find the closest slice from the provided z coordinates
closest_slice = _find_closest_slice(series_slices=series_data, z_coord=contours_coords[0][2])

# Format contour coordinates in DICOM format [x1,y1,z1,x2,y2,z2,x3,y3,z3]
contour = _flatten_lists(contours_coords)

# Add contour to
if closest_slice.SliceLocation not in slice_position_to_contours_coords:
slice_position_to_contours_coords[closest_slice.SliceLocation] = []

slice_position_to_contours_coords[closest_slice.SliceLocation].append(contour)

# Making the DICOM contour sequence
contour_sequence = Sequence()

for z_position, slice_contours in slice_position_to_contours_coords.items():
for series_slice in series_data:
if series_slice.SliceLocation == z_position:
for contour_data in slice_contours:
contour = create_contour(series_slice, contour_data)
contour_sequence.append(contour)
continue

return contour_sequence


def create_contour(series_slice: Dataset, contour_data: np.ndarray) -> Dataset:
contour_image = Dataset()
contour_image.ReferencedSOPClassUID = series_slice.SOPClassUID
Expand Down
33 changes: 33 additions & 0 deletions rt_utils/rtstruct.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,39 @@ def add_roi(
ds_helper.create_rtroi_observation(roi_data)
)

def add_roi_from_coordinates(
self,
coordinates: List[List[List[float]]],
color: Union[str, List[int]] = None,
name: str = None,
description: str = "",
use_pin_hole: bool = False,
approximate_contours: bool = True,
roi_generation_algorithm: Union[str, int] = 0,
):
roi_number = len(self.ds.StructureSetROISequence) + 1
roi_data = ROIData(
np.zeros(1), # Fake mask since we do not use it because we use coordinates
color,
roi_number,
name,
self.frame_of_reference_uid,
description,
use_pin_hole,
approximate_contours,
roi_generation_algorithm,
)

self.ds.ROIContourSequence.append(
ds_helper.create_roi_contour_from_coordinates(coordinates, roi_data, self.series_data)
)
self.ds.StructureSetROISequence.append(
ds_helper.create_structure_set_roi(roi_data)
)
self.ds.RTROIObservationsSequence.append(
ds_helper.create_rtroi_observation(roi_data)
)

def validate_mask(self, mask: np.ndarray) -> bool:
if mask.dtype != bool:
raise RTStruct.ROIException(
Expand Down
5 changes: 3 additions & 2 deletions rt_utils/utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from typing import List, Union
from random import randrange

import numpy as np
from pydicom.uid import PYDICOM_IMPLEMENTATION_UID
from dataclasses import dataclass

Expand Down Expand Up @@ -41,8 +43,7 @@ class SOPClassUID:
@dataclass
class ROIData:
"""Data class to easily pass ROI data to helper methods."""

mask: str
mask: np.ndarray
color: Union[str, List[int]]
number: int
name: str
Expand Down
35 changes: 35 additions & 0 deletions tests/test_rtstruct_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,41 @@ def test_add_valid_roi(new_rtstruct: RTStruct):
assert new_rtstruct.get_roi_names() == [NAME]


def test_add_valid_roi_from_coordinates(new_rtstruct: RTStruct):
assert new_rtstruct.get_roi_names() == []
assert len(new_rtstruct.ds.ROIContourSequence) == 0
assert len(new_rtstruct.ds.StructureSetROISequence) == 0
assert len(new_rtstruct.ds.RTROIObservationsSequence) == 0

NAME = "Test ROI"
COLOR = [123, 123, 232]
coordinates = [
[
[-100, -100, 60],
[-100, -75, 60],
[-75, -75, 60],
[-75, -100, 60]
],
[
[-90, -90, 65],
[-90, -65, 65],
[-65, -65, 65],
[-65, -90, 65],
]
]

new_rtstruct.add_roi_from_coordinates(coordinates, color=COLOR, name=NAME)

assert len(new_rtstruct.ds.ROIContourSequence) == 1
assert (
len(new_rtstruct.ds.ROIContourSequence[0].ContourSequence) == 2
) # 2 contour on to slice were added
assert len(new_rtstruct.ds.StructureSetROISequence) == 1
assert len(new_rtstruct.ds.RTROIObservationsSequence) == 1
assert new_rtstruct.ds.ROIContourSequence[0].ROIDisplayColor == COLOR
assert new_rtstruct.get_roi_names() == [NAME]


def test_get_invalid_roi_mask_by_name(new_rtstruct: RTStruct):
assert new_rtstruct.get_roi_names() == []
with pytest.raises(RTStruct.ROIException):
Expand Down

0 comments on commit ce89810

Please sign in to comment.