Skip to content

Commit

Permalink
feat: add controllable param for approximation method
Browse files Browse the repository at this point in the history
  • Loading branch information
asim-shrestha committed Apr 22, 2021
1 parent c9c7dbd commit 699765d
Show file tree
Hide file tree
Showing 7 changed files with 47 additions and 20 deletions.
2 changes: 1 addition & 1 deletion rt_utils/ds_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ def create_contour_sequence(roi_data: ROIData, series_data) -> Sequence:
print("Skipping empty mask layer")
continue

contour_coords = get_contours_coords(mask_slice, series_slice, roi_data.use_pin_hole)
contour_coords = get_contours_coords(mask_slice, series_slice, roi_data)
for contour_data in contour_coords:
contour = create_contour(series_slice, contour_data)
contour_sequence.append(contour)
Expand Down
19 changes: 10 additions & 9 deletions rt_utils/image_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from pydicom.dataset import Dataset
from pydicom.sequence import Sequence

from rt_utils.utils import SOPClassUID
from rt_utils.utils import ROIData, SOPClassUID


def load_sorted_image_series(dicom_series_path: str):
Expand Down Expand Up @@ -43,13 +43,13 @@ def load_dcm_images_from_path(dicom_series_path: str) -> List[Dataset]:
return series_data


def get_contours_coords(mask_slice: np.ndarray, series_slice: Dataset, use_pin_hole: bool):
def get_contours_coords(mask_slice: np.ndarray, series_slice: Dataset, roi_data: ROIData):
# Create pin hole mask if specified
if use_pin_hole:
mask_slice = create_pin_hole_mask(mask_slice)
if roi_data.use_pin_hole:
mask_slice = create_pin_hole_mask(mask_slice, roi_data.approximate_contours)

# Get contours from mask
contours, _ = find_mask_contours(mask_slice)
contours, _ = find_mask_contours(mask_slice, roi_data.approximate_contours)
validate_contours(contours)

