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.
- {rt_utils-1.3.1/rt_utils.egg-info → rt_utils-1.4.1}/PKG-INFO +5 -11
- {rt_utils-1.3.1 → rt_utils-1.4.1}/README.md +3 -9
- {rt_utils-1.3.1 → rt_utils-1.4.1}/pyproject.toml +1 -1
- {rt_utils-1.3.1 → rt_utils-1.4.1}/rt_utils/_version.py +1 -1
- {rt_utils-1.3.1 → rt_utils-1.4.1}/rt_utils/ds_helper.py +17 -7
- {rt_utils-1.3.1 → rt_utils-1.4.1}/rt_utils/image_helper.py +24 -3
- {rt_utils-1.3.1 → rt_utils-1.4.1}/rt_utils/rtstruct.py +40 -4
- {rt_utils-1.3.1 → rt_utils-1.4.1/rt_utils.egg-info}/PKG-INFO +5 -11
- {rt_utils-1.3.1 → rt_utils-1.4.1}/rt_utils.egg-info/requires.txt +1 -1
- {rt_utils-1.3.1 → rt_utils-1.4.1}/LICENSE +0 -0
- {rt_utils-1.3.1 → rt_utils-1.4.1}/MANIFEST.in +0 -0
- {rt_utils-1.3.1 → rt_utils-1.4.1}/RELEASING.md +0 -0
- {rt_utils-1.3.1 → rt_utils-1.4.1}/rt_utils/__init__.py +0 -0
- {rt_utils-1.3.1 → rt_utils-1.4.1}/rt_utils/nifti2rt.py +0 -0
- {rt_utils-1.3.1 → rt_utils-1.4.1}/rt_utils/rtstruct_builder.py +0 -0
- {rt_utils-1.3.1 → rt_utils-1.4.1}/rt_utils/rtstruct_merger.py +0 -0
- {rt_utils-1.3.1 → rt_utils-1.4.1}/rt_utils/utils.py +0 -0
- {rt_utils-1.3.1 → rt_utils-1.4.1}/rt_utils.egg-info/SOURCES.txt +0 -0
- {rt_utils-1.3.1 → rt_utils-1.4.1}/rt_utils.egg-info/dependency_links.txt +0 -0
- {rt_utils-1.3.1 → rt_utils-1.4.1}/rt_utils.egg-info/top_level.txt +0 -0
- {rt_utils-1.3.1 → rt_utils-1.4.1}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rt-utils
|
|
3
|
-
Version: 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 .
|
|
@@ -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):
|
|
@@ -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 =
|
|
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
|
-
|
|
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
|
+
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 .
|
|
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
|