rt-utils 1.3.1__tar.gz → 1.4.1__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.1
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
@@ -33,7 +33,7 @@ Description-Content-Type: text/markdown
33
33
  License-File: LICENSE
34
34
  Requires-Dist: pydicom
35
35
  Requires-Dist: numpy
36
- Requires-Dist: opencv-python>=4.0.0
36
+ Requires-Dist: opencv-python-headless>=4.0.0
37
37
  Provides-Extra: nifti
38
38
  Requires-Dist: SimpleITK; extra == "nifti"
39
39
  Provides-Extra: dev
@@ -71,18 +71,12 @@ RT-Utils provides a builder class to faciliate the creation and loading of an RT
71
71
  The format for the ROI mask is an nd numpy array of type bool. It is an array of 2d binary masks, one plane for each slice location within the DICOM series. The slices should be sorted in ascending order within the mask. Through these masks, we extract the contours of the regions of interest and place them within the RT Struct file. Note that there is currently only support for the use of one frame of reference UID and structered set ROI sequence. Also note that holes within the ROI may be handled poorly.
72
72
 
73
73
  ## Installation
74
- ```
74
+ ```bash
75
75
  pip install rt_utils
76
76
  ```
77
- ##
78
- **libGL**
79
- libGL is a required system dependency. Install it on Linux using:
80
- ```
81
- ```bash
82
- sudo apt install libgl1
83
- ```
77
+
84
78
  ## Installation in editable mode
85
- ```
79
+ ```bash
86
80
  git clone https://github.com/qurit/rt-utils.git
87
81
  cd rt-utils
88
82
  pip install -e .
@@ -26,18 +26,12 @@ RT-Utils provides a builder class to faciliate the creation and loading of an RT
26
26
  The format for the ROI mask is an nd numpy array of type bool. It is an array of 2d binary masks, one plane for each slice location within the DICOM series. The slices should be sorted in ascending order within the mask. Through these masks, we extract the contours of the regions of interest and place them within the RT Struct file. Note that there is currently only support for the use of one frame of reference UID and structered set ROI sequence. Also note that holes within the ROI may be handled poorly.
27
27
 
28
28
  ## Installation
29
- ```
29
+ ```bash
30
30
  pip install rt_utils
31
31
  ```
32
- ##
33
- **libGL**
34
- libGL is a required system dependency. Install it on Linux using:
35
- ```
36
- ```bash
37
- sudo apt install libgl1
38
- ```
32
+
39
33
  ## Installation in editable mode
40
- ```
34
+ ```bash
41
35
  git clone https://github.com/qurit/rt-utils.git
42
36
  cd rt-utils
43
37
  pip install -e .
@@ -38,7 +38,7 @@ classifiers = [
38
38
  dependencies = [
39
39
  "pydicom",
40
40
  "numpy",
41
- "opencv-python>=4.0.0",
41
+ "opencv-python-headless>=4.0.0",
42
42
  ]
43
43
  dynamic = ["version"]
44
44
 
@@ -1,3 +1,3 @@
1
1
  """RT-Utils package version."""
2
2
 
3
- __version__ = "1.3.1"
3
+ __version__ = "1.4.1"
@@ -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):
@@ -23,6 +23,26 @@ class RTStruct:
23
23
  """
24
24
  self.ds.SeriesDescription = description
25
25
 
26
+ def _next_roi_number(self) -> int:
27
+ """Return an ROINumber not already used in this structure set.
28
+
29
+ Counting the sequence is not enough. An ROI deleted in another
30
+ application leaves a gap, so len(StructureSetROISequence) + 1 can
31
+ collide with an ROINumber that is still in use. ROINumber is the key
32
+ that ties StructureSetROISequence to ROIContourSequence, so a
33
+ duplicate silently associates two ROIs with the same contours.
34
+
35
+ DICOM numbers ROIs from 1, so an empty structure set yields 1.
36
+ """
37
+ used = []
38
+ for structure_roi in self.ds.StructureSetROISequence:
39
+ try:
40
+ used.append(int(structure_roi.ROINumber))
41
+ except (AttributeError, TypeError, ValueError):
42
+ continue
43
+
44
+ return max(used, default=0) + 1
45
+
26
46
  def add_roi(
27
47
  self,
28
48
  mask: np.ndarray,
@@ -83,7 +103,7 @@ class RTStruct:
83
103
  """
