From fd11e3ecd842ead64ad883f02c5f7a5fed8402cc Mon Sep 17 00:00:00 2001 From: gacou54 Date: Fri, 16 Feb 2024 13:12:14 -0500 Subject: [PATCH 01/10] Handling list of DICOM datasets reference when creating rtstruct --- rt_utils/ds_helper.py | 2 +- rt_utils/image_helper.py | 8 +++++--- rt_utils/rtstruct_builder.py | 4 ++-- tests/conftest.py | 24 +++++++++++++++++++----- tests/test_rtstruct_builder.py | 19 +++++++++++++------ 5 files changed, 40 insertions(+), 17 deletions(-) diff --git a/rt_utils/ds_helper.py b/rt_utils/ds_helper.py index 36c4db7..75e18b0 100644 --- a/rt_utils/ds_helper.py +++ b/rt_utils/ds_helper.py @@ -98,7 +98,7 @@ def add_patient_information(ds: FileDataset, series_data): def add_refd_frame_of_ref_sequence(ds: FileDataset, series_data): refd_frame_of_ref = Dataset() - refd_frame_of_ref.FrameOfReferenceUID = getattr(series_data[0], 'FrameOfReferenceUID', generate_uid()) + refd_frame_of_ref.FrameOfReferenceUID = getattr(series_data[0], 'FrameOfReferenceUID', generate_uid()) refd_frame_of_ref.RTReferencedStudySequence = create_frame_of_ref_study_sequence(series_data) # Add to sequence diff --git a/rt_utils/image_helper.py b/rt_utils/image_helper.py index b030987..29b8745 100644 --- a/rt_utils/image_helper.py +++ b/rt_utils/image_helper.py @@ -11,12 +11,14 @@ from rt_utils.utils import ROIData, SOPClassUID -def load_sorted_image_series(dicom_series_path: str): +def load_sorted_image_series(dicom_series_path: str | List[Dataset]) -> List[Dataset]: """ File contains helper methods for loading / formatting DICOM images and contours """ - - series_data = load_dcm_images_from_path(dicom_series_path) + if isinstance(dicom_series_path, str): + series_data = load_dcm_images_from_path(dicom_series_path) + else: + series_data = dicom_series_path if len(series_data) == 0: raise Exception("No DICOM Images found in input path") diff --git a/rt_utils/rtstruct_builder.py b/rt_utils/rtstruct_builder.py index f92efb9..229e416 100644 --- a/rt_utils/rtstruct_builder.py +++ b/rt_utils/rtstruct_builder.py @@ -15,7 +15,7 @@ class RTStructBuilder: """ @staticmethod - def create_new(dicom_series_path: str) -> RTStruct: + def create_new(dicom_series_path: str | List[Dataset]) -> RTStruct: """ Method to generate a new rt struct from a DICOM series """ @@ -25,7 +25,7 @@ def create_new(dicom_series_path: str) -> RTStruct: return RTStruct(series_data, ds) @staticmethod - def create_from(dicom_series_path: str, rt_struct_path: str, warn_only: bool = False) -> RTStruct: + def create_from(dicom_series_path: str | List[Dataset], rt_struct_path: str, warn_only: bool = False) -> RTStruct: """ Method to load an existing rt struct, given related DICOM series and existing rt struct """ diff --git a/tests/conftest.py b/tests/conftest.py index ccd699e..25aa7bc 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,7 +1,11 @@ -from rt_utils.rtstruct import RTStruct -import pytest import os -from rt_utils import RTStructBuilder +from typing import List + +import pydicom +import pytest + +from rt_utils import RTStructBuilder, image_helper +from rt_utils.rtstruct import RTStruct @pytest.fixture() @@ -9,13 +13,23 @@ def series_path() -> str: return get_and_test_series_path("mock_data") +@pytest.fixture() +def series_datasets(series_path) -> List[pydicom.Dataset]: + return image_helper.load_dcm_images_from_path(series_path) + + +@pytest.fixture() +def rtstruct_path(series_path) -> str: + return os.path.join(series_path, "rt.dcm") + + @pytest.fixture() def new_rtstruct() -> RTStruct: return get_rtstruct("mock_data") @pytest.fixture() -def oriented_series_path() -> RTStruct: +def oriented_series_path() -> str: return get_and_test_series_path("oriented_data") @@ -25,7 +39,7 @@ def oriented_rtstruct() -> RTStruct: @pytest.fixture() -def one_slice_series_path() -> RTStruct: +def one_slice_series_path() -> str: return get_and_test_series_path("one_slice_data") diff --git a/tests/test_rtstruct_builder.py b/tests/test_rtstruct_builder.py index e8cc856..3be6b90 100644 --- a/tests/test_rtstruct_builder.py +++ b/tests/test_rtstruct_builder.py @@ -1,9 +1,9 @@ +from _pytest.fixtures import FixtureRequest + from rt_utils.rtstruct import RTStruct import pytest import os from rt_utils import RTStructBuilder -from rt_utils.utils import SOPClassUID -from rt_utils import image_helper from pydicom.dataset import validate_file_meta import numpy as np @@ -15,6 +15,13 @@ def test_create_from_empty_series_dir(): RTStructBuilder.create_new(empty_dir_path) +@pytest.mark.parametrize('series_fixture', ['series_path', 'series_datasets']) +def test_create_new_datasets(series_fixture, request: FixtureRequest): + rtstruct = RTStructBuilder.create_new(request.getfixturevalue(series_fixture)) + + assert len(rtstruct.ds.ReferencedFrameOfReferenceSequence[0].RTReferencedStudySequence) != 0 + + 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: @@ -112,10 +119,10 @@ def test_non_existant_referenced_study_sequence(series_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) - rtstruct = RTStructBuilder.create_from(series_path, valid_rt_struct_path) +@pytest.mark.parametrize('series_fixture', ['series_path', 'series_datasets']) +def test_loading_valid_rt_struct(rtstruct_path, series_fixture, request: FixtureRequest): + assert os.path.exists(rtstruct_path) + rtstruct = RTStructBuilder.create_from(request.getfixturevalue(series_fixture), rtstruct_path) # Tests existing values predefined in the file are found assert hasattr(rtstruct.ds, "ROIContourSequence") From ce898102dd428ba8ae22ae6f48404b30f2524ff7 Mon Sep 17 00:00:00 2001 From: gacou54 Date: Tue, 20 Feb 2024 20:50:21 -0500 Subject: [PATCH 02/10] adding roi from coordinates --- rt_utils/ds_helper.py | 60 +++++++++++++++++++++++++++++++++- rt_utils/rtstruct.py | 33 +++++++++++++++++++ rt_utils/utils.py | 5 +-- tests/test_rtstruct_builder.py | 35 ++++++++++++++++++++ 4 files changed, 130 insertions(+), 3 deletions(-) diff --git a/rt_utils/ds_helper.py b/rt_utils/ds_helper.py index 75e18b0..67fceb7 100644 --- a/rt_utils/ds_helper.py +++ b/rt_utils/ds_helper.py @@ -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 @@ -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 @@ -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 diff --git a/rt_utils/rtstruct.py b/rt_utils/rtstruct.py index dfe82be..4ce4749 100644 --- a/rt_utils/rtstruct.py +++ b/rt_utils/rtstruct.py @@ -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( diff --git a/rt_utils/utils.py b/rt_utils/utils.py index f04089e..229a7c7 100644 --- a/rt_utils/utils.py +++ b/rt_utils/utils.py @@ -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 @@ -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 diff --git a/tests/test_rtstruct_builder.py b/tests/test_rtstruct_builder.py index 3be6b90..8835f6c 100644 --- a/tests/test_rtstruct_builder.py +++ b/tests/test_rtstruct_builder.py @@ -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): From 382da0935626b12b324832a63c506884436113d0 Mon Sep 17 00:00:00 2001 From: gacou54 Date: Wed, 21 Feb 2024 08:20:10 -0500 Subject: [PATCH 03/10] Simplify algorithm to create contour from coordinates --- rt_utils/ds_helper.py | 47 ++++++++++++------------------------------- rt_utils/utils.py | 10 ++++++++- 2 files changed, 22 insertions(+), 35 deletions(-) diff --git a/rt_utils/ds_helper.py b/rt_utils/ds_helper.py index 67fceb7..6c38b06 100644 --- a/rt_utils/ds_helper.py +++ b/rt_utils/ds_helper.py @@ -1,5 +1,5 @@ import datetime -from typing import List +from typing import List, Union from rt_utils.image_helper import get_contours_coords from rt_utils.utils import ROIData, SOPClassUID @@ -9,6 +9,8 @@ from pydicom.sequence import Sequence from pydicom.uid import ImplicitVRLittleEndian +from utils import _flatten_lists + """ File contains helper methods that handles DICOM header creation/formatting """ @@ -177,7 +179,7 @@ def create_contour_sequence(roi_data: ROIData, series_data: List[Dataset]) -> Se return contour_sequence -def create_roi_contour_from_coordinates(coordinates: List[List[float]], roi_data: ROIData, series_data) -> Dataset: +def create_roi_contour_from_coordinates(coordinates: List[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) @@ -186,54 +188,27 @@ def create_roi_contour_from_coordinates(coordinates: List[List[float]], roi_data 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 = {} + contour_sequence = Sequence() 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) + contour_data = _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 + slice_contour = create_contour(closest_slice, contour_data) + contour_sequence.append(slice_contour) return contour_sequence -def create_contour(series_slice: Dataset, contour_data: np.ndarray) -> Dataset: +def create_contour(series_slice: Dataset, contour_data: Union[np.ndarray, List[float]]) -> Dataset: contour_image = Dataset() contour_image.ReferencedSOPClassUID = series_slice.SOPClassUID contour_image.ReferencedSOPInstanceUID = series_slice.SOPInstanceUID @@ -280,3 +255,7 @@ def get_contour_sequence_by_roi_number(ds, roi_number): return Sequence() raise Exception(f"Referenced ROI number '{roi_number}' not found") + + +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)) diff --git a/rt_utils/utils.py b/rt_utils/utils.py index 229a7c7..e2ca56e 100644 --- a/rt_utils/utils.py +++ b/rt_utils/utils.py @@ -1,5 +1,4 @@ from typing import List, Union -from random import randrange import numpy as np from pydicom.uid import PYDICOM_IMPLEMENTATION_UID @@ -126,3 +125,12 @@ def validate_roi_generation_algoirthm(self): type(self.roi_generation_algorithm) ) ) + + +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 From e88c8e82170972bd715f1ab5a30c52e80ce56217 Mon Sep 17 00:00:00 2001 From: gacou54 Date: Wed, 21 Feb 2024 09:50:20 -0500 Subject: [PATCH 04/10] Fix import from rt_utils.utils --- rt_utils/ds_helper.py | 2 +- rt_utils/image_helper.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/rt_utils/ds_helper.py b/rt_utils/ds_helper.py index 6c38b06..da0827d 100644 --- a/rt_utils/ds_helper.py +++ b/rt_utils/ds_helper.py @@ -9,7 +9,7 @@ from pydicom.sequence import Sequence from pydicom.uid import ImplicitVRLittleEndian -from utils import _flatten_lists +from rt_utils.utils import _flatten_lists """ File contains helper methods that handles DICOM header creation/formatting diff --git a/rt_utils/image_helper.py b/rt_utils/image_helper.py index 29b8745..e02df28 100644 --- a/rt_utils/image_helper.py +++ b/rt_utils/image_helper.py @@ -8,7 +8,7 @@ from pydicom.dataset import Dataset from pydicom.sequence import Sequence -from rt_utils.utils import ROIData, SOPClassUID +from rt_utils.utils import ROIData def load_sorted_image_series(dicom_series_path: str | List[Dataset]) -> List[Dataset]: From 08bc46e07200d11a14ebc929aa00a96f5b74c331 Mon Sep 17 00:00:00 2001 From: gacou54 Date: Wed, 21 Feb 2024 09:50:44 -0500 Subject: [PATCH 05/10] fix pip installation from folder and github --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 5d525ed..d4adbbe 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ long_description=long_description, long_description_content_type="text/markdown", url="https://github.com/qurit/rtutils", - package_dir={'':"rt_utils"}, + package_dir={'rt_utils': "rt_utils"}, packages=setuptools.find_packages("rt_utils", exclude="tests"), keywords=["RTStruct", "Dicom", "Pydicom"], classifiers=[ From e0be562d37b6cd6f366032c3abfff23f6fdea423 Mon Sep 17 00:00:00 2001 From: gacou54 Date: Mon, 4 Mar 2024 08:55:30 -0500 Subject: [PATCH 06/10] Add rtstruct.add_roi_from_coordinates() documentation in README --- README.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/README.md b/README.md index 7920fb5..c7cfc45 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,25 @@ rtstruct.add_roi( name="RT-Utils ROI!" ) +# Add another ROI from coordinates +rtstruct.add_roi_from_coordinates( + coordinates=[ + [ + # Example of One contour on one slice + [-20.0, -170.0, -559.0], + [30.0, -170.0, -559.0], + [30.0, -110.0, -559.0], + [-20.0, -110.0, -559.0], + ], + [ + [-20.0, -170.0, -562.4], + [30.0, -170.0, -562.4], + [30.0, -110.0, -562.4], + [-20.0, -110.0, -562.4], + ] + ] +) + rtstruct.save('new-rt-struct') ``` From be6e89cf97ad9322c5e2ad5bf4469f9c1491d5e7 Mon Sep 17 00:00:00 2001 From: Gabriel Couture Date: Mon, 8 Apr 2024 10:19:27 -0400 Subject: [PATCH 07/10] Fix type hint for python3.8 --- rt_utils/image_helper.py | 4 ++-- rt_utils/rtstruct_builder.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/rt_utils/image_helper.py b/rt_utils/image_helper.py index e02df28..2737c78 100644 --- a/rt_utils/image_helper.py +++ b/rt_utils/image_helper.py @@ -1,5 +1,5 @@ import os -from typing import List +from typing import List, Union from enum import IntEnum import cv2 as cv @@ -11,7 +11,7 @@ from rt_utils.utils import ROIData -def load_sorted_image_series(dicom_series_path: str | List[Dataset]) -> List[Dataset]: +def load_sorted_image_series(dicom_series_path: Union[str, List[Dataset]]) -> List[Dataset]: """ File contains helper methods for loading / formatting DICOM images and contours """ diff --git a/rt_utils/rtstruct_builder.py b/rt_utils/rtstruct_builder.py index 229e416..ae33301 100644 --- a/rt_utils/rtstruct_builder.py +++ b/rt_utils/rtstruct_builder.py @@ -1,4 +1,4 @@ -from typing import List +from typing import List, Union from pydicom.dataset import Dataset from pydicom.filereader import dcmread @@ -15,7 +15,7 @@ class RTStructBuilder: """ @staticmethod - def create_new(dicom_series_path: str | List[Dataset]) -> RTStruct: + def create_new(dicom_series_path: Union[str, List[Dataset]]) -> RTStruct: """ Method to generate a new rt struct from a DICOM series """ @@ -25,7 +25,7 @@ def create_new(dicom_series_path: str | List[Dataset]) -> RTStruct: return RTStruct(series_data, ds) @staticmethod - def create_from(dicom_series_path: str | List[Dataset], rt_struct_path: str, warn_only: bool = False) -> RTStruct: + def create_from(dicom_series_path: Union[str, List[Dataset]], rt_struct_path: str, warn_only: bool = False) -> RTStruct: """ Method to load an existing rt struct, given related DICOM series and existing rt struct """ From 58f8a6a66dac1e99c2b797be967976b6c4fb3f3f Mon Sep 17 00:00:00 2001 From: Gabriel Couture Date: Fri, 12 Apr 2024 10:36:28 -0400 Subject: [PATCH 08/10] Add print value when failing to read pixel array: --- rt_utils/image_helper.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/rt_utils/image_helper.py b/rt_utils/image_helper.py index 2737c78..9981119 100644 --- a/rt_utils/image_helper.py +++ b/rt_utils/image_helper.py @@ -34,12 +34,13 @@ def load_dcm_images_from_path(dicom_series_path: str) -> List[Dataset]: for root, _, files in os.walk(dicom_series_path): for file in files: try: - ds = dcmread(os.path.join(root, file)) + filepath = os.path.join(root, file) + ds = dcmread(filepath) if hasattr(ds, "pixel_array"): series_data.append(ds) - except Exception: - # Not a valid DICOM file + except Exception as e: + print(f'Failed to load pixel_array from {filepath}, error: {e}') continue return series_data From 0eab595bf5dd34e3f1db868d64c7d2a7e22816b1 Mon Sep 17 00:00:00 2001 From: Gabriel Couture Date: Mon, 22 Apr 2024 09:39:13 -0400 Subject: [PATCH 09/10] Use image position patient instead of slice location --- rt_utils/ds_helper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rt_utils/ds_helper.py b/rt_utils/ds_helper.py index da0827d..0654233 100644 --- a/rt_utils/ds_helper.py +++ b/rt_utils/ds_helper.py @@ -258,4 +258,4 @@ def get_contour_sequence_by_roi_number(ds, roi_number): 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)) + return min(series_slices, key=lambda series_slice: abs(series_slice.ImagePositionPatient[2] - z_coord)) From 277cf2489862801caa62e4b8b48dff03b06841db Mon Sep 17 00:00:00 2001 From: Gabriel Couture Date: Fri, 26 Apr 2024 09:54:39 -0400 Subject: [PATCH 10/10] Fix Series metadata (date/time/number/description) --- rt_utils/ds_helper.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/rt_utils/ds_helper.py b/rt_utils/ds_helper.py index 0654233..663e0d4 100644 --- a/rt_utils/ds_helper.py +++ b/rt_utils/ds_helper.py @@ -76,17 +76,18 @@ def add_sequence_lists_to_ds(ds: FileDataset): def add_study_and_series_information(ds: FileDataset, series_data): + current_date = datetime.datetime.now() reference_ds = series_data[0] # All elements in series should have the same data ds.StudyDate = reference_ds.StudyDate - ds.SeriesDate = getattr(reference_ds, "SeriesDate", "") + ds.SeriesDate = current_date.strftime("%Y%m%d") ds.StudyTime = reference_ds.StudyTime - ds.SeriesTime = getattr(reference_ds, "SeriesTime", "") + ds.SeriesTime = current_date.strftime("%H%M%S.%f") ds.StudyDescription = getattr(reference_ds, "StudyDescription", "") - ds.SeriesDescription = getattr(reference_ds, "SeriesDescription", "") + ds.SeriesDescription = f"Structures for CT SeriesInstanceUID={reference_ds.SeriesInstanceUID}" ds.StudyInstanceUID = reference_ds.StudyInstanceUID ds.SeriesInstanceUID = generate_uid() # TODO: find out if random generation is ok ds.StudyID = reference_ds.StudyID - ds.SeriesNumber = "1" # TODO: find out if we can just use 1 (Should be fine since its a new series) + ds.SeriesNumber = "" # TODO: find out if we can just use 1 (Should be fine since its a new series) def add_patient_information(ds: FileDataset, series_data):