# Format for DICOM
Expand All @@ -63,8 +63,9 @@ def get_contours_coords(mask_slice: np.ndarray, series_slice: Dataset, use_pin_h
return formatted_contours


def find_mask_contours(mask):
contours, hierarchy = cv.findContours(mask.astype(np.uint8), cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE)
def find_mask_contours(mask: np.ndarray, approximate_contours: bool):
approximation_method = cv.CHAIN_APPROX_SIMPLE if approximate_contours else cv.CHAIN_APPROX_NONE
contours, hierarchy = cv.findContours(mask.astype(np.uint8), cv.RETR_TREE, approximation_method)
# Format extra array out of data
for i, contour in enumerate(contours):
contours[i] = [[pos[0][0], pos[0][1]] for pos in contour]
Expand All @@ -74,13 +75,13 @@ def find_mask_contours(mask):



def create_pin_hole_mask(mask):
def create_pin_hole_mask(mask: np.ndarray, approximate_contours: bool):
"""
Creates masks with pin holes added to contour regions with holes.
This is done so that a given region can be represented by a single contour.
"""

contours, hierarchy = find_mask_contours(mask)
contours, hierarchy = find_mask_contours(mask, approximate_contours)
pin_hole_mask = mask.copy()

# Iterate through the hierarchy, for child nodes, draw a line upwards from the first point
Expand Down
21 changes: 19 additions & 2 deletions rt_utils/rtstruct.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,15 @@ def set_series_description(self, description: str):

self.ds.SeriesDescription = description

def add_roi(self, mask: np.ndarray, color: Union[str, List[int]] = None, name: str = None, description: str = '', use_pin_hole: bool = False):
def add_roi(
self,
mask: np.ndarray,
color: Union[str, List[int]] = None,
name: str = None,
description: str = '',
use_pin_hole: bool = False,
approximate_contours: bool = True,
):
"""
Add a ROI to the rtstruct given a 3D binary mask for the ROI's at each slice
Optionally input a color or name for the ROI
Expand All @@ -34,7 +42,16 @@ def add_roi(self, mask: np.ndarray, color: Union[str, List[int]] = None, name: s
# TODO test if name already exists
self.validate_mask(mask)
roi_number = len(self.ds.StructureSetROISequence) + 1
roi_data = ROIData(mask, color, roi_number, name, self.frame_of_reference_uid, description, use_pin_hole)
roi_data = ROIData(
mask,
color,
roi_number,
name,
self.frame_of_reference_uid,
description,
use_pin_hole,
approximate_contours
)

self.ds.ROIContourSequence.append(ds_helper.create_roi_contour(roi_data, self.series_data))
self.ds.StructureSetROISequence.append(ds_helper.create_structure_set_roi(roi_data))
Expand Down
1 change: 1 addition & 0 deletions rt_utils/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ class ROIData:
frame_of_reference_uid: int
description: str = ''
use_pin_hole: bool = False
approximate_contours: bool = True

def __post_init__(self):
self.validate_color()
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import setuptools

VERSION = '1.1.1'
VERSION = '1.1.2'
with open("README.md", "r", encoding="utf-8") as fh:
long_description = fh.read()
with open('requirements.txt') as f:
Expand Down
2 changes: 1 addition & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,4 @@ def get_and_test_series_path() -> str:
series_path = os.path.join(os.path.dirname(__file__), 'mock_data/')
assert os.path.exists(series_path)
return series_path


20 changes: 14 additions & 6 deletions tests/test_rtstruct_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ def test_loaded_mask_iou(new_rtstruct: RTStruct):
mask[50:100, 50:100, 0] = 1
mask[60:150, 40:120, 0] = 1

IOU_threshold = 0.95 # Expect 95% accuracy for this mask
IOU_threshold = 0.95 # Expected accuracy
run_mask_iou_test(new_rtstruct, mask, IOU_threshold)


Expand All @@ -131,8 +131,8 @@ def test_mask_with_holes_iou(new_rtstruct: RTStruct):
mask[50:100, 50:100, 0] = 1
mask[65:85, 65:85, 0] = 0

IOU_threshold = 0.85 # Expect lower accuracy holes lose information
run_mask_iou_test(new_rtstruct, mask, IOU_threshold)
IOU_threshold = 0.85 # Expect lower accuracy since holes lose information
run_mask_iou_test(new_rtstruct, mask, IOU_threshold)


def test_pin_hole_iou(new_rtstruct: RTStruct):
Expand All @@ -142,13 +142,21 @@ def test_pin_hole_iou(new_rtstruct: RTStruct):
mask[65:85, 65:85, 0] = 0

IOU_threshold = 0.85 # Expect lower accuracy holes lose information
run_mask_iou_test(new_rtstruct, mask, IOU_threshold, True)
run_mask_iou_test(new_rtstruct, mask, IOU_threshold, use_pin_hole=True)

def test_no_approximation_iou(new_rtstruct: RTStruct):
mask = get_empty_mask(new_rtstruct)
mask[50:100, 50:100, 0] = 1
mask[60:150, 40:120, 0] = 1

IOU_threshold = 0.95 # Expected accuracy
run_mask_iou_test(new_rtstruct, mask, IOU_threshold, approximate_contours=False)


def run_mask_iou_test(rtstruct:RTStruct, mask, IOU_threshold, use_pin_hole=False):
def run_mask_iou_test(rtstruct:RTStruct, mask, IOU_threshold, use_pin_hole=False, approximate_contours=True):
# Save and load mask
mask_name = "test"
rtstruct.add_roi(mask, name=mask_name, use_pin_hole=use_pin_hole)
rtstruct.add_roi(mask, name=mask_name, use_pin_hole=use_pin_hole, approximate_contours=approximate_contours)
loaded_mask = rtstruct.get_roi_mask_by_name(mask_name)

# Use IOU to test accuracy of loaded mask
Expand Down

0 comments on commit 699765d

Please sign in to comment.