Skip to content

Commit

Permalink
Merge pull request #12 from qurit/10-error-with-unreferenced-contour-…
Browse files Browse the repository at this point in the history
…images

feat: throw exception if rtstruct references images outside of series data
  • Loading branch information
awtkns authored Mar 2, 2021
2 parents 8c18a38 + 0cbc37e commit 1a7c7d7
Show file tree
Hide file tree
Showing 5 changed files with 54 additions and 13 deletions.
38 changes: 36 additions & 2 deletions rt_utils/rtstruct_builder.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from typing import List
from pydicom.dataset import Dataset
from pydicom.filereader import dcmread

from rt_utils.utils import SOPClassUID
Expand Down Expand Up @@ -29,14 +31,46 @@ def create_from(dicom_series_path: str, rt_struct_path: str) -> RTStruct:
series_data = image_helper.load_sorted_image_series(dicom_series_path)
ds = dcmread(rt_struct_path)
RTStructBuilder.validate_rtstruct(ds)
RTStructBuilder.validate_rtstruct_series_references(ds, series_data)

# TODO create new frame of reference?
# TODO create new frame of reference? Right now we assume the last frame of reference created is suitable
return RTStruct(series_data, ds)

@staticmethod
def validate_rtstruct(ds):
def validate_rtstruct(ds: Dataset):
"""
Method to validate a dataset is a valid RTStruct containing the required fields
"""

if ds.SOPClassUID != SOPClassUID.RTSTRUCT or \
not hasattr(ds, 'ROIContourSequence') or \
not hasattr(ds, 'StructureSetROISequence') or \
not hasattr(ds, 'RTROIObservationsSequence'):
raise Exception("Please check that the existing RTStruct is valid")

@staticmethod
def validate_rtstruct_series_references(ds: Dataset, series_data: List[Dataset]):
"""
Method to validate RTStruct only references dicom images found within the input series_data
"""
for refd_frame_of_ref in ds.ReferencedFrameOfReferenceSequence:
for rt_refd_study in refd_frame_of_ref.RTReferencedStudySequence:
for rt_refd_series in rt_refd_study.RTReferencedSeriesSequence:
for contour_image in rt_refd_series.ContourImageSequence:
RTStructBuilder.validate_contour_image_in_series_data(
contour_image, series_data)

@staticmethod
def validate_contour_image_in_series_data(contour_image: Dataset, series_data: List[Dataset]):
"""
Method to validate that the ReferencedSOPInstanceUID of a given contour image exists within the series data
"""
for series in series_data:
if contour_image.ReferencedSOPInstanceUID == series.file_meta.MediaStorageSOPInstanceUID:
return

# ReferencedSOPInstanceUID is NOT available
raise Exception(
f'Loaded RTStruct references image(s) that are not contained in input series data. ' +
f'Problematic image has SOP Instance Id: {contour_image.ReferencedSOPInstanceUID}'
)
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.0.4'
VERSION = '1.0.5'
with open("README.md", "r", encoding="utf-8") as fh:
long_description = fh.read()
with open('requirements.txt') as f:
Expand Down
Binary file added tests/mock_data/invalid_reference_rt.dcm
Binary file not shown.
Binary file modified tests/mock_data/rt.dcm
Binary file not shown.
27 changes: 17 additions & 10 deletions tests/test_rtstruct_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def test_create_from_empty_series_dir():
def test_only_images_loaded_into_series_data(new_rtstruct: RTStruct):
assert len(new_rtstruct.series_data) > 0
for ds in new_rtstruct.series_data:
assert ds.SOPClassUID == SOPClassUID.CT_IMAGE_STORAGE
assert hasattr(ds, 'pixel_array')


def test_valid_filemeta(new_rtstruct: RTStruct):
Expand Down Expand Up @@ -82,6 +82,14 @@ def test_loading_invalid_rt_struct(series_path):
RTStructBuilder.create_from(series_path, invalid_rt_struct_path)


def test_loading_invalid_reference_rt_struct(series_path):
# This RTStruct references images not found within the series path
invalid_reference_rt_struct_path = os.path.join(series_path, 'invalid_reference_rt.dcm')
assert os.path.exists(invalid_reference_rt_struct_path)
with pytest.raises(Exception):
RTStructBuilder.create_from(series_path, invalid_reference_rt_struct_path)


def test_loading_valid_rt_struct(series_path):
valid_rt_struct_path = os.path.join(series_path, 'rt.dcm')
assert os.path.exists(valid_rt_struct_path)
Expand All @@ -91,20 +99,20 @@ def test_loading_valid_rt_struct(series_path):
assert hasattr(rtstruct.ds, 'ROIContourSequence')
assert hasattr(rtstruct.ds, 'StructureSetROISequence')
assert hasattr(rtstruct.ds, 'RTROIObservationsSequence')
assert len(rtstruct.ds.ROIContourSequence) == 4
assert len(rtstruct.ds.StructureSetROISequence) == 4
assert len(rtstruct.ds.RTROIObservationsSequence) == 4
assert len(rtstruct.ds.ROIContourSequence) == 1
assert len(rtstruct.ds.StructureSetROISequence) == 1
assert len(rtstruct.ds.RTROIObservationsSequence) == 1

# Test adding a new ROI
mask = get_empty_mask(rtstruct)
mask[50:100,50:100,0] = 1
rtstruct.add_roi(mask)

assert len(rtstruct.ds.ROIContourSequence) == 5 # 1 should be added
assert len(rtstruct.ds.StructureSetROISequence) == 5 # 1 should be added
assert len(rtstruct.ds.RTROIObservationsSequence) == 5 # 1 should be added
assert len(rtstruct.ds.ROIContourSequence) == 2 # 1 should be added
assert len(rtstruct.ds.StructureSetROISequence) == 2 # 1 should be added
assert len(rtstruct.ds.RTROIObservationsSequence) == 2 # 1 should be added
new_roi = rtstruct.ds.StructureSetROISequence[-1]
assert new_roi.ROIName == 'ROI-5'
assert new_roi.ROIName == 'ROI-2'


def test_loaded_mask_iou(new_rtstruct: RTStruct):
Expand Down Expand Up @@ -144,13 +152,12 @@ def run_mask_iou_test(rtstruct:RTStruct, mask, IOU_threshold, use_pin_hole=False
loaded_mask = rtstruct.get_roi_mask_by_name(mask_name)

# Use IOU to test accuracy of loaded mask
print(np.sum(mask))
print(np.sum(loaded_mask))
numerator = np.logical_and(mask, loaded_mask)
denominator = np.logical_or(mask, loaded_mask)
IOU = np.sum(numerator) / np.sum(denominator)
assert IOU >= IOU_threshold


def get_empty_mask(rtstruct) -> np.ndarray:
ref_dicom_image = rtstruct.series_data[0]
mask_dims = (int(ref_dicom_image.Columns), int(ref_dicom_image.Rows), len(rtstruct.series_data))
Expand Down

0 comments on commit 1a7c7d7

Please sign in to comment.