mediml 0.9.9__py3-none-any.whl

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.
Files changed (78) hide show
  1. MEDiml/MEDscan.py +1696 -0
  2. MEDiml/__init__.py +21 -0
  3. MEDiml/biomarkers/BatchExtractor.py +806 -0
  4. MEDiml/biomarkers/BatchExtractorTexturalFilters.py +840 -0
  5. MEDiml/biomarkers/__init__.py +16 -0
  6. MEDiml/biomarkers/diagnostics.py +125 -0
  7. MEDiml/biomarkers/get_oriented_bound_box.py +158 -0
  8. MEDiml/biomarkers/glcm.py +1602 -0
  9. MEDiml/biomarkers/gldzm.py +523 -0
  10. MEDiml/biomarkers/glrlm.py +1315 -0
  11. MEDiml/biomarkers/glszm.py +555 -0
  12. MEDiml/biomarkers/int_vol_hist.py +527 -0
  13. MEDiml/biomarkers/intensity_histogram.py +615 -0
  14. MEDiml/biomarkers/local_intensity.py +89 -0
  15. MEDiml/biomarkers/morph.py +1756 -0
  16. MEDiml/biomarkers/ngldm.py +780 -0
  17. MEDiml/biomarkers/ngtdm.py +414 -0
  18. MEDiml/biomarkers/stats.py +373 -0
  19. MEDiml/biomarkers/utils.py +389 -0
  20. MEDiml/filters/TexturalFilter.py +299 -0
  21. MEDiml/filters/__init__.py +9 -0
  22. MEDiml/filters/apply_filter.py +134 -0
  23. MEDiml/filters/gabor.py +215 -0
  24. MEDiml/filters/laws.py +283 -0
  25. MEDiml/filters/log.py +147 -0
  26. MEDiml/filters/mean.py +121 -0
  27. MEDiml/filters/textural_filters_kernels.py +1738 -0
  28. MEDiml/filters/utils.py +107 -0
  29. MEDiml/filters/wavelet.py +237 -0
  30. MEDiml/learning/DataCleaner.py +198 -0
  31. MEDiml/learning/DesignExperiment.py +480 -0
  32. MEDiml/learning/FSR.py +667 -0
  33. MEDiml/learning/Normalization.py +112 -0
  34. MEDiml/learning/RadiomicsLearner.py +714 -0
  35. MEDiml/learning/Results.py +2237 -0
  36. MEDiml/learning/Stats.py +694 -0
  37. MEDiml/learning/__init__.py +10 -0
  38. MEDiml/learning/cleaning_utils.py +107 -0
  39. MEDiml/learning/ml_utils.py +1015 -0
  40. MEDiml/processing/__init__.py +6 -0
  41. MEDiml/processing/compute_suv_map.py +121 -0
  42. MEDiml/processing/discretisation.py +149 -0
  43. MEDiml/processing/interpolation.py +275 -0
  44. MEDiml/processing/resegmentation.py +66 -0
  45. MEDiml/processing/segmentation.py +912 -0
  46. MEDiml/utils/__init__.py +25 -0
  47. MEDiml/utils/batch_patients.py +45 -0
  48. MEDiml/utils/create_radiomics_table.py +131 -0
  49. MEDiml/utils/data_frame_export.py +42 -0
  50. MEDiml/utils/find_process_names.py +16 -0
  51. MEDiml/utils/get_file_paths.py +34 -0
  52. MEDiml/utils/get_full_rad_names.py +21 -0
  53. MEDiml/utils/get_institutions_from_ids.py +16 -0
  54. MEDiml/utils/get_patient_id_from_scan_name.py +22 -0
  55. MEDiml/utils/get_patient_names.py +26 -0
  56. MEDiml/utils/get_radiomic_names.py +27 -0
  57. MEDiml/utils/get_scan_name_from_rad_name.py +22 -0
  58. MEDiml/utils/image_reader_SITK.py +37 -0
  59. MEDiml/utils/image_volume_obj.py +22 -0
  60. MEDiml/utils/imref.py +340 -0
  61. MEDiml/utils/initialize_features_names.py +62 -0
  62. MEDiml/utils/inpolygon.py +159 -0
  63. MEDiml/utils/interp3.py +43 -0
  64. MEDiml/utils/json_utils.py +78 -0
  65. MEDiml/utils/mode.py +31 -0
  66. MEDiml/utils/parse_contour_string.py +58 -0
  67. MEDiml/utils/save_MEDscan.py +30 -0
  68. MEDiml/utils/strfind.py +32 -0
  69. MEDiml/utils/textureTools.py +188 -0
  70. MEDiml/utils/texture_features_names.py +115 -0
  71. MEDiml/utils/write_radiomics_csv.py +47 -0
  72. MEDiml/wrangling/DataManager.py +1724 -0
  73. MEDiml/wrangling/ProcessDICOM.py +512 -0
  74. MEDiml/wrangling/__init__.py +3 -0
  75. mediml-0.9.9.dist-info/LICENSE.md +674 -0
  76. mediml-0.9.9.dist-info/METADATA +232 -0
  77. mediml-0.9.9.dist-info/RECORD +78 -0
  78. mediml-0.9.9.dist-info/WHEEL +4 -0
