rt-utils 1.3.1__tar.gz → 1.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {rt_utils-1.3.1/rt_utils.egg-info → rt_utils-1.4.0}/PKG-INFO +1 -1
- {rt_utils-1.3.1 → rt_utils-1.4.0}/rt_utils/_version.py +1 -1
- {rt_utils-1.3.1 → rt_utils-1.4.0}/rt_utils/ds_helper.py +17 -7
- {rt_utils-1.3.1 → rt_utils-1.4.0}/rt_utils/image_helper.py +24 -3
- {rt_utils-1.3.1 → rt_utils-1.4.0}/rt_utils/rtstruct.py +19 -3
- {rt_utils-1.3.1 → rt_utils-1.4.0/rt_utils.egg-info}/PKG-INFO +1 -1
- {rt_utils-1.3.1 → rt_utils-1.4.0}/LICENSE +0 -0
- {rt_utils-1.3.1 → rt_utils-1.4.0}/MANIFEST.in +0 -0
- {rt_utils-1.3.1 → rt_utils-1.4.0}/README.md +0 -0
- {rt_utils-1.3.1 → rt_utils-1.4.0}/RELEASING.md +0 -0
- {rt_utils-1.3.1 → rt_utils-1.4.0}/pyproject.toml +0 -0
- {rt_utils-1.3.1 → rt_utils-1.4.0}/rt_utils/__init__.py +0 -0
- {rt_utils-1.3.1 → rt_utils-1.4.0}/rt_utils/nifti2rt.py +0 -0
- {rt_utils-1.3.1 → rt_utils-1.4.0}/rt_utils/rtstruct_builder.py +0 -0
- {rt_utils-1.3.1 → rt_utils-1.4.0}/rt_utils/rtstruct_merger.py +0 -0
- {rt_utils-1.3.1 → rt_utils-1.4.0}/rt_utils/utils.py +0 -0
- {rt_utils-1.3.1 → rt_utils-1.4.0}/rt_utils.egg-info/SOURCES.txt +0 -0
- {rt_utils-1.3.1 → rt_utils-1.4.0}/rt_utils.egg-info/dependency_links.txt +0 -0
- {rt_utils-1.3.1 → rt_utils-1.4.0}/rt_utils.egg-info/requires.txt +0 -0
- {rt_utils-1.3.1 → rt_utils-1.4.0}/rt_utils.egg-info/top_level.txt +0 -0
- {rt_utils-1.3.1 → rt_utils-1.4.0}/setup.cfg +0 -0
|
@@ -168,9 +168,11 @@ def create_contour_sequence(roi_data: ROIData, series_data) -> Sequence:
|
|
|
168
168
|
contours_coords = get_contours_coords(roi_data, series_data)
|
|
169
169
|
|
|
170
170
|
for series_slice, slice_contours in zip(series_data, contours_coords):
|
|
171
|
+
|
|
171
172
|
for contour_data in slice_contours:
|
|
172
173
|
contour = create_contour(series_slice, contour_data)
|
|
173
|
-
|
|
174
|
+
if (contour.ContourGeometricType == 'CLOSED_PLANAR' and contour.NumberOfContourPoints > 2) or contour.ContourGeometricType != 'CLOSED_PLANAR':
|
|
175
|
+
contour_sequence.append(contour)
|
|
174
176
|
|
|
175
177
|
return contour_sequence
|
|
176
178
|
|
|
@@ -184,14 +186,22 @@ def create_contour(series_slice: Dataset, contour_data: np.ndarray) -> Dataset:
|
|
|
184
186
|
contour_image_sequence = Sequence()
|
|
185
187
|
contour_image_sequence.append(contour_image)
|
|
186
188
|
|
|
189
|
+
number_of_points = len(contour_data) // 3 # Each point has an x, y and z value
|
|
190
|
+
|
|
191
|
+
# PS3.3 C.8.8.6.1: CLOSED_PLANAR requires at least three points. A contour
|
|
192
|
+
# of one or two points is a POINT or an OPEN_PLANAR, and labelling it
|
|
193
|
+
# CLOSED_PLANAR produces a dataset some treatment planning systems reject.
|
|
194
|
+
if number_of_points == 1:
|
|
195
|
+
geometric_type = "POINT"
|
|
196
|
+
elif number_of_points == 2:
|
|
197
|
+
geometric_type = "OPEN_PLANAR"
|
|
198
|
+
else:
|
|
199
|
+
geometric_type = "CLOSED_PLANAR"
|
|
200
|
+
|
|
187
201
|
contour = Dataset()
|
|
188
202
|
contour.ContourImageSequence = contour_image_sequence
|
|
189
|
-
contour.ContourGeometricType =
|
|
190
|
-
|
|
191
|
-
)
|
|
192
|
-
contour.NumberOfContourPoints = (
|
|
193
|
-
len(contour_data) / 3
|
|
194
|
-
) # Each point has an x, y, and z value
|
|
203
|
+
contour.ContourGeometricType = geometric_type
|
|
204
|
+
contour.NumberOfContourPoints = number_of_points
|
|
195
205
|
|
|
196
206
|
# Rounds ContourData to 10 decimal places to ensure it is <16 bytes length, as per NEMA DICOM standard guidelines.
|
|
197
207
|
contour.ContourData = [round(val, 10) for val in contour_data]
|
|
@@ -370,15 +370,36 @@ def get_slice_mask_from_slice_contour_data(
|
|
|
370
370
|
):
|
|
371
371
|
# Go through all contours in a slice, create polygons in correct space and with a correct format
|
|
372
372
|
# and append to polygons array (appropriate for fillPoly)
|
|
373
|
+
slice_mask = create_empty_slice_mask(series_slice).astype(np.uint8)
|
|
373
374
|
polygons = []
|
|
375
|
+
|
|
374
376
|
for contour_coords in slice_contour_data:
|
|
375
377
|
reshaped_contour_data = np.reshape(contour_coords, [len(contour_coords) // 3, 3])
|
|
376
378
|
translated_contour_data = apply_transformation_to_3d_points(reshaped_contour_data, transformation_matrix)
|
|
377
379
|
polygon = [np.around([translated_contour_data[:, :2]]).astype(np.int32)]
|
|
378
380
|
polygon = np.array(polygon).squeeze()
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
381
|
+
|
|
382
|
+
num_points = len(reshaped_contour_data)
|
|
383
|
+
|
|
384
|
+
if num_points == 1:
|
|
385
|
+
# Single point contour - set exactly the one voxel it names.
|
|
386
|
+
# radius=0 paints a single pixel; radius=1 would paint five and
|
|
387
|
+
# inflate the smallest ROIs (fiducials, seeds, reference points).
|
|
388
|
+
point = tuple(polygon.flatten()[:2])
|
|
389
|
+
cv.circle(slice_mask, point, radius=0, color=1, thickness=-1)
|
|
390
|
+
elif num_points == 2:
|
|
391
|
+
# Two point contour - draw a line
|
|
392
|
+
pt1 = tuple(polygon[0])
|
|
393
|
+
pt2 = tuple(polygon[1])
|
|
394
|
+
cv.line(slice_mask, pt1, pt2, color=1, thickness=1)
|
|
395
|
+
else:
|
|
396
|
+
# Three or more points - use fillPoly
|
|
397
|
+
polygons.append(polygon)
|
|
398
|
+
|
|
399
|
+
# Fill all valid polygons (3+ points) at once
|
|
400
|
+
if polygons:
|
|
401
|
+
cv.fillPoly(img=slice_mask, pts=polygons, color=1)
|
|
402
|
+
|
|
382
403
|
return slice_mask
|
|
383
404
|
|
|
384
405
|
def create_empty_series_mask(series_data):
|
|
@@ -139,13 +139,29 @@ class RTStruct:
|
|
|
139
139
|
structure_roi.ROIName for structure_roi in self.ds.StructureSetROISequence
|
|
140
140
|
]
|
|
141
141
|
|
|
142
|
-
def get_roi_mask_by_name(self, name) -> np.ndarray:
|
|
142
|
+
def get_roi_mask_by_name(self, name, case_sensitive: bool = True) -> np.ndarray:
|
|
143
143
|
"""
|
|
144
|
-
Returns the 3D binary mask of the ROI with the given input name
|
|
144
|
+
Returns the 3D binary mask of the ROI with the given input name.
|
|
145
|
+
|
|
146
|
+
The name must match exactly by default. Pass case_sensitive=False to
|
|
147
|
+
match while ignoring case, which helps when ROI naming conventions
|
|
148
|
+
differ between treatment planning systems.
|
|
149
|
+
|
|
150
|
+
Matching exactly is the default because a structure set may contain
|
|
151
|
+
several ROIs whose names differ only in case, and those are distinct
|
|
152
|
+
structures. With case_sensitive=False the first one in
|
|
153
|
+
StructureSetROISequence wins and the others become unreachable.
|
|
145
154
|
"""
|
|
155
|
+
target = name
|
|
156
|
+
if not case_sensitive and isinstance(name, str):
|
|
157
|
+
target = name.casefold()
|
|
146
158
|
|
|
147
159
|
for structure_roi in self.ds.StructureSetROISequence:
|
|
148
|
-
|
|
160
|
+
roi_name = structure_roi.ROIName
|
|
161
|
+
if not case_sensitive and isinstance(roi_name, str):
|
|
162
|
+
roi_name = roi_name.casefold()
|
|
163
|
+
|
|
164
|
+
if roi_name == target:
|
|
149
165
|
contour_sequence = ds_helper.get_contour_sequence_by_roi_number(
|
|
150
166
|
self.ds, structure_roi.ROINumber
|
|
151
167
|
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|