84
104
  # TODO: test if name already exists
85
105
  self.validate_mask(mask)
86
- roi_number = len(self.ds.StructureSetROISequence) + 1
106
+ roi_number = self._next_roi_number()
87
107
  roi_data = ROIData(
88
108
  mask,
89
109
  color,
@@ -139,13 +159,29 @@ class RTStruct:
139
159
  structure_roi.ROIName for structure_roi in self.ds.StructureSetROISequence
140
160
  ]
141
161
 
142
- def get_roi_mask_by_name(self, name) -> np.ndarray:
162
+ def get_roi_mask_by_name(self, name, case_sensitive: bool = True) -> np.ndarray:
143
163
  """
144
- Returns the 3D binary mask of the ROI with the given input name
164
+ Returns the 3D binary mask of the ROI with the given input name.
165
+
166
+ The name must match exactly by default. Pass case_sensitive=False to
167
+ match while ignoring case, which helps when ROI naming conventions
168
+ differ between treatment planning systems.
169
+
170
+ Matching exactly is the default because a structure set may contain
171
+ several ROIs whose names differ only in case, and those are distinct
172
+ structures. With case_sensitive=False the first one in
173
+ StructureSetROISequence wins and the others become unreachable.
145
174
  """
175
+ target = name
176
+ if not case_sensitive and isinstance(name, str):
177
+ target = name.casefold()
146
178
 
147
179
  for structure_roi in self.ds.StructureSetROISequence:
148
- if structure_roi.ROIName == name:
180
+ roi_name = structure_roi.ROIName
181
+ if not case_sensitive and isinstance(roi_name, str):
182
+ roi_name = roi_name.casefold()
183
+
184
+ if roi_name == target:
149
185
  contour_sequence = ds_helper.get_contour_sequence_by_roi_number(
150
186
  self.ds, structure_roi.ROINumber
151
187
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rt-utils
3
- Version: 1.3.1
3
+ Version: 1.4.1
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
@@ -33,7 +33,7 @@ Description-Content-Type: text/markdown
33
33
  License-File: LICENSE
34
34
  Requires-Dist: pydicom
35
35
  Requires-Dist: numpy
36
- Requires-Dist: opencv-python>=4.0.0
36
+ Requires-Dist: opencv-python-headless>=4.0.0
37
37
  Provides-Extra: nifti
38
38
  Requires-Dist: SimpleITK; extra == "nifti"
39
39
  Provides-Extra: dev
@@ -71,18 +71,12 @@ RT-Utils provides a builder class to faciliate the creation and loading of an RT
71
71
  The format for the ROI mask is an nd numpy array of type bool. It is an array of 2d binary masks, one plane for each slice location within the DICOM series. The slices should be sorted in ascending order within the mask. Through these masks, we extract the contours of the regions of interest and place them within the RT Struct file. Note that there is currently only support for the use of one frame of reference UID and structered set ROI sequence. Also note that holes within the ROI may be handled poorly.
72
72
 
73
73
  ## Installation
74
- ```
74
+ ```bash
75
75
  pip install rt_utils
76
76
  ```
77
- ##
78
- **libGL**
79
- libGL is a required system dependency. Install it on Linux using:
80
- ```
81
- ```bash
82
- sudo apt install libgl1
83
- ```
77
+
84
78
  ## Installation in editable mode
85
- ```
79
+ ```bash
86
80
  git clone https://github.com/qurit/rt-utils.git
87
81
  cd rt-utils
88
82
  pip install -e .
@@ -1,6 +1,6 @@
1
1
  pydicom
2
2
  numpy
3
- opencv-python>=4.0.0
3
+ opencv-python-headless>=4.0.0
4
4
 
5
5
  [dev]
6
6
  pytest
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes