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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rt-utils
3
- Version: 1.3.1
3
+ Version: 1.4.0
4
4
  Summary: A small library for handling masks and RT-Structs
5
5
  Author-email: Asim Shrestha <asim.shrestha@hotmail.com>
6
6
  License-Expression: MIT
@@ -1,3 +1,3 @@
1
1
  """RT-Utils package version."""
2
2
 
3
- __version__ = "1.3.1"
3
+ __version__ = "1.4.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
- contour_sequence.append(contour)
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
- "CLOSED_PLANAR" # TODO figure out how to get this value
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
- polygons.append(polygon)
380
- slice_mask = create_empty_slice_mask(series_slice).astype(np.uint8)
381
- cv.fillPoly(img=slice_mask, pts = polygons, color = 1)
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
- if structure_roi.ROIName == name:
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
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rt-utils
3
- Version: 1.3.1
3
+ Version: 1.4.0
4
4
  Summary: A small library for handling masks and RT-Structs
5
5
  Author-email: Asim Shrestha <asim.shrestha@hotmail.com>
6
6
  License-Expression: MIT
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