@@ -0,0 +1,512 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ import sys
5
+ import warnings
6
+ from typing import List, Union
7
+
8
+ import numpy as np
9
+ import pydicom
10
+ import ray
11
+
12
+ from ..utils.imref import imref3d
13
+
14
+ warnings.simplefilter("ignore")
15
+
16
+ from pathlib import Path
17
+
18
+ from ..MEDscan import MEDscan
19
+ from ..processing.segmentation import get_roi
20
+ from ..utils.save_MEDscan import save_MEDscan
21
+
22
+
23
+ class ProcessDICOM():
24
+ """
25
+ Class to process dicom files and extract imaging volume and 3D masks from it
26
+ in order to oganize the data in a MEDscan class object.
27
+ """
28
+
29
+ def __init__(
30
+ self,
31
+ path_images: List[Path],
32
+ path_rs: List[Path],
33
+ path_save: Union[str, Path],
34
+ save: bool) -> None:
35
+ """
36
+ Args:
37
+ path_images (List[Path]): List of paths to the dicom files of a single scan.
38
+ path_rs (List[Path]): List of paths to the RT struct dicom files for the same scan.
39
+ path_save (Union[str, Path]): Path to the folder where the MEDscan object will be saved.
40
+ save (bool): Whether to save the MEDscan object or not.
41
+
42
+ Returns:
43
+ None.
44
+ """
45
+ self.path_images = path_images
46
+ self.path_rs = path_rs
47
+ self.path_save = Path(path_save) if path_save is str else path_save
48
+ self.save = save
49
+
50
+ def __get_dicom_scan_orientation(self, dicom_header: List[pydicom.dataset.FileDataset]) -> str:
51
+ """
52
+ Get the orientation of the scan.
53
+
54
+ Args:
55
+ dicom_header (List[pydicom.dataset.FileDataset]): List of dicom headers.
56
+
57
+ Returns:
58
+ str: Orientation of the scan.
59
+ """
60
+ n_slices = len(dicom_header)
61
+ image_patient_positions_x = [dicom_header[i].ImagePositionPatient[0] for i in range(n_slices)]
62
+ image_patient_positions_y = [dicom_header[i].ImagePositionPatient[1] for i in range(n_slices)]
63
+ image_patient_positions_z = [dicom_header[i].ImagePositionPatient[2] for i in range(n_slices)]
64
+ dist = [
65
+ np.median(np.abs(np.diff(image_patient_positions_x))),
66
+ np.median(np.abs(np.diff(image_patient_positions_y))),
67
+ np.median(np.abs(np.diff(image_patient_positions_z)))
68
+ ]
69
+ index = dist.index(max(dist))
70
+ if index == 0:
71
+ orientation = 'Sagittal'
72
+ elif index == 1:
73
+ orientation = 'Coronal'
74
+ else:
75
+ orientation = 'Axial'
76
+
77
+ return orientation
78
+
79
+ def __merge_slice_pixel_arrays(self, slice_datasets):
80
+ first_dataset = slice_datasets[0]
81
+ num_rows = first_dataset.Rows
82
+ num_columns = first_dataset.Columns
83
+ num_slices = len(slice_datasets)
84
+
85
+ sorted_slice_datasets = self.__sort_by_slice_spacing(slice_datasets)
86
+
87
+ if any(self.__requires_rescaling(d) for d in sorted_slice_datasets):
88
+ voxels = np.empty(
89
+ (num_columns, num_rows, num_slices), dtype=np.float32)
90
+ for k, dataset in enumerate(sorted_slice_datasets):
91
+ slope = float(getattr(dataset, 'RescaleSlope', 1))
92
+ intercept = float(getattr(dataset, 'RescaleIntercept', 0))
93
+ voxels[:, :, k] = dataset.pixel_array.T.astype(
94
+ np.float32)*slope + intercept
95
+ else:
96
+ dtype = first_dataset.pixel_array.dtype
97
+ voxels = np.empty((num_columns, num_rows, num_slices), dtype=dtype)
98
+ for k, dataset in enumerate(sorted_slice_datasets):
99
+ voxels[:, :, k] = dataset.pixel_array.T
100
+
101
+ return voxels
102
+
103
+ def __requires_rescaling(self, dataset):
104
+ return hasattr(dataset, 'RescaleSlope') or hasattr(dataset, 'RescaleIntercept')
105
+
106
+ def __ijk_to_patient_xyz_transform_matrix(self, slice_datasets):
107
+ first_dataset = self.__sort_by_slice_spacing(slice_datasets)[0]
108
+ image_orientation = first_dataset.ImageOrientationPatient
109
+ row_cosine, column_cosine, slice_cosine = self.__extract_cosines(
110
+ image_orientation)
111
+
112
+ row_spacing, column_spacing = first_dataset.PixelSpacing
113
+ slice_spacing = self.__slice_spacing(slice_datasets)
114
+
115
+ transform = np.identity(4, dtype=np.float32)
116
+ rotation = np.identity(3, dtype=np.float32)
117
+ scaling = np.identity(3, dtype=np.float32)
118
+
119
+ transform[:3, 0] = row_cosine*column_spacing
120
+ transform[:3, 1] = column_cosine*row_spacing
121
+ transform[:3, 2] = slice_cosine*slice_spacing
122
+
123
+ transform[:3, 3] = first_dataset.ImagePositionPatient
124
+
125
+ rotation[:3, 0] = row_cosine
126
+ rotation[:3, 1] = column_cosine
127
+ rotation[:3, 2] = slice_cosine
128
+
129
+ rotation = np.transpose(rotation)
130
+
131
+ scaling[0, 0] = column_spacing
132
+ scaling[1, 1] = row_spacing
133
+ scaling[2, 2] = slice_spacing
134
+
135
+ return transform, rotation, scaling
136
+
137
+ def __validate_slices_form_uniform_grid(self, slice_datasets):
138
+ """
139
+ Perform various data checks to ensure that the list of slices form a
140
+ evenly-spaced grid of data.
141
+ Some of these checks are probably not required if the data follows the
142
+ DICOM specification, however it seems pertinent to check anyway.
143
+ """
144
+ invariant_properties = [
145
+ 'Modality',
146
+ 'SOPClassUID',
147
+ 'SeriesInstanceUID',
148
+ 'Rows',
149
+ 'Columns',
150
+ 'ImageOrientationPatient',
151
+ 'PixelSpacing',
152
+ 'PixelRepresentation',
153
+ 'BitsAllocated',
154
+ 'BitsStored',
155
+ 'HighBit',
156
+ ]
157
+
158
+ for property_name in invariant_properties:
159
+ self.__slice_attribute_equal(slice_datasets, property_name)
160
+
161
+ self.__validate_image_orientation(slice_datasets[0].ImageOrientationPatient)
162
+
163
+ slice_positions = self.__slice_positions(slice_datasets)
164
+ self.__check_for_missing_slices(slice_positions)
165
+
166
+ def __validate_image_orientation(self, image_orientation):
167
+ """
168
+ Ensure that the image orientation is supported
169
+ - The direction cosines have magnitudes of 1 (just in case)
170
+ - The direction cosines are perpendicular
171
+ """
172
+
173
+ row_cosine, column_cosine, slice_cosine = self.__extract_cosines(
174
+ image_orientation)
175
+
176
+ if not self.__almost_zero(np.dot(row_cosine, column_cosine), 1e-4):
177
+ raise ValueError(
178
+ "Non-orthogonal direction cosines: {}, {}".format(row_cosine, column_cosine))
179
+ elif not self.__almost_zero(np.dot(row_cosine, column_cosine), 1e-8):
180
+ warnings.warn("Direction cosines aren't quite orthogonal: {}, {}".format(
181
+ row_cosine, column_cosine))
182
+
183
+ if not self.__almost_one(np.linalg.norm(row_cosine), 1e-4):
184
+ raise ValueError(
185
+ "The row direction cosine's magnitude is not 1: {}".format(row_cosine))
186
+ elif not self.__almost_one(np.linalg.norm(row_cosine), 1e-8):
187
+ warnings.warn(
188
+ "The row direction cosine's magnitude is not quite 1: {}".format(row_cosine))
189
+
190
+ if not self.__almost_one(np.linalg.norm(column_cosine), 1e-4):
191
+ raise ValueError(
192
+ "The column direction cosine's magnitude is not 1: {}".format(column_cosine))
193
+ elif not self.__almost_one(np.linalg.norm(column_cosine), 1e-8):
194
+ warnings.warn(
195
+ "The column direction cosine's magnitude is not quite 1: {}".format(column_cosine))
196
+ sys.stderr.flush()
197
+
198
+ def __is_close(self, a, b, rel_tol=1e-9, abs_tol=0.0):
199
+ return abs(a-b) <= max(rel_tol*max(abs(a), abs(b)), abs_tol)
200
+
201
+ def __almost_zero(self, value, abs_tol):
202
+ return self.__is_close(value, 0.0, abs_tol=abs_tol)
203
+
204
+ def __almost_one(self, value, abs_tol):
205
+ return self.__is_close(value, 1.0, abs_tol=abs_tol)
206
+
207
+ def __extract_cosines(self, image_orientation):
208
+ row_cosine = np.array(image_orientation[:3])
209
+ column_cosine = np.array(image_orientation[3:])
210
+ slice_cosine = np.cross(row_cosine, column_cosine)
211
+ return row_cosine, column_cosine, slice_cosine
212
+
213
+ def __slice_attribute_equal(self, slice_datasets, property_name):
214
+ initial_value = getattr(slice_datasets[0], property_name, None)
215
+ for slice_idx, dataset in enumerate(slice_datasets[1:]):
216
+ value = getattr(dataset, property_name, None)
217
+ if value != initial_value:
218
+ msg = f'Slice {slice_idx+1} have different value for {property_name}: {value} != {initial_value}'
219
+ warnings.warn(msg)
220
+
221
+ def __slice_positions(self, slice_datasets):
222
+ image_orientation = slice_datasets[0].ImageOrientationPatient
223
+ row_cosine, column_cosine, slice_cosine = self.__extract_cosines(
224
+ image_orientation)
225
+ return [np.dot(slice_cosine, d.ImagePositionPatient) for d in slice_datasets]
226
+
227
+ def __check_for_missing_slices(self, slice_positions):
228
+ slice_positions_diffs = np.diff(sorted(slice_positions))
229
+ if not np.allclose(slice_positions_diffs, slice_positions_diffs[0], atol=0, rtol=1e-5):
230
+ msg = "The slice spacing is non-uniform. Slice spacings:\n{}"
231
+ warnings.warn(msg.format(slice_positions_diffs))
232
+ sys.stderr.flush()
233
+ if not np.allclose(slice_positions_diffs, slice_positions_diffs[0], atol=0, rtol=1e-1):
234
+ raise ValueError('The slice spacing is non-uniform. It appears there are extra slices from another scan')
235
+
236
+ def __slice_spacing(self, slice_datasets):
237
+ if len(slice_datasets) > 1:
238
+ slice_positions = self.__slice_positions(slice_datasets)
239
+ slice_positions_diffs = np.diff(sorted(slice_positions))
240
+ return np.mean(slice_positions_diffs)
241
+
242
+ return 0.0
243
+
244
+ def __sort_by_slice_spacing(self, slice_datasets):
245
+ slice_spacing = self.__slice_positions(slice_datasets)
246
+ return [d for (s, d) in sorted(zip(slice_spacing, slice_datasets))]
247
+
248
+ def combine_slices(self, slice_datasets: List[pydicom.dataset.FileDataset]) -> List[np.ndarray]:
249
+ """
250
+ Given a list of pydicom datasets for an image series, stitch them together into a
251
+ three-dimensional numpy array of iamging data. Also calculate a 4x4 affine transformation
252
+ matrix that converts the ijk-pixel-indices into the xyz-coordinates in the
253
+ DICOM patient's coordinate system and 4x4 rotation and scaling matrix.
254
+ If any of the DICOM images contain either the
255
+ `Rescale Slope <https://dicom.innolitics.com/ciods/ct-image/ct-image/00281053>`__ or the
256
+ `Rescale Intercept <https://dicom.innolitics.com/ciods/ct-image/ct-image/00281052>`__
257
+ attributes they will be applied to each slice individually.
258
+ This function requires that the datasets:
259
+
260
+ - Be in same series (have the same
261
+ `Series Instance UID <https://dicom.innolitics.com/ciods/ct-image/general-series/0020000e>`__,
262
+ `Modality <https://dicom.innolitics.com/ciods/ct-image/general-series/00080060>`__,
263
+ and `SOP Class UID <https://dicom.innolitics.com/ciods/ct-image/sop-common/00080016>`__).
264
+ - The binary storage of each slice must be the same (have the same
265
+ `Bits Allocated <https://dicom.innolitics.com/ciods/ct-image/image-pixel/00280100>`__,
266
+ `Bits Stored <https://dicom.innolitics.com/ciods/ct-image/image-pixel/00280101>`__,
267
+ `High Bit <https://dicom.innolitics.com/ciods/ct-image/image-pixel/00280102>`__, and
268
+ `Pixel Representation <https://dicom.innolitics.com/ciods/ct-image/image-pixel/00280103>`__).
269
+ - The image slice must approximately form a grid. This means there can not
270
+ be any missing internal slices (missing slices on the ends of the dataset
271
+ are not detected). It also means that each slice must have the same
272
+ `Rows <https://dicom.innolitics.com/ciods/ct-image/image-pixel/00280010>`__,
273
+ `Columns <https://dicom.innolitics.com/ciods/ct-image/image-pixel/00280011>`__,
274
+ `Pixel Spacing <https://dicom.innolitics.com/ciods/ct-image/image-plane/00280030>`__, and
275
+ `Image Orientation (Patient) <https://dicom.innolitics.com/ciods/ct-image/image-plane/00200037>`__
276
+ attribute values.
277
+ - The direction cosines derived from the
278
+ `Image Orientation (Patient) <https://dicom.innolitics.com/ciods/ct-image/image-plane/00200037>`__
279
+ attribute must, within 1e-4, have a magnitude of 1. The cosines must
280
+ also be approximately perpendicular (their dot-product must be within
281
+ 1e-4 of 0). Warnings are displayed if any of theseapproximations are
282
+ below 1e-8, however, since we have seen real datasets with values up to
283
+ 1e-4, we let them pass.
284
+ - The `Image Position (Patient) <https://dicom.innolitics.com/ciods/ct-image/image-plane/00200032>`__
285
+ values must approximately form a line.
286
+
287
+ If any of these conditions are not met, a `dicom_numpy.DicomImportException` is raised.
288
+
289
+ Args:
290
+ slice_datasets (List[pydicom.dataset.FileDataset]): List of dicom headers.
291
+ Returns:
292
+ List[numpy.ndarray]: List of numpy arrays containing the data extracted the dicom files
293
+ (voxels, translation, rotation and scaling matrix).
294
+ """
295
+
296
+ if not slice_datasets:
297
+ raise ValueError("Must provide at least one DICOM dataset")
298
+
299
+ self.__validate_slices_form_uniform_grid(slice_datasets)
300
+
301
+ voxels = self.__merge_slice_pixel_arrays(slice_datasets)
302
+ transform, rotation, scaling = self.__ijk_to_patient_xyz_transform_matrix(
303
+ slice_datasets)
304
+
305
+ return voxels, transform, rotation, scaling
306
+
307
+ def process_files(self):
308
+ """
309
+ Reads DICOM files (imaging volume + ROIs) in the instance data path
310
+ and then organizes it in the MEDscan class.
311
+
312
+ Args:
313
+ None.
314
+
315
+ Returns:
316
+ medscan (MEDscan): Instance of a MEDscan class.
317
+ """
318
+
319
+ return self.process_files_wrapper.remote(self)
320
+
321
+ @ray.remote
322
+ def process_files_wrapper(self) -> MEDscan:
323
+ """
324
+ Wrapper function to process the files.
325
+ """
326
+
327
+ # PARTIAL PARSING OF ARGUMENTS
328
+ if self.path_images is None:
329
+ raise ValueError('At least two arguments must be provided')
330
+
331
+ # INITIALIZATION
332
+ medscan = MEDscan()
333
+
334
+ # IMAGING DATA AND ROI DEFINITION (if applicable)
335
+ # Reading DICOM images and headers
336
+ dicom_hi = [pydicom.dcmread(str(dicom_file), force=True)
337
+ for dicom_file in self.path_images]
338
+
339
+ try:
340
+ # Determination of the scan orientation
341
+ medscan.data.orientation = self.__get_dicom_scan_orientation(dicom_hi)
342
+
343
+ # IMPORTANT NOTE: extract_voxel_data using combine_slices from dicom_numpy
344
+ # missing slices and oblique restrictions apply see the reference:
345
+ # https://dicom-numpy.readthedocs.io/en/latest/index.html#dicom_numpy.combine_slices
346
+ try:
347
+ voxel_ndarray, ijk_to_xyz, rotation_m, scaling_m = self.combine_slices(dicom_hi)
348
+ except ValueError as e:
349
+ raise ValueError(f'Invalid DICOM data for combine_slices(). Error: {e}')
350
+
351
+ # Alignment of scan coordinates for MR scans
352
+ # (inverse of ImageOrientationPatient rotation matrix)
353
+ if not np.allclose(rotation_m, np.eye(rotation_m.shape[0])):
354
+ medscan.data.volume.scan_rot = rotation_m
355
+
356
+ medscan.data.volume.array = voxel_ndarray
357
+ medscan.type = dicom_hi[0].Modality + 'scan'
358
+
359
+ # 7. Creation of imref3d object
360
+ pixel_x = scaling_m[0, 0]
361
+ pixel_y = scaling_m[1, 1]
362
+ slice_s = scaling_m[2, 2]
363
+ min_grid = rotation_m@ijk_to_xyz[:3, 3]
364
+ min_x_grid = min_grid[0]
365
+ min_y_grid = min_grid[1]
366
+ min_z_grid = min_grid[2]
367
+ size_image = np.shape(voxel_ndarray)
368
+ spatial_ref = imref3d(size_image, pixel_x, pixel_y, slice_s)
369
+ spatial_ref.XWorldLimits = (np.array(spatial_ref.XWorldLimits) -
370
+ (spatial_ref.XWorldLimits[0] -
371
+ (min_x_grid-pixel_x/2))).tolist()
372
+ spatial_ref.YWorldLimits = (np.array(spatial_ref.YWorldLimits) -
373
+ (spatial_ref.YWorldLimits[0] -
374
+ (min_y_grid-pixel_y/2))).tolist()
375
+ spatial_ref.ZWorldLimits = (np.array(spatial_ref.ZWorldLimits) -
376
+ (spatial_ref.ZWorldLimits[0] -
377
+ (min_z_grid-slice_s/2))).tolist()
378
+
379
+ # Converting the results into lists
380
+ spatial_ref.ImageSize = spatial_ref.ImageSize.tolist()
381
+ spatial_ref.XIntrinsicLimits = spatial_ref.XIntrinsicLimits.tolist()
382
+ spatial_ref.YIntrinsicLimits = spatial_ref.YIntrinsicLimits.tolist()
383
+ spatial_ref.ZIntrinsicLimits = spatial_ref.ZIntrinsicLimits.tolist()
384
+
385
+ # Update the spatial reference in the MEDscan class
386
+ medscan.data.volume.spatialRef = spatial_ref
387
+
388
+ # DICOM HEADERS OF IMAGING DATA
389
+ dicom_h = [
390
+ pydicom.dcmread(str(dicom_file),stop_before_pixels=True,force=True) for dicom_file in self.path_images
391
+ ]
392
+ for i in range(0, len(dicom_h)):
393
+ dicom_h[i].remove_private_tags()
394
+ medscan.dicomH = dicom_h
395
+
396
+ # DICOM RTstruct (if applicable)
397
+ if self.path_rs is not None and len(self.path_rs) > 0:
398
+ dicom_rs_full = [
399
+ pydicom.dcmread(str(dicom_file),
400
+ stop_before_pixels=True,
401
+ force=True)
402
+ for dicom_file in self.path_rs
403
+ ]
404
+ for i in range(0, len(dicom_rs_full)):
405
+ dicom_rs_full[i].remove_private_tags()
406
+
407
+ # GATHER XYZ POINTS OF ROIs USING RTstruct
408
+ n_rs = len(dicom_rs_full) if type(dicom_rs_full) is list else dicom_rs_full
409
+ contour_num = 0
410
+ for rs in range(n_rs):
411
+ n_roi = len(dicom_rs_full[rs].StructureSetROISequence)
412
+ for roi in range(n_roi):
413
+ if roi!=0:
414
+ if dicom_rs_full[rs].StructureSetROISequence[roi].ROIName == \
415
+ dicom_rs_full[rs].StructureSetROISequence[roi-1].ROIName:
416
+ continue
417
+ points = []
418
+ name_set_strings = ['StructureSetName', 'StructureSetDescription',
419
+ 'series_description', 'SeriesInstanceUID']
420
+ for name_field in name_set_strings:
421
+ if name_field in dicom_rs_full[rs]:
422
+ name_set = getattr(dicom_rs_full[rs], name_field)
423
+ name_set_info = name_field
424
+ break
425
+
426
+ medscan.data.ROI.update_roi_name(key=contour_num,
427
+ roi_name=dicom_rs_full[rs].StructureSetROISequence[roi].ROIName)
428
+ medscan.data.ROI.update_indexes(key=contour_num,
429
+ indexes=None)
430
+ medscan.data.ROI.update_name_set(key=contour_num,
431
+ name_set=name_set)
432
+ medscan.data.ROI.update_name_set_info(key=contour_num,
433
+ nameSetInfo=name_set_info)
434
+
435
+ try:
436
+ n_closed_contour = len(dicom_rs_full[rs].ROIContourSequence[roi].ContourSequence)
437
+ ind_closed_contour = []
438
+ for s in range(0, n_closed_contour):
439
+ # points stored in the RTstruct file for a given closed
440
+ # contour (beware: there can be multiple closed contours
441
+ # on a given slice).
442
+ pts_temp = dicom_rs_full[rs].ROIContourSequence[roi].ContourSequence[s].ContourData
443
+ n_points = int(len(pts_temp) / 3)
444
+ if len(pts_temp) > 0:
445
+ ind_closed_contour = ind_closed_contour + np.tile(s, n_points).tolist()
446
+ if type(points) == list:
447
+ points = np.reshape(np.transpose(pts_temp),(n_points, 3))
448
+ else:
449
+ points = np.concatenate(
450
+ (points, np.reshape(np.transpose(pts_temp), (n_points, 3))),
451
+ axis=0
452
+ )
453
+ if n_closed_contour == 0:
454
+ print(f'Warning: no contour data found for ROI: \
455
+ {dicom_rs_full[rs].StructureSetROISequence[roi].ROIName}')
456
+ else:
457
+ # Save the XYZ points in the MEDscan class
458
+ medscan.data.ROI.update_indexes(
459
+ key=contour_num,
460
+ indexes=np.concatenate(
461
+ (points,
462
+ np.reshape(ind_closed_contour, (len(ind_closed_contour), 1))),
463
+ axis=1)
464
+ )
465
+ # Compute the ROI box
466
+ _, roi_obj = get_roi(
467
+ medscan,
468
+ name_roi='{' + dicom_rs_full[rs].StructureSetROISequence[roi].ROIName + '}',
469
+ box_string='full'
470
+ )
471
+
472
+ # Save the ROI box non-zero indexes in the MEDscan class
473
+ medscan.data.ROI.update_indexes(key=contour_num, indexes=np.nonzero(roi_obj.data.flatten()))
474
+
475
+ except Exception as e:
476
+ if 'SeriesDescription' in dicom_h[0]:
477
+ print(f'patientID: {dicom_hi[0].PatientID} Modality: {dicom_hi[0].SeriesDescription} error: \
478
+ {str(e)} n_roi: {str(roi)} n_rs: {str(rs)}')
479
+ else:
480
+ print(f'patientID: {dicom_hi[0].PatientID} Modality: {dicom_hi[0].Modality} error: \
481
+ {str(e)} n_roi: {str(roi)} n_rs: {str(rs)}')
482
+ medscan.data.ROI.update_indexes(key=contour_num, indexes=np.NaN)
483
+ contour_num += 1
484
+
485
+ # Save additional scan information in the MEDscan class
486
+ medscan.data.set_patient_position(patient_position=dicom_h[0].PatientPosition)
487
+ medscan.patientID = str(dicom_h[0].PatientID)
488
+ medscan.format = "dicom"
489
+ if 'SeriesDescription' in dicom_h[0]:
490
+ medscan.series_description = dicom_h[0].SeriesDescription
491
+ else:
492
+ medscan.series_description = dicom_h[0].Modality
493
+
494
+ # save MEDscan class instance as a pickle object
495
+ if self.save and self.path_save:
496
+ name_complete = save_MEDscan(medscan, self.path_save)
497
+ del medscan
498
+ else:
499
+ series_description = medscan.series_description.translate({ord(ch): '-' for ch in '/\\ ()&:*'})
500
+ name_id = medscan.patientID.translate({ord(ch): '-' for ch in '/\\ ()&:*'})
501
+
502
+ # final saving name
503
+ name_complete = name_id + '__' + series_description + '.' + medscan.type + '.npy'
504
+
505
+ except Exception as e:
506
+ if 'SeriesDescription' in dicom_hi[0]:
507
+ print(f'patientID: {dicom_hi[0].PatientID} Modality: {dicom_hi[0].SeriesDescription} error: {str(e)}')
508
+ else:
509
+ print(f'patientID: {dicom_hi[0].PatientID} Modality: {dicom_hi[0].Modality} error: {str(e)}')
510
+ return ''
511
+
512
+ return name_complete
@@ -0,0 +1,3 @@
1
+ from . import *
2
+ from .DataManager import *
3
+ from .ProcessDICOM import *