Upload Process library
Browse files- libraries/Process/CTimage.py +125 -0
- libraries/Process/MRimage.py +120 -0
- libraries/Process/PatientData.py +307 -0
- libraries/Process/RTdose.py +259 -0
- libraries/Process/RTplan.py +449 -0
- libraries/Process/RTstruct.py +542 -0
- libraries/utils_nii_dicom.py +130 -0
- libraries/utils_preprocess.py +148 -0
libraries/Process/CTimage.py
ADDED
@@ -0,0 +1,125 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import pydicom
|
2 |
+
import numpy as np
|
3 |
+
import scipy
|
4 |
+
|
5 |
+
class CTimage:
|
6 |
+
|
7 |
+
def __init__(self):
|
8 |
+
self.SeriesInstanceUID = ""
|
9 |
+
self.PatientInfo = {}
|
10 |
+
self.StudyInfo = {}
|
11 |
+
self.FrameOfReferenceUID = ""
|
12 |
+
self.ImgName = ""
|
13 |
+
self.SOPClassUID = ""
|
14 |
+
self.DcmFiles = []
|
15 |
+
self.isLoaded = 0
|
16 |
+
|
17 |
+
|
18 |
+
|
19 |
+
def print_CT_info(self, prefix=""):
|
20 |
+
print(prefix + "CT series: " + self.SeriesInstanceUID)
|
21 |
+
for ct_slice in self.DcmFiles:
|
22 |
+
print(prefix + " " + ct_slice)
|
23 |
+
|
24 |
+
|
25 |
+
def resample_CT(self, newvoxelsize):
|
26 |
+
ct = self.Image
|
27 |
+
# Rescaling to the newvoxelsize if given in parameter
|
28 |
+
|
29 |
+
source_shape = self.GridSize
|
30 |
+
voxelsize = self.PixelSpacing
|
31 |
+
#print("self.ImagePositionPatient",self.ImagePositionPatient, "source_shape",source_shape,"voxelsize",voxelsize)
|
32 |
+
VoxelX_source = self.ImagePositionPatient[0] + np.arange(source_shape[0])*voxelsize[0]
|
33 |
+
VoxelY_source = self.ImagePositionPatient[1] + np.arange(source_shape[1])*voxelsize[1]
|
34 |
+
VoxelZ_source = self.ImagePositionPatient[2] + np.arange(source_shape[2])*voxelsize[2]
|
35 |
+
|
36 |
+
target_shape = np.ceil(np.array(source_shape).astype(float)*np.array(voxelsize).astype(float)/newvoxelsize).astype(int)
|
37 |
+
VoxelX_target = self.ImagePositionPatient[0] + np.arange(target_shape[0])*newvoxelsize[0]
|
38 |
+
VoxelY_target = self.ImagePositionPatient[1] + np.arange(target_shape[1])*newvoxelsize[1]
|
39 |
+
VoxelZ_target = self.ImagePositionPatient[2] + np.arange(target_shape[2])*newvoxelsize[2]
|
40 |
+
#print("source_shape",source_shape,"target_shape",target_shape)
|
41 |
+
if(all(source_shape == target_shape) and np.linalg.norm(np.subtract(voxelsize, newvoxelsize) < 0.001)):
|
42 |
+
print("Image does not need filtering")
|
43 |
+
else:
|
44 |
+
# anti-aliasing filter
|
45 |
+
sigma = [0, 0, 0]
|
46 |
+
if(newvoxelsize[0] > voxelsize[0]): sigma[0] = 0.4 * (newvoxelsize[0]/voxelsize[0])
|
47 |
+
if(newvoxelsize[1] > voxelsize[1]): sigma[1] = 0.4 * (newvoxelsize[1]/voxelsize[1])
|
48 |
+
if(newvoxelsize[2] > voxelsize[2]): sigma[2] = 0.4 * (newvoxelsize[2]/voxelsize[2])
|
49 |
+
|
50 |
+
if(sigma != [0, 0, 0]):
|
51 |
+
print("Image is filtered before downsampling")
|
52 |
+
ct = scipy.ndimage.gaussian_filter(ct, sigma)
|
53 |
+
|
54 |
+
|
55 |
+
|
56 |
+
xi = np.array(np.meshgrid(VoxelX_target, VoxelY_target, VoxelZ_target))
|
57 |
+
xi = np.rollaxis(xi, 0, 4)
|
58 |
+
xi = xi.reshape((xi.size // 3, 3))
|
59 |
+
|
60 |
+
# get resized ct
|
61 |
+
ct = scipy.interpolate.interpn((VoxelX_source,VoxelY_source,VoxelZ_source), ct, xi, method='linear', fill_value=-1000, bounds_error=False).reshape(target_shape).transpose(1,0,2)
|
62 |
+
|
63 |
+
self.PixelSpacing = newvoxelsize
|
64 |
+
self.GridSize = list(ct.shape)
|
65 |
+
self.NumVoxels = self.GridSize[0] * self.GridSize[1] * self.GridSize[2]
|
66 |
+
self.Image = ct
|
67 |
+
#print("self.ImagePositionPatient",self.ImagePositionPatient, "self.GridSize[0]",self.GridSize[0],"self.PixelSpacing",self.PixelSpacing)
|
68 |
+
|
69 |
+
self.VoxelX = self.ImagePositionPatient[0] + np.arange(self.GridSize[0])*self.PixelSpacing[0]
|
70 |
+
self.VoxelY = self.ImagePositionPatient[1] + np.arange(self.GridSize[1])*self.PixelSpacing[1]
|
71 |
+
self.VoxelZ = self.ImagePositionPatient[2] + np.arange(self.GridSize[2])*self.PixelSpacing[2]
|
72 |
+
self.isLoaded = 1
|
73 |
+
|
74 |
+
def import_Dicom_CT(self):
|
75 |
+
|
76 |
+
if(self.isLoaded == 1):
|
77 |
+
print("Warning: CT serries " + self.SeriesInstanceUID + " is already loaded")
|
78 |
+
return
|
79 |
+
|
80 |
+
images = []
|
81 |
+
SOPInstanceUIDs = []
|
82 |
+
SliceLocation = np.zeros(len(self.DcmFiles), dtype='float')
|
83 |
+
|
84 |
+
for i in range(len(self.DcmFiles)):
|
85 |
+
file_path = self.DcmFiles[i]
|
86 |
+
dcm = pydicom.dcmread(file_path)
|
87 |
+
|
88 |
+
if(hasattr(dcm, 'SliceLocation') and abs(dcm.SliceLocation - dcm.ImagePositionPatient[2]) > 0.001):
|
89 |
+
print("WARNING: SliceLocation (" + str(dcm.SliceLocation) + ") is different than ImagePositionPatient[2] (" + str(dcm.ImagePositionPatient[2]) + ") for " + file_path)
|
90 |
+
|
91 |
+
SliceLocation[i] = float(dcm.ImagePositionPatient[2])
|
92 |
+
images.append(dcm.pixel_array * dcm.RescaleSlope + dcm.RescaleIntercept)
|
93 |
+
SOPInstanceUIDs.append(dcm.SOPInstanceUID)
|
94 |
+
|
95 |
+
# sort slices according to their location in order to reconstruct the 3d image
|
96 |
+
sort_index = np.argsort(SliceLocation)
|
97 |
+
SliceLocation = SliceLocation[sort_index]
|
98 |
+
SOPInstanceUIDs = [SOPInstanceUIDs[n] for n in sort_index]
|
99 |
+
images = [images[n] for n in sort_index]
|
100 |
+
ct = np.dstack(images).astype("float32")
|
101 |
+
|
102 |
+
if ct.shape[0:2] != (dcm.Rows, dcm.Columns):
|
103 |
+
print("WARNING: GridSize " + str(ct.shape[0:2]) + " different from Dicom Rows (" + str(dcm.Rows) + ") and Columns (" + str(dcm.Columns) + ")")
|
104 |
+
|
105 |
+
MeanSliceDistance = (SliceLocation[-1] - SliceLocation[0]) / (len(images)-1)
|
106 |
+
if(abs(MeanSliceDistance - dcm.SliceThickness) > 0.001):
|
107 |
+
print("WARNING: MeanSliceDistance (" + str(MeanSliceDistance) + ") is different from SliceThickness (" + str(dcm.SliceThickness) + ")")
|
108 |
+
|
109 |
+
self.SOPClassUID = dcm.SOPClassUID
|
110 |
+
self.FrameOfReferenceUID = dcm.FrameOfReferenceUID
|
111 |
+
self.ImagePositionPatient = [float(dcm.ImagePositionPatient[0]), float(dcm.ImagePositionPatient[1]), SliceLocation[0]]
|
112 |
+
self.PixelSpacing = [float(dcm.PixelSpacing[0]), float(dcm.PixelSpacing[1]), MeanSliceDistance]
|
113 |
+
self.GridSize = list(ct.shape)
|
114 |
+
self.NumVoxels = self.GridSize[0] * self.GridSize[1] * self.GridSize[2]
|
115 |
+
self.Image = ct
|
116 |
+
self.SOPInstanceUIDs = SOPInstanceUIDs
|
117 |
+
self.VoxelX = self.ImagePositionPatient[0] + np.arange(self.GridSize[0])*self.PixelSpacing[0]
|
118 |
+
self.VoxelY = self.ImagePositionPatient[1] + np.arange(self.GridSize[1])*self.PixelSpacing[1]
|
119 |
+
self.VoxelZ = self.ImagePositionPatient[2] + np.arange(self.GridSize[2])*self.PixelSpacing[2]
|
120 |
+
self.isLoaded = 1
|
121 |
+
|
122 |
+
|
123 |
+
|
124 |
+
|
125 |
+
|
libraries/Process/MRimage.py
ADDED
@@ -0,0 +1,120 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import pydicom
|
2 |
+
import numpy as np
|
3 |
+
import scipy
|
4 |
+
|
5 |
+
class MRimage:
|
6 |
+
|
7 |
+
def __init__(self):
|
8 |
+
self.SeriesInstanceUID = ""
|
9 |
+
self.PatientInfo = {}
|
10 |
+
self.StudyInfo = {}
|
11 |
+
self.FrameOfReferenceUID = ""
|
12 |
+
self.ImgName = ""
|
13 |
+
self.SOPClassUID = ""
|
14 |
+
|
15 |
+
self.DcmFiles = []
|
16 |
+
self.isLoaded = 0
|
17 |
+
|
18 |
+
|
19 |
+
|
20 |
+
def print_MR_info(self, prefix=""):
|
21 |
+
print(prefix + "MR series: " + self.SeriesInstanceUID)
|
22 |
+
for mr_slice in self.DcmFiles:
|
23 |
+
print(prefix + " " + mr_slice)
|
24 |
+
|
25 |
+
def resample_MR(self, newvoxelsize):
|
26 |
+
mr = self.Image
|
27 |
+
# Rescaling to the newvoxelsize if given in parameter
|
28 |
+
|
29 |
+
source_shape = self.GridSize
|
30 |
+
voxelsize = self.PixelSpacing
|
31 |
+
VoxelX_source = self.ImagePositionPatient[0] + np.arange(source_shape[0])*voxelsize[0]
|
32 |
+
VoxelY_source = self.ImagePositionPatient[1] + np.arange(source_shape[1])*voxelsize[1]
|
33 |
+
VoxelZ_source = self.ImagePositionPatient[2] + np.arange(source_shape[2])*voxelsize[2]
|
34 |
+
|
35 |
+
target_shape = np.ceil(np.array(source_shape).astype(float)*np.array(voxelsize).astype(float)/newvoxelsize).astype(int)
|
36 |
+
VoxelX_target = self.ImagePositionPatient[0] + np.arange(target_shape[0])*newvoxelsize[0]
|
37 |
+
VoxelY_target = self.ImagePositionPatient[1] + np.arange(target_shape[1])*newvoxelsize[1]
|
38 |
+
VoxelZ_target = self.ImagePositionPatient[2] + np.arange(target_shape[2])*newvoxelsize[2]
|
39 |
+
print("source_shape",source_shape,"target_shape",target_shape)
|
40 |
+
if(all(source_shape == target_shape) and np.linalg.norm(np.subtract(voxelsize, newvoxelsize) < 0.001)):
|
41 |
+
print("Image does not need filtering")
|
42 |
+
else:
|
43 |
+
# anti-aliasing filter
|
44 |
+
sigma = [0, 0, 0]
|
45 |
+
if(newvoxelsize[0] > voxelsize[0]): sigma[0] = 0.4 * (newvoxelsize[0]/voxelsize[0])
|
46 |
+
if(newvoxelsize[1] > voxelsize[1]): sigma[1] = 0.4 * (newvoxelsize[1]/voxelsize[1])
|
47 |
+
if(newvoxelsize[2] > voxelsize[2]): sigma[2] = 0.4 * (newvoxelsize[2]/voxelsize[2])
|
48 |
+
|
49 |
+
if(sigma != [0, 0, 0]):
|
50 |
+
print("Image is filtered before downsampling")
|
51 |
+
mr = scipy.ndimage.gaussian_filter(mr, sigma)
|
52 |
+
|
53 |
+
xi = np.array(np.meshgrid(VoxelX_target, VoxelY_target, VoxelZ_target))
|
54 |
+
xi = np.rollaxis(xi, 0, 4)
|
55 |
+
xi = xi.reshape((xi.size // 3, 3))
|
56 |
+
|
57 |
+
# get resized mr
|
58 |
+
mr = scipy.interpolate.interpn((VoxelX_source,VoxelY_source,VoxelZ_source), mr, xi, method='linear', fill_value=0, bounds_error=False).reshape(target_shape).transpose(1,0,2)
|
59 |
+
|
60 |
+
self.PixelSpacing = newvoxelsize
|
61 |
+
|
62 |
+
self.GridSize = list(mr.shape)
|
63 |
+
self.NumVoxels = self.GridSize[0] * self.GridSize[1] * self.GridSize[2]
|
64 |
+
self.Image = mr
|
65 |
+
self.VoxelX = self.ImagePositionPatient[0] + np.arange(self.GridSize[0])*self.PixelSpacing[0]
|
66 |
+
self.VoxelY = self.ImagePositionPatient[1] + np.arange(self.GridSize[1])*self.PixelSpacing[1]
|
67 |
+
self.VoxelZ = self.ImagePositionPatient[2] + np.arange(self.GridSize[2])*self.PixelSpacing[2]
|
68 |
+
self.isLoaded = 1
|
69 |
+
|
70 |
+
def import_Dicom_MR(self):
|
71 |
+
|
72 |
+
if(self.isLoaded == 1):
|
73 |
+
print("Warning: CT series " + self.SeriesInstanceUID + " is already loaded")
|
74 |
+
return
|
75 |
+
|
76 |
+
images = []
|
77 |
+
SOPInstanceUIDs = []
|
78 |
+
SliceLocation = np.zeros(len(self.DcmFiles), dtype='float')
|
79 |
+
|
80 |
+
for i in range(len(self.DcmFiles)):
|
81 |
+
file_path = self.DcmFiles[i]
|
82 |
+
dcm = pydicom.dcmread(file_path)
|
83 |
+
|
84 |
+
if(hasattr(dcm, 'SliceLocation') and abs(dcm.SliceLocation - dcm.ImagePositionPatient[2]) > 0.001):
|
85 |
+
print("WARNING: SliceLocation (" + str(dcm.SliceLocation) + ") is different than ImagePositionPatient[2] (" + str(dcm.ImagePositionPatient[2]) + ") for " + file_path)
|
86 |
+
|
87 |
+
SliceLocation[i] = float(dcm.ImagePositionPatient[2])
|
88 |
+
images.append(dcm.pixel_array)# * dcm.RescaleSlope + dcm.RescaleIntercept)
|
89 |
+
SOPInstanceUIDs.append(dcm.SOPInstanceUID)
|
90 |
+
|
91 |
+
# sort slices according to their location in order to reconstruct the 3d image
|
92 |
+
sort_index = np.argsort(SliceLocation)
|
93 |
+
SliceLocation = SliceLocation[sort_index]
|
94 |
+
SOPInstanceUIDs = [SOPInstanceUIDs[n] for n in sort_index]
|
95 |
+
images = [images[n] for n in sort_index]
|
96 |
+
mr = np.dstack(images).astype("float32")
|
97 |
+
|
98 |
+
if mr.shape[0:2] != (dcm.Rows, dcm.Columns):
|
99 |
+
print("WARNING: GridSize " + str(mr.shape[0:2]) + " different from Dicom Rows (" + str(dcm.Rows) + ") and Columns (" + str(dcm.Columns) + ")")
|
100 |
+
|
101 |
+
MeanSliceDistance = (SliceLocation[-1] - SliceLocation[0]) / (len(images)-1)
|
102 |
+
if(abs(MeanSliceDistance - dcm.SliceThickness) > 0.001):
|
103 |
+
print("WARNING: MeanSliceDistance (" + str(MeanSliceDistance) + ") is different from SliceThickness (" + str(dcm.SliceThickness) + ")")
|
104 |
+
|
105 |
+
self.FrameOfReferenceUID = dcm.FrameOfReferenceUID
|
106 |
+
self.ImagePositionPatient = [float(dcm.ImagePositionPatient[0]), float(dcm.ImagePositionPatient[1]), SliceLocation[0]]
|
107 |
+
self.PixelSpacing = [float(dcm.PixelSpacing[0]), float(dcm.PixelSpacing[1]), MeanSliceDistance]
|
108 |
+
self.GridSize = list(mr.shape)
|
109 |
+
self.NumVoxels = self.GridSize[0] * self.GridSize[1] * self.GridSize[2]
|
110 |
+
self.Image = mr
|
111 |
+
self.SeriesDescription = dcm.SeriesDescription
|
112 |
+
self.SOPInstanceUIDs = SOPInstanceUIDs
|
113 |
+
self.VoxelX = self.ImagePositionPatient[0] + np.arange(self.GridSize[0])*self.PixelSpacing[0]
|
114 |
+
self.VoxelY = self.ImagePositionPatient[1] + np.arange(self.GridSize[1])*self.PixelSpacing[1]
|
115 |
+
self.VoxelZ = self.ImagePositionPatient[2] + np.arange(self.GridSize[2])*self.PixelSpacing[2]
|
116 |
+
self.isLoaded = 1
|
117 |
+
|
118 |
+
|
119 |
+
|
120 |
+
|
libraries/Process/PatientData.py
ADDED
@@ -0,0 +1,307 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import pydicom
|
3 |
+
|
4 |
+
from libraries.Process.CTimage import *
|
5 |
+
from libraries.Process.MRimage import *
|
6 |
+
from libraries.Process.RTdose import *
|
7 |
+
from libraries.Process.RTstruct import *
|
8 |
+
from libraries.Process.RTplan import *
|
9 |
+
|
10 |
+
class PatientList:
|
11 |
+
|
12 |
+
def __init__(self):
|
13 |
+
self.list = []
|
14 |
+
|
15 |
+
|
16 |
+
|
17 |
+
def find_CT_image(self, display_id):
|
18 |
+
count = -1
|
19 |
+
for patient_id in range(len(self.list)):
|
20 |
+
for ct_id in range(len(self.list[patient_id].CTimages)):
|
21 |
+
if(self.list[patient_id].CTimages[ct_id].isLoaded == 1): count += 1
|
22 |
+
if(count == display_id): break
|
23 |
+
if(count == display_id): break
|
24 |
+
|
25 |
+
return patient_id, ct_id
|
26 |
+
|
27 |
+
|
28 |
+
|
29 |
+
def find_dose_image(self, display_id):
|
30 |
+
count = -1
|
31 |
+
for patient_id in range(len(self.list)):
|
32 |
+
for dose_id in range(len(self.list[patient_id].RTdoses)):
|
33 |
+
if(self.list[patient_id].RTdoses[dose_id].isLoaded == 1): count += 1
|
34 |
+
if(count == display_id): break
|
35 |
+
if(count == display_id): break
|
36 |
+
|
37 |
+
return patient_id, dose_id
|
38 |
+
|
39 |
+
|
40 |
+
|
41 |
+
def find_contour(self, ROIName):
|
42 |
+
for patient_id in range(len(self.list)):
|
43 |
+
for struct_id in range(len(self.list[patient_id].RTstructs)):
|
44 |
+
if(self.list[patient_id].RTstructs[struct_id].isLoaded == 1):
|
45 |
+
for contour_id in range(len(self.list[patient_id].RTstructs[struct_id].Contours)):
|
46 |
+
if(self.list[patient_id].RTstructs[struct_id].Contours[contour_id].ROIName == ROIName):
|
47 |
+
return patient_id, struct_id, contour_id
|
48 |
+
|
49 |
+
|
50 |
+
|
51 |
+
def list_dicom_files(self, folder_path, recursive):
|
52 |
+
file_list = os.listdir(folder_path)
|
53 |
+
#print("len file_list", len(file_list), "folderpath",folder_path)
|
54 |
+
for file_name in file_list:
|
55 |
+
file_path = os.path.join(folder_path, file_name)
|
56 |
+
|
57 |
+
# folders
|
58 |
+
if os.path.isdir(file_path):
|
59 |
+
if recursive == True:
|
60 |
+
subfolder_list = self.list_dicom_files(file_path, True)
|
61 |
+
#join_patient_lists(Patients, subfolder_list)
|
62 |
+
|
63 |
+
# files
|
64 |
+
elif os.path.isfile(file_path):
|
65 |
+
|
66 |
+
try:
|
67 |
+
dcm = pydicom.dcmread(file_path)
|
68 |
+
except:
|
69 |
+
print("Invalid Dicom file: " + file_path)
|
70 |
+
continue
|
71 |
+
|
72 |
+
patient_id = next((x for x, val in enumerate(self.list) if val.PatientInfo.PatientID == dcm.PatientID), -1)
|
73 |
+
|
74 |
+
|
75 |
+
if patient_id == -1:
|
76 |
+
Patient = PatientData()
|
77 |
+
Patient.PatientInfo.PatientID = dcm.PatientID
|
78 |
+
Patient.PatientInfo.PatientName = str(dcm.PatientName)
|
79 |
+
Patient.PatientInfo.PatientBirthDate = dcm.PatientBirthDate
|
80 |
+
Patient.PatientInfo.PatientSex = dcm.PatientSex
|
81 |
+
self.list.append(Patient)
|
82 |
+
patient_id = len(self.list) - 1
|
83 |
+
|
84 |
+
# Dicom CT
|
85 |
+
if dcm.SOPClassUID == "1.2.840.10008.5.1.4.1.1.2":
|
86 |
+
ct_id = next((x for x, val in enumerate(self.list[patient_id].CTimages) if val.SeriesInstanceUID == dcm.SeriesInstanceUID), -1)
|
87 |
+
if ct_id == -1:
|
88 |
+
CT = CTimage()
|
89 |
+
CT.SeriesInstanceUID = dcm.SeriesInstanceUID
|
90 |
+
CT.SOPClassUID == "1.2.840.10008.5.1.4.1.1.2"
|
91 |
+
CT.PatientInfo = self.list[patient_id].PatientInfo
|
92 |
+
CT.StudyInfo = StudyInfo()
|
93 |
+
CT.StudyInfo.StudyInstanceUID = dcm.StudyInstanceUID
|
94 |
+
CT.StudyInfo.StudyID = dcm.StudyID
|
95 |
+
CT.StudyInfo.StudyDate = dcm.StudyDate
|
96 |
+
CT.StudyInfo.StudyTime = dcm.StudyTime
|
97 |
+
if(hasattr(dcm, 'SeriesDescription') and dcm.SeriesDescription != ""): CT.ImgName = dcm.SeriesDescription
|
98 |
+
else: CT.ImgName = dcm.SeriesInstanceUID
|
99 |
+
self.list[patient_id].CTimages.append(CT)
|
100 |
+
ct_id = len(self.list[patient_id].CTimages) - 1
|
101 |
+
|
102 |
+
self.list[patient_id].CTimages[ct_id].DcmFiles.append(file_path)
|
103 |
+
|
104 |
+
# Dicom MR
|
105 |
+
elif dcm.SOPClassUID == "1.2.840.10008.5.1.4.1.1.4":
|
106 |
+
mr_id = next((x for x, val in enumerate(self.list[patient_id].MRimages) if val.SeriesInstanceUID == dcm.SeriesInstanceUID), -1)
|
107 |
+
if mr_id == -1:
|
108 |
+
MR = MRimage()
|
109 |
+
MR.SeriesInstanceUID = dcm.SeriesInstanceUID
|
110 |
+
MR.SOPClassUID == "1.2.840.10008.5.1.4.1.1.4"
|
111 |
+
MR.PatientInfo = self.list[patient_id].PatientInfo
|
112 |
+
MR.StudyInfo = StudyInfo()
|
113 |
+
MR.StudyInfo.StudyInstanceUID = dcm.StudyInstanceUID
|
114 |
+
MR.StudyInfo.StudyID = dcm.StudyID
|
115 |
+
MR.StudyInfo.StudyDate = dcm.StudyDate
|
116 |
+
MR.StudyInfo.StudyTime = dcm.StudyTime
|
117 |
+
if(hasattr(dcm, 'SeriesDescription') and dcm.SeriesDescription != ""): MR.ImgName = dcm.SeriesDescription
|
118 |
+
else: MR.ImgName = dcm.SeriesInstanceUID
|
119 |
+
self.list[patient_id].MRimages.append(MR)
|
120 |
+
mr_id = len(self.list[patient_id].MRimages) - 1
|
121 |
+
|
122 |
+
self.list[patient_id].MRimages[mr_id].DcmFiles.append(file_path)
|
123 |
+
|
124 |
+
# Dicom dose
|
125 |
+
elif dcm.SOPClassUID == "1.2.840.10008.5.1.4.1.1.481.2":
|
126 |
+
dose_id = next((x for x, val in enumerate(self.list[patient_id].RTdoses) if val.SOPInstanceUID == dcm.SOPInstanceUID), -1)
|
127 |
+
if dose_id == -1:
|
128 |
+
dose = RTdose()
|
129 |
+
dose.SOPInstanceUID = dcm.SOPInstanceUID
|
130 |
+
dose.SeriesInstanceUID = dcm.SeriesInstanceUID
|
131 |
+
dose.PatientInfo = self.list[patient_id].PatientInfo
|
132 |
+
dose.StudyInfo = StudyInfo()
|
133 |
+
dose.StudyInfo.StudyInstanceUID = dcm.StudyInstanceUID
|
134 |
+
dose.StudyInfo.StudyID = dcm.StudyID
|
135 |
+
dose.StudyInfo.StudyDate = dcm.StudyDate
|
136 |
+
dose.StudyInfo.StudyTime = dcm.StudyTime
|
137 |
+
if dcm.DoseSummationType == "BEAM":
|
138 |
+
dose.beam_number = str(dcm.ReferencedRTPlanSequence[0].ReferencedFractionGroupSequence[0].ReferencedBeamSequence[0].ReferencedBeamNumber)
|
139 |
+
elif dcm.DoseSummationType == "PRIOR_TARGET":
|
140 |
+
dose.beam_number = "PRIOR_TARGET"
|
141 |
+
elif "PRIOR_TARGET_OAR" in dcm.DoseSummationType :
|
142 |
+
dose.beam_number = "PRIOR_TARGET_OAR"
|
143 |
+
else:
|
144 |
+
dose.beam_number = "PLAN"
|
145 |
+
if(hasattr(dcm, 'SeriesDescription') and dcm.SeriesDescription != ""): dose.ImgName = dcm.SeriesDescription
|
146 |
+
else: dose.ImgName = dcm.SeriesInstanceUID
|
147 |
+
dose.DcmFile = file_path
|
148 |
+
self.list[patient_id].RTdoses.append(dose)
|
149 |
+
|
150 |
+
# Dicom struct
|
151 |
+
elif dcm.SOPClassUID == "1.2.840.10008.5.1.4.1.1.481.3":
|
152 |
+
#struct_id = next((x for x, val in enumerate(self.list[patient_id].RTstructs_CT) if val.SeriesInstanceUID == dcm.SeriesInstanceUID), -1)
|
153 |
+
#if struct_id == -1:
|
154 |
+
struct = RTstruct()
|
155 |
+
struct.SeriesInstanceUID = dcm.SeriesInstanceUID
|
156 |
+
struct.PatientInfo = self.list[patient_id].PatientInfo
|
157 |
+
struct.StudyInfo = StudyInfo()
|
158 |
+
struct.StudyInfo.StudyInstanceUID = dcm.StudyInstanceUID
|
159 |
+
struct.StudyInfo.StudyID = dcm.StudyID
|
160 |
+
struct.StudyInfo.StudyDate = dcm.StudyDate
|
161 |
+
struct.StudyInfo.StudyTime = dcm.StudyTime
|
162 |
+
struct.DcmFile = file_path
|
163 |
+
|
164 |
+
# Avoid ContourSequence of the first contour is not present!
|
165 |
+
stop = 0
|
166 |
+
for s in range(len(dcm.ROIContourSequence)):
|
167 |
+
if hasattr(dcm.ROIContourSequence[s], 'ContourSequence') and stop == 0:
|
168 |
+
stop = 1
|
169 |
+
if dcm.ROIContourSequence[s].ContourSequence[0].ContourImageSequence[0].ReferencedSOPClassUID=="1.2.840.10008.5.1.4.1.1.2":
|
170 |
+
self.list[patient_id].RTstructs_CT.append(struct)
|
171 |
+
elif dcm.ROIContourSequence[s].ContourSequence[0].ContourImageSequence[0].ReferencedSOPClassUID=="1.2.840.10008.5.1.4.1.1.4":
|
172 |
+
self.list[patient_id].RTstructs_MR.append(struct)
|
173 |
+
else:
|
174 |
+
continue
|
175 |
+
|
176 |
+
|
177 |
+
# Dicom plans
|
178 |
+
elif dcm.SOPClassUID == "1.2.840.10008.5.1.4.1.1.481.5" or dcm.SOPClassUID == "1.2.840.10008.5.1.4.1.1.481.8":
|
179 |
+
plan_id = next((x for x, val in enumerate(self.list[patient_id].RTplans) if val.SeriesInstanceUID == dcm.SeriesInstanceUID), -1)
|
180 |
+
if plan_id == -1:
|
181 |
+
plan = RTplan()
|
182 |
+
plan.SeriesInstanceUID = dcm.SeriesInstanceUID
|
183 |
+
plan.PatientInfo = self.list[patient_id].PatientInfo
|
184 |
+
plan.StudyInfo = StudyInfo()
|
185 |
+
plan.StudyInfo.StudyInstanceUID = dcm.StudyInstanceUID
|
186 |
+
plan.StudyInfo.StudyID = dcm.StudyID
|
187 |
+
plan.StudyInfo.StudyDate = dcm.StudyDate
|
188 |
+
plan.StudyInfo.StudyTime = dcm.StudyTime
|
189 |
+
if(hasattr(dcm, 'SeriesDescription') and dcm.SeriesDescription != ""): plan.PlanName = dcm.SeriesDescription
|
190 |
+
else: plan.PlanName = dcm.SeriesInstanceUID
|
191 |
+
plan.DcmFile = file_path
|
192 |
+
self.list[patient_id].RTplans.append(plan)
|
193 |
+
|
194 |
+
else:
|
195 |
+
print("Unknown SOPClassUID " + dcm.SOPClassUID + " for file " + file_path)
|
196 |
+
|
197 |
+
# other
|
198 |
+
else:
|
199 |
+
print("Unknown file type " + file_path)
|
200 |
+
|
201 |
+
|
202 |
+
def print_patient_list(self):
|
203 |
+
print("")
|
204 |
+
for patient in self.list:
|
205 |
+
patient.print_patient_info()
|
206 |
+
|
207 |
+
print("")
|
208 |
+
|
209 |
+
|
210 |
+
|
211 |
+
class PatientData:
|
212 |
+
|
213 |
+
def __init__(self):
|
214 |
+
self.PatientInfo = PatientInfo()
|
215 |
+
self.CTimages = []
|
216 |
+
self.MRimages = []
|
217 |
+
self.RTdoses = []
|
218 |
+
self.RTplans = []
|
219 |
+
self.RTstructs_CT = []
|
220 |
+
self.RTstructs_MR = []
|
221 |
+
|
222 |
+
def print_patient_info(self, prefix=""):
|
223 |
+
print("")
|
224 |
+
print(prefix + "PatientName: " + self.PatientInfo.PatientName)
|
225 |
+
print(prefix+ "PatientID: " + self.PatientInfo.PatientID)
|
226 |
+
|
227 |
+
for ct in self.CTimages:
|
228 |
+
print("")
|
229 |
+
ct.print_CT_info(prefix + " ")
|
230 |
+
print("")
|
231 |
+
|
232 |
+
for mr in self.MRimages:
|
233 |
+
print("")
|
234 |
+
mr.print_MR_info(prefix + " ")
|
235 |
+
|
236 |
+
print("")
|
237 |
+
for dose in self.RTdoses:
|
238 |
+
print("")
|
239 |
+
dose.print_dose_info(prefix + " ")
|
240 |
+
|
241 |
+
print("")
|
242 |
+
for struct in self.RTstructs_CT:
|
243 |
+
print("")
|
244 |
+
struct.print_struct_info(prefix + " ")
|
245 |
+
|
246 |
+
print("")
|
247 |
+
for struct in self.RTstructs_MR:
|
248 |
+
print("")
|
249 |
+
struct.print_struct_info(prefix + " ")
|
250 |
+
|
251 |
+
|
252 |
+
def import_patient_data(self,newvoxelsize=None):
|
253 |
+
# import CT images
|
254 |
+
for i,ct in enumerate(self.CTimages):
|
255 |
+
if(ct.isLoaded == 1): continue
|
256 |
+
ct.import_Dicom_CT()
|
257 |
+
# import MR images
|
258 |
+
for i,mr in enumerate(self.MRimages):
|
259 |
+
if(mr.isLoaded == 1): continue
|
260 |
+
mr.import_Dicom_MR()
|
261 |
+
# import RTstructs linked to CT
|
262 |
+
for i, struct in enumerate(self.RTstructs_CT):
|
263 |
+
struct.import_Dicom_struct(self.CTimages[i]) # to be improved: user select CT image
|
264 |
+
# import RTstructs linked to MR
|
265 |
+
for i, struct in enumerate(self.RTstructs_MR):
|
266 |
+
struct.import_Dicom_struct(self.MRimages[i]) # to be improved: user select CT image
|
267 |
+
# import RTPlan
|
268 |
+
for i,plan in enumerate(self.RTplans):
|
269 |
+
if(plan.isLoaded == 1): continue
|
270 |
+
plan.import_Dicom_plan()
|
271 |
+
#RESAMPLE ONLY IF NEWVOXELSIZE IS NOT NONE
|
272 |
+
if newvoxelsize is not None:
|
273 |
+
# Resample CT images
|
274 |
+
for i,ct in enumerate(self.CTimages):
|
275 |
+
ct.resample_CT(newvoxelsize)
|
276 |
+
# Resample MR images
|
277 |
+
for i,mr in enumerate(self.MRimages):
|
278 |
+
mr.resample_MR(newvoxelsize)
|
279 |
+
# Resample RTstructs linked to CT images
|
280 |
+
for i, struct in enumerate(self.RTstructs_CT):
|
281 |
+
struct.resample_struct(newvoxelsize) # to be improved: user select CT image
|
282 |
+
# import dose distributions
|
283 |
+
for i, dose in enumerate(self.RTdoses):
|
284 |
+
if(dose.isLoaded == 1): continue
|
285 |
+
dose.import_Dicom_dose(self.CTimages[0]) # to be improved: user select CT image
|
286 |
+
|
287 |
+
|
288 |
+
|
289 |
+
|
290 |
+
class PatientInfo:
|
291 |
+
|
292 |
+
def __init__(self):
|
293 |
+
self.PatientID = ''
|
294 |
+
self.PatientName = ''
|
295 |
+
self.PatientBirthDate = ''
|
296 |
+
self.PatientSex = ''
|
297 |
+
|
298 |
+
|
299 |
+
|
300 |
+
|
301 |
+
class StudyInfo:
|
302 |
+
|
303 |
+
def __init__(self):
|
304 |
+
self.StudyInstanceUID = ''
|
305 |
+
self.StudyID = ''
|
306 |
+
self.StudyDate = ''
|
307 |
+
self.StudyTime = ''
|
libraries/Process/RTdose.py
ADDED
@@ -0,0 +1,259 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import pydicom
|
2 |
+
import datetime
|
3 |
+
import numpy as np
|
4 |
+
import scipy
|
5 |
+
import nibabel as nib
|
6 |
+
|
7 |
+
class PatientInfo:
|
8 |
+
|
9 |
+
def __init__(self):
|
10 |
+
self.PatientID = ''
|
11 |
+
self.PatientName = ''
|
12 |
+
self.PatientBirthDate = ''
|
13 |
+
self.PatientSex = ''
|
14 |
+
|
15 |
+
class StudyInfo:
|
16 |
+
|
17 |
+
def __init__(self):
|
18 |
+
self.StudyInstanceUID = ''
|
19 |
+
self.StudyID = ''
|
20 |
+
self.StudyDate = ''
|
21 |
+
self.StudyTime = ''
|
22 |
+
|
23 |
+
class RTdose:
|
24 |
+
|
25 |
+
def __init__(self):
|
26 |
+
self.SeriesInstanceUID = ""
|
27 |
+
self.SOPInstanceUID = ""
|
28 |
+
self.PatientInfo = PatientInfo()
|
29 |
+
self.StudyInfo = StudyInfo()
|
30 |
+
self.CT_SeriesInstanceUID = ""
|
31 |
+
self.Plan_SOPInstanceUID = ""
|
32 |
+
self.FrameOfReferenceUID = ""
|
33 |
+
self.ImgName = ""
|
34 |
+
self.beam_number = "" # Beam number (str) or PLAN if sum of all
|
35 |
+
self.DcmFile = ""
|
36 |
+
self.isLoaded = 0
|
37 |
+
|
38 |
+
def print_dose_info(self, prefix=""):
|
39 |
+
print(prefix + "Dose: " + self.SOPInstanceUID)
|
40 |
+
print(prefix + " " + self.DcmFile)
|
41 |
+
|
42 |
+
|
43 |
+
|
44 |
+
def import_Dicom_dose(self, CT):
|
45 |
+
if(self.isLoaded == 1):
|
46 |
+
print("Warning: Dose image " + self.SOPInstanceUID + " is already loaded")
|
47 |
+
return
|
48 |
+
|
49 |
+
dcm = pydicom.dcmread(self.DcmFile)
|
50 |
+
|
51 |
+
self.CT_SeriesInstanceUID = CT.SeriesInstanceUID
|
52 |
+
# self.Plan_SOPInstanceUID = dcm.ReferencedRTPlanSequence[0].ReferencedSOPInstanceUID
|
53 |
+
|
54 |
+
|
55 |
+
if(dcm.BitsStored == 16 and dcm.PixelRepresentation == 0):
|
56 |
+
dt = np.dtype('uint16')
|
57 |
+
elif(dcm.BitsStored == 16 and dcm.PixelRepresentation == 1):
|
58 |
+
dt = np.dtype('int16')
|
59 |
+
elif(dcm.BitsStored == 32 and dcm.PixelRepresentation == 0):
|
60 |
+
dt = np.dtype('uint32')
|
61 |
+
elif(dcm.BitsStored == 32 and dcm.PixelRepresentation == 1):
|
62 |
+
dt = np.dtype('int32')
|
63 |
+
else:
|
64 |
+
print("Error: Unknown data type for " + self.DcmFile)
|
65 |
+
return
|
66 |
+
|
67 |
+
if(dcm.HighBit == dcm.BitsStored-1):
|
68 |
+
dt = dt.newbyteorder('L')
|
69 |
+
else:
|
70 |
+
dt = dt.newbyteorder('B')
|
71 |
+
|
72 |
+
dose_image = np.frombuffer(dcm.PixelData, dtype=dt)
|
73 |
+
dose_image = dose_image.reshape((dcm.Columns, dcm.Rows, dcm.NumberOfFrames), order='F').transpose(1,0,2)
|
74 |
+
dose_image = dose_image * dcm.DoseGridScaling
|
75 |
+
|
76 |
+
self.Image = dose_image
|
77 |
+
self.FrameOfReferenceUID = dcm.FrameOfReferenceUID
|
78 |
+
self.ImagePositionPatient = dcm.ImagePositionPatient
|
79 |
+
if dcm.SliceThickness is not None:
|
80 |
+
self.PixelSpacing = [dcm.PixelSpacing[0], dcm.PixelSpacing[1], dcm.SliceThickness]
|
81 |
+
else:
|
82 |
+
self.PixelSpacing = [dcm.PixelSpacing[0], dcm.PixelSpacing[1], dcm.GridFrameOffsetVector[1]-dcm.GridFrameOffsetVector[0]]
|
83 |
+
self.GridSize = [dcm.Columns, dcm.Rows, dcm.NumberOfFrames]
|
84 |
+
self.NumVoxels = self.GridSize[0] * self.GridSize[1] * self.GridSize[2]
|
85 |
+
|
86 |
+
if hasattr(dcm, 'GridFrameOffsetVector'):
|
87 |
+
if(dcm.GridFrameOffsetVector[1] - dcm.GridFrameOffsetVector[0] < 0):
|
88 |
+
self.Image = np.flip(self.Image, 2)
|
89 |
+
self.ImagePositionPatient[2] = self.ImagePositionPatient[2] - self.GridSize[2]*self.PixelSpacing[2]
|
90 |
+
|
91 |
+
self.resample_to_CT_grid(CT)
|
92 |
+
self.isLoaded = 1
|
93 |
+
|
94 |
+
|
95 |
+
|
96 |
+
def euclidean_dist(self, v1, v2):
|
97 |
+
return sum((p-q)**2 for p, q in zip(v1, v2)) ** .5
|
98 |
+
|
99 |
+
|
100 |
+
|
101 |
+
def resample_to_CT_grid(self, CT):
|
102 |
+
if(self.GridSize == CT.GridSize and self.euclidean_dist(self.ImagePositionPatient, CT.ImagePositionPatient) < 0.001 and self.euclidean_dist(self.PixelSpacing, CT.PixelSpacing) < 0.001):
|
103 |
+
return
|
104 |
+
else:
|
105 |
+
# anti-aliasing filter
|
106 |
+
sigma = [0, 0, 0]
|
107 |
+
if(CT.PixelSpacing[0] > self.PixelSpacing[0]): sigma[0] = 0.4 * (CT.PixelSpacing[0]/self.PixelSpacing[0])
|
108 |
+
if(CT.PixelSpacing[1] > self.PixelSpacing[1]): sigma[1] = 0.4 * (CT.PixelSpacing[1]/self.PixelSpacing[1])
|
109 |
+
if(CT.PixelSpacing[2] > self.PixelSpacing[2]): sigma[2] = 0.4 * (CT.PixelSpacing[2]/self.PixelSpacing[2])
|
110 |
+
if(sigma != [0, 0, 0]):
|
111 |
+
print("Image is filtered before downsampling")
|
112 |
+
self.Image = scipy.ndimage.gaussian_filter(self.Image, sigma)
|
113 |
+
|
114 |
+
|
115 |
+
print('Resample dose image to CT grid.')
|
116 |
+
|
117 |
+
x = self.ImagePositionPatient[1] + np.arange(self.GridSize[1]) * self.PixelSpacing[1]
|
118 |
+
y = self.ImagePositionPatient[0] + np.arange(self.GridSize[0]) * self.PixelSpacing[0]
|
119 |
+
z = self.ImagePositionPatient[2] + np.arange(self.GridSize[2]) * self.PixelSpacing[2]
|
120 |
+
|
121 |
+
xi = np.array(np.meshgrid(CT.VoxelY, CT.VoxelX, CT.VoxelZ))
|
122 |
+
xi = np.rollaxis(xi, 0, 4)
|
123 |
+
xi = xi.reshape((xi.size // 3, 3))
|
124 |
+
|
125 |
+
self.Image = scipy.interpolate.interpn((x,y,z), self.Image, xi, method='linear', fill_value=0, bounds_error=False)
|
126 |
+
self.Image = self.Image.reshape((CT.GridSize[0], CT.GridSize[1], CT.GridSize[2])).transpose(1,0,2)
|
127 |
+
|
128 |
+
self.ImagePositionPatient = CT.ImagePositionPatient
|
129 |
+
self.PixelSpacing = CT.PixelSpacing
|
130 |
+
self.GridSize = CT.GridSize
|
131 |
+
self.NumVoxels = CT.NumVoxels
|
132 |
+
|
133 |
+
|
134 |
+
def load_from_nii(self, dose_nii):
|
135 |
+
|
136 |
+
# load the nii image
|
137 |
+
img = nib.load(dose_nii)
|
138 |
+
|
139 |
+
self.Image = img.get_fdata() ### SHOULD I TRANSPOSE?
|
140 |
+
self.GridSize = self.Image.shape
|
141 |
+
self.PixelSpacing = [img.header['pixdim'][1], img.header['pixdim'][2], img.header['pixdim'][3]]
|
142 |
+
self.ImagePositionPatient = [ img.affine[0][3], img.affine[1][3], img.affine[2][3]]
|
143 |
+
|
144 |
+
def export_Dicom(self, refCT, OutputFile):
|
145 |
+
|
146 |
+
# meta data
|
147 |
+
SOPInstanceUID = pydicom.uid.generate_uid()
|
148 |
+
meta = pydicom.dataset.FileMetaDataset()
|
149 |
+
meta.MediaStorageSOPClassUID = '1.2.840.10008.5.1.4.1.1.481.2' # UID class for RTDOSE
|
150 |
+
meta.MediaStorageSOPInstanceUID = SOPInstanceUID
|
151 |
+
#meta.ImplementationClassUID = '1.2.826.0.1.3680043.1.2.100.5.7.0.47' # from RayStation
|
152 |
+
#meta.ImplementationClassUID = '1.2.826.0.1.3680043.5.5.100.5.7.0.03' # modified OpenTPS
|
153 |
+
meta.ImplementationClassUID = '1.2.826.0.1.3680043.1.2.100.6.40.0.76'# from Halcyon? st. luc breast patients
|
154 |
+
#meta.TransferSyntaxUID = '1.2.840.10008.1.2'
|
155 |
+
|
156 |
+
meta.FileMetaInformationGroupLength = 200
|
157 |
+
#meta.FileMetaInformationVersion =
|
158 |
+
#meta.ImplementationVersionName = 'DicomObjects.NET'
|
159 |
+
# dcm_file.ImplementationVersionName =
|
160 |
+
# dcm_file.SoftwareVersion =
|
161 |
+
|
162 |
+
# dicom dataset
|
163 |
+
dcm_file = pydicom.dataset.FileDataset(OutputFile, {}, file_meta=meta, preamble=b"\0" * 128) # CONFIRM WHAT IS OUTPUTFILE AND WHAT THIS LINE IS DOING
|
164 |
+
|
165 |
+
# transfer syntax
|
166 |
+
dcm_file.file_meta.TransferSyntaxUID = pydicom.uid.ExplicitVRLittleEndian
|
167 |
+
print(dcm_file.file_meta.TransferSyntaxUID)
|
168 |
+
dcm_file.is_little_endian = True
|
169 |
+
dcm_file.is_implicit_VR = False
|
170 |
+
|
171 |
+
# Patient
|
172 |
+
dcm_file.PatientName = refCT.PatientInfo.PatientName #self.PatientInfo.PatientName
|
173 |
+
dcm_file.PatientID = refCT.PatientInfo.PatientID #self.PatientInfo.PatientID
|
174 |
+
dcm_file.PatientBirthDate = refCT.PatientInfo.PatientBirthDate #self.PatientInfo.PatientBirthDate
|
175 |
+
dcm_file.PatientSex = refCT.PatientInfo.PatientSex #self.PatientInfo.PatientSex
|
176 |
+
|
177 |
+
# General Study
|
178 |
+
dt = datetime.datetime.now()
|
179 |
+
dcm_file.StudyDate = dt.strftime('%Y%m%d')
|
180 |
+
dcm_file.StudyTime = dt.strftime('%H%M%S.%f')
|
181 |
+
dcm_file.AccessionNumber = '1' # A RIS/PACS (Radiology Information System/picture archiving and communication system) generated number that identifies the order for the Study.
|
182 |
+
dcm_file.ReferringPhysicianName = 'NA'
|
183 |
+
dcm_file.StudyInstanceUID = refCT.StudyInfo.StudyInstanceUID # get from reference CT to indicate that they belong to the same study#self.StudyInfo.StudyInstanceUID
|
184 |
+
dcm_file.StudyID = refCT.StudyInfo.StudyID # get from reference CT to indicate that they belong to the same study
|
185 |
+
|
186 |
+
# RT Series
|
187 |
+
dcm_file.Modality = 'RTDOSE'
|
188 |
+
dcm_file.SeriesDescription = 'AI-predicted' + dt.strftime('%Y%m%d') + dt.strftime('%H%M%S.%f')#self.ImgName
|
189 |
+
dcm_file.OperatorsName = 'MIRO'
|
190 |
+
dcm_file.SeriesInstanceUID = pydicom.uid.generate_uid() # if we have a uid_base --> pydicom.uid.generate_uid(uid_base)
|
191 |
+
dcm_file.SeriesNumber = 1
|
192 |
+
|
193 |
+
# Frame of Reference
|
194 |
+
dcm_file.FrameOfReferenceUID = refCT.FrameOfReferenceUID # pydicom.uid.generate_uid()
|
195 |
+
dcm_file.PositionReferenceIndicator = '' #empty if unknown https://dicom.innolitics.com/ciods/rt-dose/frame-of-reference/00201040
|
196 |
+
|
197 |
+
# General Equipment
|
198 |
+
dcm_file.Manufacturer = 'echarp'
|
199 |
+
#dcm_file.ManufacturerModelName = 'echarp'
|
200 |
+
#dcm_file.PixelPaddingValue = # conditionally required! https://dicom.innolitics.com/ciods/rt-dose/general-equipment/00280120
|
201 |
+
|
202 |
+
# General Image
|
203 |
+
dcm_file.ContentDate = dt.strftime('%Y%m%d')
|
204 |
+
dcm_file.ContentTime = dt.strftime('%H%M%S.%f')
|
205 |
+
dcm_file.InstanceNumber = 1
|
206 |
+
dcm_file.PatientOrientation = ''
|
207 |
+
|
208 |
+
# Image Plane
|
209 |
+
dcm_file.SliceThickness = self.PixelSpacing[2]
|
210 |
+
dcm_file.ImagePositionPatient = self.ImagePositionPatient
|
211 |
+
dcm_file.ImageOrientationPatient = [1, 0, 0, 0, 1, 0] # HeadFirstSupine=1,0,0,0,1,0 FeetFirstSupine=-1,0,0,0,1,0 HeadFirstProne=-1,0,0,0,-1,0 FeetFirstProne=1,0,0,0,-1,0
|
212 |
+
dcm_file.PixelSpacing = self.PixelSpacing[0:2]
|
213 |
+
|
214 |
+
# Image pixel
|
215 |
+
dcm_file.SamplesPerPixel = 1
|
216 |
+
dcm_file.PhotometricInterpretation = 'MONOCHROME2'
|
217 |
+
dcm_file.Rows = self.GridSize[1]
|
218 |
+
dcm_file.Columns = self.GridSize[0]
|
219 |
+
dcm_file.BitsAllocated = 16
|
220 |
+
dcm_file.BitsStored = 16
|
221 |
+
dcm_file.HighBit = 15
|
222 |
+
dcm_file.BitDepth = 16
|
223 |
+
dcm_file.PixelRepresentation = 0 # 0=unsigned, 1=signed
|
224 |
+
#dcm_file.ColorType = 'grayscale'
|
225 |
+
|
226 |
+
# multi-frame
|
227 |
+
dcm_file.NumberOfFrames = self.GridSize[2]
|
228 |
+
dcm_file.FrameIncrementPointer = pydicom.tag.Tag((0x3004, 0x000c))
|
229 |
+
|
230 |
+
# RT Dose
|
231 |
+
dcm_file.DoseUnits = 'GY'
|
232 |
+
dcm_file.DoseType = 'PHYSICAL' # or 'EFFECTIVE' for RBE dose (but RayStation exports physical dose even if 1.1 factor is already taken into account)
|
233 |
+
dcm_file.DoseSummationType = 'PLAN'
|
234 |
+
dcm_file.GridFrameOffsetVector = list(np.arange(0, self.GridSize[2]*self.PixelSpacing[2], self.PixelSpacing[2]))
|
235 |
+
dcm_file.DoseGridScaling = self.Image.max()/(2**dcm_file.BitDepth - 1)
|
236 |
+
# pixel data
|
237 |
+
dcm_file.PixelData = (self.Image/dcm_file.DoseGridScaling).astype(np.uint16).transpose(2,0,1).tostring() # ALTERNATIVE: self.Image.tobytes()
|
238 |
+
|
239 |
+
#dcm_file.TissueHeterogeneityCorrection = 'IMAGE,ROI_OVERRIDE'
|
240 |
+
# ReferencedPlan = pydicom.dataset.Dataset()
|
241 |
+
# ReferencedPlan.ReferencedSOPClassUID = "1.2.840.10008.5.1.4.1.1.481.8" # ion plan
|
242 |
+
# if(plan_uid == []): ReferencedPlan.ReferencedSOPInstanceUID = self.Plan_SOPInstanceUID
|
243 |
+
# else: ReferencedPlan.ReferencedSOPInstanceUID = plan_uid
|
244 |
+
# dcm_file.ReferencedRTPlanSequence = pydicom.sequence.Sequence([ReferencedPlan])
|
245 |
+
|
246 |
+
# SOP common
|
247 |
+
dcm_file.SpecificCharacterSet = 'ISO_IR 100'
|
248 |
+
dcm_file.InstanceCreationDate = dt.strftime('%Y%m%d')
|
249 |
+
dcm_file.InstanceCreationTime = dt.strftime('%H%M%S.%f')
|
250 |
+
dcm_file.SOPClassUID = meta.MediaStorageSOPClassUID
|
251 |
+
dcm_file.SOPInstanceUID = SOPInstanceUID
|
252 |
+
|
253 |
+
# save dicom file
|
254 |
+
print("Export dicom RTDOSE: " + OutputFile)
|
255 |
+
dcm_file.save_as(OutputFile)
|
256 |
+
|
257 |
+
|
258 |
+
|
259 |
+
|
libraries/Process/RTplan.py
ADDED
@@ -0,0 +1,449 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import pydicom
|
2 |
+
import numpy as np
|
3 |
+
import math
|
4 |
+
import time
|
5 |
+
import pickle, scipy
|
6 |
+
|
7 |
+
|
8 |
+
|
9 |
+
class RTplan:
|
10 |
+
|
11 |
+
def __init__(self):
|
12 |
+
self.SeriesInstanceUID = ""
|
13 |
+
self.SOPInstanceUID = ""
|
14 |
+
self.PatientInfo = {}
|
15 |
+
self.StudyInfo = {}
|
16 |
+
self.DcmFile = ""
|
17 |
+
self.Modality = ""
|
18 |
+
self.RadiationType = ""
|
19 |
+
self.ScanMode = ""
|
20 |
+
self.TreatmentMachineName = ""
|
21 |
+
self.NumberOfFractionsPlanned = 1
|
22 |
+
self.NumberOfSpots = 0
|
23 |
+
self.Beams = []
|
24 |
+
self.TotalMeterset = 0.0
|
25 |
+
self.PlanName = ""
|
26 |
+
self.isLoaded = 0
|
27 |
+
self.beamlets = []
|
28 |
+
self.OriginalDicomDataset = []
|
29 |
+
|
30 |
+
|
31 |
+
|
32 |
+
def print_plan_info(self, prefix=""):
|
33 |
+
print(prefix + "Plan: " + self.SeriesInstanceUID)
|
34 |
+
print(prefix + " " + self.DcmFile)
|
35 |
+
|
36 |
+
|
37 |
+
|
38 |
+
def import_Dicom_plan(self):
|
39 |
+
if(self.isLoaded == 1):
|
40 |
+
print("Warning: RTplan " + self.SeriesInstanceUID + " is already loaded")
|
41 |
+
return
|
42 |
+
|
43 |
+
dcm = pydicom.dcmread(self.DcmFile)
|
44 |
+
|
45 |
+
self.OriginalDicomDataset = dcm
|
46 |
+
|
47 |
+
# Photon plan
|
48 |
+
if dcm.SOPClassUID == "1.2.840.10008.5.1.4.1.1.481.5":
|
49 |
+
print("ERROR: Conventional radiotherapy (photon) plans are not supported")
|
50 |
+
self.Modality = "Radiotherapy"
|
51 |
+
return
|
52 |
+
|
53 |
+
# Ion plan
|
54 |
+
elif dcm.SOPClassUID == "1.2.840.10008.5.1.4.1.1.481.8":
|
55 |
+
self.Modality = "Ion therapy"
|
56 |
+
|
57 |
+
if dcm.IonBeamSequence[0].RadiationType == "PROTON":
|
58 |
+
self.RadiationType = "Proton"
|
59 |
+
else:
|
60 |
+
print("ERROR: Radiation type " + dcm.IonBeamSequence[0].RadiationType + " not supported")
|
61 |
+
self.RadiationType = dcm.IonBeamSequence[0].RadiationType
|
62 |
+
return
|
63 |
+
|
64 |
+
if dcm.IonBeamSequence[0].ScanMode == "MODULATED":
|
65 |
+
self.ScanMode = "MODULATED" # PBS
|
66 |
+
else:
|
67 |
+
print("ERROR: Scan mode " + dcm.IonBeamSequence[0].ScanMode + " not supported")
|
68 |
+
self.ScanMode = dcm.IonBeamSequence[0].ScanMode
|
69 |
+
return
|
70 |
+
|
71 |
+
# Other
|
72 |
+
else:
|
73 |
+
print("ERROR: Unknown SOPClassUID " + dcm.SOPClassUID + " for file " + self.DcmFile)
|
74 |
+
self.Modality = "Unknown"
|
75 |
+
return
|
76 |
+
|
77 |
+
# Start parsing PBS plan
|
78 |
+
self.SOPInstanceUID = dcm.SOPInstanceUID
|
79 |
+
self.NumberOfFractionsPlanned = int(dcm.FractionGroupSequence[0].NumberOfFractionsPlanned)
|
80 |
+
self.NumberOfSpots = 0
|
81 |
+
self.TotalMeterset = 0
|
82 |
+
|
83 |
+
if(hasattr(dcm.IonBeamSequence[0], 'TreatmentMachineName')):
|
84 |
+
self.TreatmentMachineName = dcm.IonBeamSequence[0].TreatmentMachineName
|
85 |
+
else:
|
86 |
+
self.TreatmentMachineName = ""
|
87 |
+
|
88 |
+
for dcm_beam in dcm.IonBeamSequence:
|
89 |
+
if dcm_beam.TreatmentDeliveryType != "TREATMENT":
|
90 |
+
continue
|
91 |
+
|
92 |
+
first_layer = dcm_beam.IonControlPointSequence[0]
|
93 |
+
|
94 |
+
beam = Plan_IonBeam()
|
95 |
+
beam.SeriesInstanceUID = self.SeriesInstanceUID
|
96 |
+
beam.BeamName = dcm_beam.BeamName
|
97 |
+
beam.IsocenterPosition = [float(first_layer.IsocenterPosition[0]), float(first_layer.IsocenterPosition[1]), float(first_layer.IsocenterPosition[2])]
|
98 |
+
beam.GantryAngle = float(first_layer.GantryAngle)
|
99 |
+
beam.PatientSupportAngle = float(first_layer.PatientSupportAngle)
|
100 |
+
beam.FinalCumulativeMetersetWeight = float(dcm_beam.FinalCumulativeMetersetWeight)
|
101 |
+
|
102 |
+
# find corresponding beam in FractionGroupSequence (beam order may be different from IonBeamSequence)
|
103 |
+
ReferencedBeam_id = next((x for x, val in enumerate(dcm.FractionGroupSequence[0].ReferencedBeamSequence) if val.ReferencedBeamNumber == dcm_beam.BeamNumber), -1)
|
104 |
+
if ReferencedBeam_id == -1:
|
105 |
+
print("ERROR: Beam number " + dcm_beam.BeamNumber + " not found in FractionGroupSequence.")
|
106 |
+
print("This beam is therefore discarded.")
|
107 |
+
continue
|
108 |
+
else: beam.BeamMeterset = float(dcm.FractionGroupSequence[0].ReferencedBeamSequence[ReferencedBeam_id].BeamMeterset)
|
109 |
+
|
110 |
+
self.TotalMeterset += beam.BeamMeterset
|
111 |
+
|
112 |
+
if dcm_beam.NumberOfRangeShifters == 0:
|
113 |
+
beam.RangeShifterID = ""
|
114 |
+
beam.RangeShifterType = "none"
|
115 |
+
elif dcm_beam.NumberOfRangeShifters == 1:
|
116 |
+
beam.RangeShifterID = dcm_beam.RangeShifterSequence[0].RangeShifterID
|
117 |
+
if dcm_beam.RangeShifterSequence[0].RangeShifterType == "BINARY":
|
118 |
+
beam.RangeShifterType = "binary"
|
119 |
+
elif dcm_beam.RangeShifterSequence[0].RangeShifterType == "ANALOG":
|
120 |
+
beam.RangeShifterType = "analog"
|
121 |
+
else:
|
122 |
+
print("ERROR: Unknown range shifter type for beam " + dcm_beam.BeamName)
|
123 |
+
beam.RangeShifterType = "none"
|
124 |
+
else:
|
125 |
+
print("ERROR: More than one range shifter defined for beam " + dcm_beam.BeamName)
|
126 |
+
beam.RangeShifterID = ""
|
127 |
+
beam.RangeShifterType = "none"
|
128 |
+
|
129 |
+
|
130 |
+
SnoutPosition = 0
|
131 |
+
if hasattr(first_layer, 'SnoutPosition'):
|
132 |
+
SnoutPosition = float(first_layer.SnoutPosition)
|
133 |
+
|
134 |
+
IsocenterToRangeShifterDistance = SnoutPosition
|
135 |
+
RangeShifterWaterEquivalentThickness = ""
|
136 |
+
RangeShifterSetting = "OUT"
|
137 |
+
ReferencedRangeShifterNumber = 0
|
138 |
+
|
139 |
+
if hasattr(first_layer, 'RangeShifterSettingsSequence'):
|
140 |
+
if hasattr(first_layer.RangeShifterSettingsSequence[0], 'IsocenterToRangeShifterDistance'):
|
141 |
+
IsocenterToRangeShifterDistance = float(first_layer.RangeShifterSettingsSequence[0].IsocenterToRangeShifterDistance)
|
142 |
+
if hasattr(first_layer.RangeShifterSettingsSequence[0], 'RangeShifterWaterEquivalentThickness'):
|
143 |
+
RangeShifterWaterEquivalentThickness = float(first_layer.RangeShifterSettingsSequence[0].RangeShifterWaterEquivalentThickness)
|
144 |
+
if hasattr(first_layer.RangeShifterSettingsSequence[0], 'RangeShifterSetting'):
|
145 |
+
RangeShifterSetting = first_layer.RangeShifterSettingsSequence[0].RangeShifterSetting
|
146 |
+
if hasattr(first_layer.RangeShifterSettingsSequence[0], 'ReferencedRangeShifterNumber'):
|
147 |
+
ReferencedRangeShifterNumber = int(first_layer.RangeShifterSettingsSequence[0].ReferencedRangeShifterNumber)
|
148 |
+
|
149 |
+
CumulativeMeterset = 0
|
150 |
+
|
151 |
+
for dcm_layer in dcm_beam.IonControlPointSequence:
|
152 |
+
if dcm_layer.NumberOfScanSpotPositions == 1: sum_weights = dcm_layer.ScanSpotMetersetWeights
|
153 |
+
else: sum_weights = sum(dcm_layer.ScanSpotMetersetWeights)
|
154 |
+
|
155 |
+
if sum_weights == 0.0:
|
156 |
+
continue
|
157 |
+
|
158 |
+
layer = Plan_IonLayer()
|
159 |
+
layer.SeriesInstanceUID = self.SeriesInstanceUID
|
160 |
+
|
161 |
+
if hasattr(dcm_layer, 'SnoutPosition'):
|
162 |
+
SnoutPosition = float(dcm_layer.SnoutPosition)
|
163 |
+
|
164 |
+
if hasattr(dcm_layer, 'NumberOfPaintings'): layer.NumberOfPaintings = int(dcm_layer.NumberOfPaintings)
|
165 |
+
else: layer.NumberOfPaintings = 1
|
166 |
+
|
167 |
+
layer.NominalBeamEnergy = float(dcm_layer.NominalBeamEnergy)
|
168 |
+
layer.ScanSpotPositionMap_x = dcm_layer.ScanSpotPositionMap[0::2]
|
169 |
+
layer.ScanSpotPositionMap_y = dcm_layer.ScanSpotPositionMap[1::2]
|
170 |
+
layer.ScanSpotMetersetWeights = dcm_layer.ScanSpotMetersetWeights
|
171 |
+
layer.SpotMU = np.array(dcm_layer.ScanSpotMetersetWeights) * beam.BeamMeterset / beam.FinalCumulativeMetersetWeight # spot weights are converted to MU
|
172 |
+
if layer.SpotMU.size == 1: layer.SpotMU = [layer.SpotMU]
|
173 |
+
else: layer.SpotMU = layer.SpotMU.tolist()
|
174 |
+
|
175 |
+
self.NumberOfSpots += len(layer.SpotMU)
|
176 |
+
CumulativeMeterset += sum(layer.SpotMU)
|
177 |
+
layer.CumulativeMeterset = CumulativeMeterset
|
178 |
+
|
179 |
+
if beam.RangeShifterType != "none":
|
180 |
+
if hasattr(dcm_layer, 'RangeShifterSettingsSequence'):
|
181 |
+
RangeShifterSetting = dcm_layer.RangeShifterSettingsSequence[0].RangeShifterSetting
|
182 |
+
ReferencedRangeShifterNumber = dcm_layer.RangeShifterSettingsSequence[0].ReferencedRangeShifterNumber
|
183 |
+
if hasattr(dcm_layer.RangeShifterSettingsSequence[0], 'IsocenterToRangeShifterDistance'):
|
184 |
+
IsocenterToRangeShifterDistance = dcm_layer.RangeShifterSettingsSequence[0].IsocenterToRangeShifterDistance
|
185 |
+
if hasattr(dcm_layer.RangeShifterSettingsSequence[0], 'RangeShifterWaterEquivalentThickness'):
|
186 |
+
RangeShifterWaterEquivalentThickness = dcm_layer.RangeShifterSettingsSequence[0].RangeShifterWaterEquivalentThickness
|
187 |
+
|
188 |
+
layer.RangeShifterSetting = RangeShifterSetting
|
189 |
+
layer.IsocenterToRangeShifterDistance = IsocenterToRangeShifterDistance
|
190 |
+
layer.RangeShifterWaterEquivalentThickness = RangeShifterWaterEquivalentThickness
|
191 |
+
layer.ReferencedRangeShifterNumber = ReferencedRangeShifterNumber
|
192 |
+
|
193 |
+
|
194 |
+
beam.Layers.append(layer)
|
195 |
+
|
196 |
+
self.Beams.append(beam)
|
197 |
+
|
198 |
+
self.isLoaded = 1
|
199 |
+
|
200 |
+
def export_Dicom_with_new_UID(self, OutputFile):
|
201 |
+
# generate new uid
|
202 |
+
initial_uid = self.OriginalDicomDataset.SOPInstanceUID
|
203 |
+
new_uid = pydicom.uid.generate_uid()
|
204 |
+
self.OriginalDicomDataset.SOPInstanceUID = new_uid
|
205 |
+
|
206 |
+
# save dicom file
|
207 |
+
print("Export dicom RTPLAN: " + OutputFile)
|
208 |
+
self.OriginalDicomDataset.save_as(OutputFile)
|
209 |
+
|
210 |
+
# restore initial uid
|
211 |
+
self.OriginalDicomDataset.SOPInstanceUID = initial_uid
|
212 |
+
|
213 |
+
return new_uid
|
214 |
+
|
215 |
+
|
216 |
+
|
217 |
+
def save(self, file_path):
|
218 |
+
beamlets = self.beamlets
|
219 |
+
self.beamlets = []
|
220 |
+
|
221 |
+
with open(file_path, 'wb') as fid:
|
222 |
+
pickle.dump(self.__dict__, fid)
|
223 |
+
|
224 |
+
self.beamlets = beamlets
|
225 |
+
|
226 |
+
|
227 |
+
|
228 |
+
def load(self, file_path):
|
229 |
+
with open(file_path, 'rb') as fid:
|
230 |
+
tmp = pickle.load(fid)
|
231 |
+
|
232 |
+
self.__dict__.update(tmp)
|
233 |
+
|
234 |
+
def compute_cartesian_coordinates(self, CT, Scanner, beams, RangeShifters=[]):
|
235 |
+
time_start = time.time()
|
236 |
+
|
237 |
+
SPR = SPRimage()
|
238 |
+
SPR.convert_CT_to_SPR(CT, Scanner)
|
239 |
+
|
240 |
+
CTborders_x = [SPR.ImagePositionPatient[0], SPR.ImagePositionPatient[0] + SPR.GridSize[0] * SPR.PixelSpacing[0]]
|
241 |
+
CTborders_y = [SPR.ImagePositionPatient[1], SPR.ImagePositionPatient[1] + SPR.GridSize[1] * SPR.PixelSpacing[1]]
|
242 |
+
CTborders_z = [SPR.ImagePositionPatient[2], SPR.ImagePositionPatient[2] + SPR.GridSize[2] * SPR.PixelSpacing[2]]
|
243 |
+
|
244 |
+
spot_positions = []
|
245 |
+
spot_directions = []
|
246 |
+
spot_ranges = []
|
247 |
+
|
248 |
+
# initialize spot info for raytracing
|
249 |
+
for beam in self.Beams:
|
250 |
+
#beam = self.Beams[b]
|
251 |
+
|
252 |
+
RangeShifter = -1
|
253 |
+
if beam.RangeShifterType == "binary":
|
254 |
+
RangeShifter = next((RS for RS in RangeShifters if RS.ID == beam.RangeShifterID), -1)
|
255 |
+
|
256 |
+
for layer in beam.Layers:
|
257 |
+
|
258 |
+
range_in_water = SPR.energyToRange(layer.NominalBeamEnergy)*10
|
259 |
+
if(layer.RangeShifterSetting == 'IN'):
|
260 |
+
if(layer.RangeShifterWaterEquivalentThickness != ""): RangeShifter_WET = layer.RangeShifterWaterEquivalentThickness
|
261 |
+
elif(RangeShifter != -1): RangeShifter_WET = RangeShifter.WET
|
262 |
+
else: RangeShifter_WET = 0.0
|
263 |
+
|
264 |
+
if(RangeShifter_WET > 0.0): range_in_water -= RangeShifter_WET
|
265 |
+
|
266 |
+
for s in range(len(layer.ScanSpotPositionMap_x)):
|
267 |
+
|
268 |
+
# BEV coordinates to 3D coordinates: position (x,y,z) and direction (u,v,w)
|
269 |
+
x,y,z = layer.ScanSpotPositionMap_x[s], 0, layer.ScanSpotPositionMap_y[s]
|
270 |
+
u,v,w = 1e-10, 1.0, 1e-10
|
271 |
+
|
272 |
+
# rotation for gantry angle (around Z axis)
|
273 |
+
angle = math.radians(beam.GantryAngle)
|
274 |
+
[x,y,z] = self.Rotate_vector([x,y,z], angle, 'z')
|
275 |
+
[u,v,w] = self.Rotate_vector([u,v,w], angle, 'z')
|
276 |
+
|
277 |
+
# rotation for couch angle (around Y axis)
|
278 |
+
angle = math.radians(beam.PatientSupportAngle)
|
279 |
+
[x,y,z] = self.Rotate_vector([x,y,z], angle, 'y')
|
280 |
+
[u,v,w] = self.Rotate_vector([u,v,w], angle, 'y')
|
281 |
+
|
282 |
+
# Dicom CT coordinates
|
283 |
+
x = x + beam.IsocenterPosition[0]
|
284 |
+
y = y + beam.IsocenterPosition[1]
|
285 |
+
z = z + beam.IsocenterPosition[2]
|
286 |
+
|
287 |
+
# translate initial position at the CT image border
|
288 |
+
Translation = np.array([1.0, 1.0, 1.0])
|
289 |
+
Translation[0] = (x - CTborders_x[int(u<0)]) / u
|
290 |
+
Translation[1] = (y - CTborders_y[int(v<0)]) / v
|
291 |
+
Translation[2] = (z - CTborders_z[int(w<0)]) / w
|
292 |
+
Translation = Translation[np.argmin(np.absolute(Translation))]
|
293 |
+
x = x - Translation * u
|
294 |
+
y = y - Translation * v
|
295 |
+
z = z - Translation * w
|
296 |
+
|
297 |
+
# append data to the list of spots to process
|
298 |
+
spot_positions.append([x,y,z])
|
299 |
+
spot_directions.append([u,v,w])
|
300 |
+
spot_ranges.append(range_in_water)
|
301 |
+
|
302 |
+
|
303 |
+
CartesianSpotPositions = compute_position_from_range(SPR, spot_positions, spot_directions, spot_ranges)
|
304 |
+
|
305 |
+
print("Spot RayTracing: " + str(time.time()-time_start) + " sec")
|
306 |
+
return CartesianSpotPositions
|
307 |
+
|
308 |
+
#TO ADAPT
|
309 |
+
def compute_spot_maps(self, CT, plan, Struct, RangeShifters):
|
310 |
+
|
311 |
+
# Find BODY
|
312 |
+
for ROI in Struct.Contours:
|
313 |
+
if ROI.ROIName == 'BODY':
|
314 |
+
BODY = ROI
|
315 |
+
break
|
316 |
+
|
317 |
+
# Initialize Spot Maps
|
318 |
+
SpotMapBinary = np.full((CT.GridSize[0], CT.GridSize[1], CT.GridSize[2], len(plan.Beams)), False)
|
319 |
+
SpotMapWeights = np.zeros((CT.GridSize[0], CT.GridSize[1], CT.GridSize[2], len(plan.Beams)))
|
320 |
+
|
321 |
+
|
322 |
+
#Compute cartesian coordinates
|
323 |
+
CartesianCoordinates = plan.compute_cartesian_coordinates(CT, 'UCL_Toshiba', plan.Beams, RangeShifters)
|
324 |
+
|
325 |
+
# Initialize SpotID
|
326 |
+
SpotsID = np.zeros((plan.NumberOfSpots, 4),dtype=int)
|
327 |
+
SpotNumber = 0
|
328 |
+
SpotsOutsideBody = 0
|
329 |
+
# Repeat = []
|
330 |
+
# SpotsID_repeat = []
|
331 |
+
|
332 |
+
print('ImagePositionPatient',CT.ImagePositionPatient)
|
333 |
+
# Compute SpotsID
|
334 |
+
print('\n')
|
335 |
+
counter = 0
|
336 |
+
for beamNumber, beam in enumerate(plan.Beams):
|
337 |
+
print('nb of layers',len(beam.Layers))
|
338 |
+
for layer in beam.Layers:
|
339 |
+
for weight in layer.SpotMU:
|
340 |
+
#print('SpotNumber',SpotNumber)
|
341 |
+
#print('CartesianCoordinates',CartesianCoordinates[SpotNumber])
|
342 |
+
#print('ImagePositionPatient',CT.ImagePositionPatient)
|
343 |
+
#print('PixelSpacing',CT.PixelSpacing)
|
344 |
+
# x = int((CartesianCoordinates[SpotNumber][0] - CT.ImagePositionPatient[0])/CT.PixelSpacing[0])-1
|
345 |
+
# y = int((CartesianCoordinates[SpotNumber][1] - CT.ImagePositionPatient[1])/CT.PixelSpacing[1])-1
|
346 |
+
# z = int((CartesianCoordinates[SpotNumber][2] - CT.ImagePositionPatient[2])/CT.PixelSpacing[2])-1
|
347 |
+
|
348 |
+
x = int(CartesianCoordinates[SpotNumber][0]/CT.PixelSpacing[0])
|
349 |
+
y = int(CartesianCoordinates[SpotNumber][1]/CT.PixelSpacing[1])
|
350 |
+
z = int(CartesianCoordinates[SpotNumber][2]/CT.PixelSpacing[2])
|
351 |
+
|
352 |
+
SpotsID[SpotNumber,:] = [x,y,z,beamNumber]
|
353 |
+
#print('coordinates',[x,y,z,beamNumber])
|
354 |
+
#print('Spots ID',SpotsID[SpotNumber,:])
|
355 |
+
|
356 |
+
SpotNumber += 1
|
357 |
+
|
358 |
+
if BODY.Mask[x,y,z]==False: # avoid spots outside BODY
|
359 |
+
SpotsOutsideBody += 1
|
360 |
+
print('This spot is outside the body',[x,y,z])
|
361 |
+
continue
|
362 |
+
|
363 |
+
# if (x,y,z,beamNumber) in SpotsID_repeat:
|
364 |
+
# Repeat.append((x,y,z,beamNumber))
|
365 |
+
|
366 |
+
# SpotsID_repeat.append((x,y,z,beamNumber))
|
367 |
+
SpotMapBinary[x,y,z,beamNumber] = True
|
368 |
+
SpotMapWeights[x,y,z,beamNumber] = weight
|
369 |
+
counter+=1
|
370 |
+
# We can get SpotMapBinary as boolean SpotMapWeights
|
371 |
+
# plan.SpotMapBinary = SpotMapBinary
|
372 |
+
plan.SpotMapWeights = SpotMapWeights
|
373 |
+
|
374 |
+
# print('SpotsID beam_' + str(beamNumber) + ' computed...')
|
375 |
+
# print('nb of iteration',counter)
|
376 |
+
# save_file_binary = os.path.join(dst_dir, 'SpotMapBinary_beams.npz')
|
377 |
+
# np.savez_compressed(save_file_binary,SpotMapBinary)
|
378 |
+
# print('\nSpot Map Binary saved = ' + save_file_binary)
|
379 |
+
|
380 |
+
# save_file_weights = os.path.join(dst_dir, 'SpotMapWeights_beams.npz')
|
381 |
+
# np.savez_compressed(save_file_weights, SpotMapWeights.astype(np.float16))
|
382 |
+
# print('Spot Map Weights saved = ' + save_file_weights)
|
383 |
+
|
384 |
+
# save_file_ID = os.path.join(dst_dir, 'spotsID.npz')
|
385 |
+
# np.savez_compressed(save_file_ID, SpotsID)
|
386 |
+
# print('SpotsID saved = ' + save_file_ID)
|
387 |
+
|
388 |
+
# print('\nNumber of Spots in plan = ' + str(plan.NumberOfSpots))
|
389 |
+
# print('Number of Spots in binary mask = ' + str(np.ndarray.flatten(SpotMapBinary).tolist().count(True)))
|
390 |
+
# print('Spots placed outside the body = ' + (str(SpotsOutsideBody)))
|
391 |
+
|
392 |
+
# if len(Repeat)!=0:
|
393 |
+
# spots_repeated_by_patient.append(['Patient_' + str(PatientNumber) + ' (' + Patient.PatientInfo.PatientName + ')', len(Repeat)])
|
394 |
+
|
395 |
+
# print('\nAt this point, there are ' + str(len(spots_repeated_by_patient)) + ' patients with spots repeated')
|
396 |
+
# for repeat in spots_repeated_by_patient:
|
397 |
+
# print(repeat[0] + ' has ' + str(repeat[1]) + ' spots repeated')
|
398 |
+
# print('\n')
|
399 |
+
|
400 |
+
def Rotate_vector(self, vec, angle, axis):
|
401 |
+
if axis == 'x':
|
402 |
+
x = vec[0]
|
403 |
+
y = vec[1] * math.cos(angle) - vec[2] * math.sin(angle)
|
404 |
+
z = vec[1] * math.sin(angle) + vec[2] * math.cos(angle)
|
405 |
+
elif axis == 'y':
|
406 |
+
x = vec[0] * math.cos(angle) + vec[2] * math.sin(angle)
|
407 |
+
y = vec[1]
|
408 |
+
z = -vec[0] * math.sin(angle) + vec[2] * math.cos(angle)
|
409 |
+
elif axis == 'z':
|
410 |
+
x = vec[0] * math.cos(angle) - vec[1] * math.sin(angle)
|
411 |
+
y = vec[0] * math.sin(angle) + vec[1] * math.cos(angle)
|
412 |
+
z = vec[2]
|
413 |
+
|
414 |
+
return [x,y,z]
|
415 |
+
|
416 |
+
|
417 |
+
class Plan_IonBeam:
|
418 |
+
|
419 |
+
def __init__(self):
|
420 |
+
self.SeriesInstanceUID = ""
|
421 |
+
self.BeamName = ""
|
422 |
+
self.IsocenterPosition = [0,0,0]
|
423 |
+
self.GantryAngle = 0.0
|
424 |
+
self.PatientSupportAngle = 0.0
|
425 |
+
self.FinalCumulativeMetersetWeight = 0.0
|
426 |
+
self.BeamMeterset = 0.0
|
427 |
+
self.RangeShifter = "none"
|
428 |
+
self.Layers = []
|
429 |
+
|
430 |
+
|
431 |
+
|
432 |
+
class Plan_IonLayer:
|
433 |
+
|
434 |
+
def __init__(self):
|
435 |
+
self.SeriesInstanceUID = ""
|
436 |
+
self.NumberOfPaintings = 1
|
437 |
+
self.NominalBeamEnergy = 0.0
|
438 |
+
self.ScanSpotPositionMap_x = []
|
439 |
+
self.ScanSpotPositionMap_y = []
|
440 |
+
self.ScanSpotMetersetWeights = []
|
441 |
+
self.SpotMU = []
|
442 |
+
self.CumulativeMeterset = 0.0
|
443 |
+
self.RangeShifterSetting = 'OUT'
|
444 |
+
self.IsocenterToRangeShifterDistance = 0.0
|
445 |
+
self.RangeShifterWaterEquivalentThickness = 0.0
|
446 |
+
self.ReferencedRangeShifterNumber = 0
|
447 |
+
|
448 |
+
|
449 |
+
|
libraries/Process/RTstruct.py
ADDED
@@ -0,0 +1,542 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import pydicom
|
2 |
+
import numpy as np
|
3 |
+
import nibabel as nib
|
4 |
+
#from matplotlib.path import Path
|
5 |
+
from PIL import Image, ImageDraw
|
6 |
+
import scipy
|
7 |
+
import datetime
|
8 |
+
import SimpleITK as sitk
|
9 |
+
|
10 |
+
|
11 |
+
def Taubin_smoothing(contour):
|
12 |
+
""" Here, we do smoothing in 2D contours!
|
13 |
+
Parameters:
|
14 |
+
a Nx2 numpy array containing the contour to smooth
|
15 |
+
Returns:
|
16 |
+
a Nx2 numpy array containing the smoothed contour """
|
17 |
+
smoothingloops = 5
|
18 |
+
smoothed = [np.empty_like(contour) for i in range(smoothingloops+1)]
|
19 |
+
smoothed[0] = contour
|
20 |
+
for i in range(smoothingloops):
|
21 |
+
# loop over all elements in the contour
|
22 |
+
for vertex_i in range(smoothed[0].shape[0]):
|
23 |
+
if vertex_i == 0:
|
24 |
+
vertex_prev = smoothed[i].shape[0]-1
|
25 |
+
vertex_next = vertex_i+1
|
26 |
+
elif vertex_i == smoothed[i].shape[0]-1:
|
27 |
+
vertex_prev = vertex_i-1
|
28 |
+
vertex_next = 0
|
29 |
+
else:
|
30 |
+
vertex_prev = vertex_i -1
|
31 |
+
vertex_next = vertex_i +1
|
32 |
+
neighbours_x = np.array([smoothed[i][vertex_prev,0], smoothed[i][vertex_next,0]])
|
33 |
+
neighbours_y = np.array([smoothed[i][vertex_prev,1], smoothed[i][vertex_next,1]])
|
34 |
+
smoothed[i+1][vertex_i,0] = smoothed[i][vertex_i,0] - 0.3*(smoothed[i][vertex_i,0] - np.mean(neighbours_x))
|
35 |
+
smoothed[i+1][vertex_i,1] = smoothed[i][vertex_i,1] - 0.3*(smoothed[i][vertex_i,1] - np.mean(neighbours_y))
|
36 |
+
|
37 |
+
return np.round(smoothed[smoothingloops],3)
|
38 |
+
|
39 |
+
class RTstruct:
|
40 |
+
|
41 |
+
def __init__(self):
|
42 |
+
self.SeriesInstanceUID = ""
|
43 |
+
self.PatientInfo = {}
|
44 |
+
self.StudyInfo = {}
|
45 |
+
self.CT_SeriesInstanceUID = ""
|
46 |
+
self.DcmFile = ""
|
47 |
+
self.isLoaded = 0
|
48 |
+
self.Contours = []
|
49 |
+
self.NumContours = 0
|
50 |
+
|
51 |
+
|
52 |
+
def print_struct_info(self, prefix=""):
|
53 |
+
print(prefix + "Struct: " + self.SeriesInstanceUID)
|
54 |
+
print(prefix + " " + self.DcmFile)
|
55 |
+
|
56 |
+
|
57 |
+
def print_ROINames(self):
|
58 |
+
print("RT Struct UID: " + self.SeriesInstanceUID)
|
59 |
+
count = -1
|
60 |
+
for contour in self.Contours:
|
61 |
+
count += 1
|
62 |
+
print(' [' + str(count) + '] ' + contour.ROIName)
|
63 |
+
|
64 |
+
def resample_struct(self, newvoxelsize):
|
65 |
+
# Rescaling to the newvoxelsize if given in parameter
|
66 |
+
|
67 |
+
for i, Contour in enumerate(self.Contours):
|
68 |
+
source_shape = Contour.Mask_GridSize
|
69 |
+
voxelsize = Contour.Mask_PixelSpacing
|
70 |
+
VoxelX_source = Contour.Mask_Offset[0] + np.arange(source_shape[0])*voxelsize[0]
|
71 |
+
VoxelY_source = Contour.Mask_Offset[1] + np.arange(source_shape[1])*voxelsize[1]
|
72 |
+
VoxelZ_source = Contour.Mask_Offset[2] + np.arange(source_shape[2])*voxelsize[2]
|
73 |
+
|
74 |
+
target_shape = np.ceil(np.array(source_shape).astype(float)*np.array(voxelsize).astype(float)/newvoxelsize).astype(int)
|
75 |
+
VoxelX_target = Contour.Mask_Offset[0] + np.arange(target_shape[0])*newvoxelsize[0]
|
76 |
+
VoxelY_target = Contour.Mask_Offset[1] + np.arange(target_shape[1])*newvoxelsize[1]
|
77 |
+
VoxelZ_target = Contour.Mask_Offset[2] + np.arange(target_shape[2])*newvoxelsize[2]
|
78 |
+
|
79 |
+
contour = Contour.Mask
|
80 |
+
|
81 |
+
if(all(source_shape == target_shape) and np.linalg.norm(np.subtract(voxelsize, newvoxelsize) < 0.001)):
|
82 |
+
print("! Image does not need filtering")
|
83 |
+
else:
|
84 |
+
# anti-aliasing filter
|
85 |
+
sigma = [0, 0, 0]
|
86 |
+
if(newvoxelsize[0] > voxelsize[0]): sigma[0] = 0.4 * (newvoxelsize[0]/voxelsize[0])
|
87 |
+
if(newvoxelsize[1] > voxelsize[1]): sigma[1] = 0.4 * (newvoxelsize[1]/voxelsize[1])
|
88 |
+
if(newvoxelsize[2] > voxelsize[2]): sigma[2] = 0.4 * (newvoxelsize[2]/voxelsize[2])
|
89 |
+
|
90 |
+
if(sigma != [0, 0, 0]):
|
91 |
+
contour = scipy.ndimage.gaussian_filter(contour.astype(float), sigma)
|
92 |
+
#come back to binary
|
93 |
+
contour[np.where(contour>=0.5)] = 1
|
94 |
+
contour[np.where(contour<0.5)] = 0
|
95 |
+
|
96 |
+
xi = np.array(np.meshgrid(VoxelX_target, VoxelY_target, VoxelZ_target))
|
97 |
+
xi = np.rollaxis(xi, 0, 4)
|
98 |
+
xi = xi.reshape((xi.size // 3, 3))
|
99 |
+
|
100 |
+
# get resized ct
|
101 |
+
contour = scipy.interpolate.interpn((VoxelX_source,VoxelY_source,VoxelZ_source), contour, xi, method='nearest', fill_value=0, bounds_error=False).astype(bool).reshape(target_shape).transpose(1,0,2)
|
102 |
+
Contour.Mask_PixelSpacing = newvoxelsize
|
103 |
+
Contour.Mask_GridSize = list(contour.shape)
|
104 |
+
Contour.NumVoxels = Contour.Mask_GridSize[0] * Contour.Mask_GridSize[1] * Contour.Mask_GridSize[2]
|
105 |
+
Contour.Mask = contour
|
106 |
+
self.Contours[i]=Contour
|
107 |
+
|
108 |
+
|
109 |
+
def import_Dicom_struct(self, CT):
|
110 |
+
if(self.isLoaded == 1):
|
111 |
+
print("Warning: RTstruct " + self.SeriesInstanceUID + " is already loaded")
|
112 |
+
return
|
113 |
+
dcm = pydicom.dcmread(self.DcmFile)
|
114 |
+
|
115 |
+
self.CT_SeriesInstanceUID = CT.SeriesInstanceUID
|
116 |
+
|
117 |
+
for dcm_struct in dcm.StructureSetROISequence:
|
118 |
+
ReferencedROI_id = next((x for x, val in enumerate(dcm.ROIContourSequence) if val.ReferencedROINumber == dcm_struct.ROINumber), -1)
|
119 |
+
dcm_contour = dcm.ROIContourSequence[ReferencedROI_id]
|
120 |
+
|
121 |
+
Contour = ROIcontour()
|
122 |
+
Contour.SeriesInstanceUID = self.SeriesInstanceUID
|
123 |
+
Contour.ROIName = dcm_struct.ROIName
|
124 |
+
Contour.ROIDisplayColor = dcm_contour.ROIDisplayColor
|
125 |
+
|
126 |
+
print("Import contour " + str(len(self.Contours)) + ": " + Contour.ROIName)
|
127 |
+
|
128 |
+
Contour.Mask = np.zeros((CT.GridSize[0], CT.GridSize[1], CT.GridSize[2]), dtype=np.bool)
|
129 |
+
Contour.Mask_GridSize = CT.GridSize
|
130 |
+
Contour.Mask_PixelSpacing = CT.PixelSpacing
|
131 |
+
Contour.Mask_Offset = CT.ImagePositionPatient
|
132 |
+
Contour.Mask_NumVoxels = CT.NumVoxels
|
133 |
+
Contour.ContourMask = np.zeros((CT.GridSize[0], CT.GridSize[1], CT.GridSize[2]), dtype=np.bool)
|
134 |
+
|
135 |
+
SOPInstanceUID_match = 1
|
136 |
+
|
137 |
+
if not hasattr(dcm_contour, 'ContourSequence'):
|
138 |
+
print("The structure [ ", dcm_struct.ROIName ," ] has no attribute ContourSequence. Skipping ...")
|
139 |
+
continue
|
140 |
+
|
141 |
+
for dcm_slice in dcm_contour.ContourSequence:
|
142 |
+
Slice = {}
|
143 |
+
|
144 |
+
# list of Dicom coordinates
|
145 |
+
Slice["XY_dcm"] = list(zip( np.array(dcm_slice.ContourData[0::3]), np.array(dcm_slice.ContourData[1::3]) ))
|
146 |
+
Slice["Z_dcm"] = float(dcm_slice.ContourData[2])
|
147 |
+
|
148 |
+
# list of coordinates in the image frame
|
149 |
+
Slice["XY_img"] = list(zip( ((np.array(dcm_slice.ContourData[0::3]) - CT.ImagePositionPatient[0]) / CT.PixelSpacing[0]), ((np.array(dcm_slice.ContourData[1::3]) - CT.ImagePositionPatient[1]) / CT.PixelSpacing[1]) ))
|
150 |
+
Slice["Z_img"] = (Slice["Z_dcm"] - CT.ImagePositionPatient[2]) / CT.PixelSpacing[2]
|
151 |
+
Slice["Slice_id"] = int(round(Slice["Z_img"]))
|
152 |
+
|
153 |
+
# convert polygon to mask (based on matplotlib - slow)
|
154 |
+
#x, y = np.meshgrid(np.arange(CT.GridSize[0]), np.arange(CT.GridSize[1]))
|
155 |
+
#points = np.transpose((x.ravel(), y.ravel()))
|
156 |
+
#path = Path(Slice["XY_img"])
|
157 |
+
#mask = path.contains_points(points)
|
158 |
+
#mask = mask.reshape((CT.GridSize[0], CT.GridSize[1]))
|
159 |
+
|
160 |
+
# convert polygon to mask (based on PIL - fast)
|
161 |
+
img = Image.new('L', (CT.GridSize[0], CT.GridSize[1]), 0)
|
162 |
+
if(len(Slice["XY_img"]) > 1): ImageDraw.Draw(img).polygon(Slice["XY_img"], outline=1, fill=1)
|
163 |
+
mask = np.array(img)
|
164 |
+
Contour.Mask[:,:,Slice["Slice_id"]] = np.logical_xor(Contour.Mask[:,:,Slice["Slice_id"]], mask)
|
165 |
+
|
166 |
+
# do the same, but only keep contour in the mask
|
167 |
+
img = Image.new('L', (CT.GridSize[0], CT.GridSize[1]), 0)
|
168 |
+
if(len(Slice["XY_img"]) > 1): ImageDraw.Draw(img).polygon(Slice["XY_img"], outline=1, fill=0)
|
169 |
+
mask = np.array(img)
|
170 |
+
Contour.ContourMask[:,:,Slice["Slice_id"]] = np.logical_or(Contour.ContourMask[:,:,Slice["Slice_id"]], mask)
|
171 |
+
|
172 |
+
Contour.ContourSequence.append(Slice)
|
173 |
+
|
174 |
+
# check if the contour sequence is imported on the correct CT slice:
|
175 |
+
if(hasattr(dcm_slice, 'ContourImageSequence') and CT.SOPInstanceUIDs[Slice["Slice_id"]] != dcm_slice.ContourImageSequence[0].ReferencedSOPInstanceUID):
|
176 |
+
SOPInstanceUID_match = 0
|
177 |
+
|
178 |
+
if SOPInstanceUID_match != 1:
|
179 |
+
print("WARNING: some SOPInstanceUIDs don't match during importation of " + Contour.ROIName + " contour on CT image")
|
180 |
+
|
181 |
+
self.Contours.append(Contour)
|
182 |
+
self.NumContours += 1
|
183 |
+
#print("self.NumContours",self.NumContours, len(self.Contours))
|
184 |
+
self.isLoaded = 1
|
185 |
+
|
186 |
+
def load_from_nii(self, struct_nii_path, rtstruct_labels, rtstruct_colors, data_format, flip):
|
187 |
+
|
188 |
+
# data_format can be either "integers" or "one-hot" (i.e. 2**i)
|
189 |
+
|
190 |
+
#flip if needed
|
191 |
+
if flip == True:
|
192 |
+
#struct_nib = nib.orientations.flip_axis(struct_nib, axis=0)
|
193 |
+
img = sitk.ReadImage(struct_nii_path)
|
194 |
+
img2 = sitk.PermuteAxes(img, [1,0,2])
|
195 |
+
sitk.WriteImage(img2, struct_nii_path)
|
196 |
+
|
197 |
+
# load the nii image
|
198 |
+
struct_nib = nib.load(struct_nii_path)
|
199 |
+
struct_data = struct_nib.get_fdata()
|
200 |
+
|
201 |
+
# load the file according to data_format
|
202 |
+
if data_format == 'integers':
|
203 |
+
roinumbers = np.unique(struct_data)
|
204 |
+
roinumbers = roinumbers[roinumbers > 0] # assumes we have a background = 0
|
205 |
+
print(roinumbers)
|
206 |
+
elif data_format == 'one-hot':
|
207 |
+
roinumbers = list(np.arange(np.floor(np.log2(np.max(struct_data))).astype(int)+1)) # CAREFUL WITH THIS LINE, MIGHT NOT WORK IF WE HAVE OVERLAP OF STRUCTURES
|
208 |
+
|
209 |
+
# get contourexists from header or compute it from labels and data values
|
210 |
+
if hasattr(struct_nib.header, 'extensions') and len(struct_nib.header.extensions) != 0:
|
211 |
+
contoursexist = list(struct_nib.header.extensions[0].get_content())
|
212 |
+
else:
|
213 |
+
if data_format == 'integers':
|
214 |
+
contoursexist = []
|
215 |
+
for i in range(len(rtstruct_labels)):
|
216 |
+
if i+1 in roinumbers:
|
217 |
+
contoursexist.append(1)
|
218 |
+
else:
|
219 |
+
contoursexist.append(0)
|
220 |
+
elif data_format == 'one-hot': # AGAIN, THIS ONLY WORKS IF WE DONT HAVE OVERLAP
|
221 |
+
contoursexist = []
|
222 |
+
for i in range(len(rtstruct_labels)):
|
223 |
+
if 2**i in roinumbers:
|
224 |
+
contoursexist.append(1)
|
225 |
+
else:
|
226 |
+
contoursexist.append(0)
|
227 |
+
|
228 |
+
# get number of rois in struct_data
|
229 |
+
nb_rois_in_struct = len(roinumbers)
|
230 |
+
self.NumContours = len(rtstruct_labels)
|
231 |
+
# check that they match
|
232 |
+
if not len(rtstruct_labels) == len(contoursexist) == nb_rois_in_struct:
|
233 |
+
#raise TypeError("The number or struct labels, contoursexist, and masks in struct.nii.gz is not the same")
|
234 |
+
raise Warning("The number or struct labels, contoursexist, and estimated masks in struct.nii.gz is not the same. Taking len(rtstruct_labels) as number of rois")
|
235 |
+
|
236 |
+
# fill in contours
|
237 |
+
#TODO fill in ContourSequence and ContourData to be faster later in writeDicomRTstruct
|
238 |
+
for c in range(self.NumContours):
|
239 |
+
|
240 |
+
Contour = ROIcontour()
|
241 |
+
Contour.SeriesInstanceUID = self.SeriesInstanceUID
|
242 |
+
Contour.ROIName = rtstruct_labels[c]
|
243 |
+
if rtstruct_colors[c] == None:
|
244 |
+
Contour.ROIDisplayColor = [0, 0, 255] # default color is blue
|
245 |
+
else:
|
246 |
+
Contour.ROIDisplayColor = rtstruct_colors[c]
|
247 |
+
if contoursexist[c] == 0:
|
248 |
+
Contour.Mask = np.zeros((struct_nib.header['dim'][1], struct_nib.header['dim'][2], struct_nib.header['dim'][3]), dtype=np.bool)
|
249 |
+
else:
|
250 |
+
if data_format == 'integers':
|
251 |
+
Contour.Mask = (struct_data == c+1).astype(bool)
|
252 |
+
elif data_format == 'one-hot':
|
253 |
+
Contour.Mask = np.bitwise_and(struct_data.astype(int), 2 ** c).astype(bool)
|
254 |
+
#TODO enable option for consecutive integers masks?
|
255 |
+
Contour.Mask_GridSize = [struct_nib.header['dim'][1], struct_nib.header['dim'][2], struct_nib.header['dim'][3]]
|
256 |
+
Contour.Mask_PixelSpacing = [struct_nib.header['pixdim'][1], struct_nib.header['pixdim'][2], struct_nib.header['pixdim'][3]]
|
257 |
+
Contour.Mask_Offset = [struct_nib.header['qoffset_x'], struct_nib.header['qoffset_y'], struct_nib.header['qoffset_z']]
|
258 |
+
Contour.Mask_NumVoxels = struct_nib.header['dim'][1].astype(int) * struct_nib.header['dim'][2].astype(int) * struct_nib.header['dim'][3].astype(int)
|
259 |
+
# Contour.ContourMask --> this should be only the contour, so far we don't need it so I'll skip it
|
260 |
+
|
261 |
+
# apend to self
|
262 |
+
self.Contours.append(Contour)
|
263 |
+
|
264 |
+
|
265 |
+
def export_Dicom(self, refCT, outputFile):
|
266 |
+
|
267 |
+
# meta data
|
268 |
+
|
269 |
+
# generate UID
|
270 |
+
#uid_base = '' #TODO define one for us if we want? Siri is using: uid_base='1.2.826.0.1.3680043.10.230.',
|
271 |
+
# personal UID, applied for via https://www.medicalconnections.co.uk/FreeUID/
|
272 |
+
|
273 |
+
SOPInstanceUID = pydicom.uid.generate_uid() #TODO verify this! Siri was using a uid_base, this line is taken from OpenTPS writeRTPlan
|
274 |
+
#SOPInstanceUID = pydicom.uid.generate_uid('1.2.840.10008.5.1.4.1.1.481.3.') # siri's version
|
275 |
+
|
276 |
+
meta = pydicom.dataset.FileMetaDataset()
|
277 |
+
meta.MediaStorageSOPClassUID = '1.2.840.10008.5.1.4.1.1.481.3' # UID class for RTSTRUCT
|
278 |
+
meta.MediaStorageSOPInstanceUID = SOPInstanceUID
|
279 |
+
# meta.ImplementationClassUID = uid_base + '1.1.1' # Siri's
|
280 |
+
meta.ImplementationClassUID = '1.2.250.1.59.3.0.3.5.0' # from OpenREGGUI
|
281 |
+
meta.TransferSyntaxUID = '1.2.840.10008.1.2' # Siri's and OpenREGGUI
|
282 |
+
meta.FileMetaInformationGroupLength = 188 # from Siri
|
283 |
+
# meta.ImplementationVersionName = 'DCIE 2.2' # from Siri
|
284 |
+
|
285 |
+
|
286 |
+
# Main data elements - only required fields, optional fields like StudyDescription are not included for simplicity
|
287 |
+
ds = pydicom.dataset.FileDataset(outputFile, {}, file_meta=meta, preamble=b"\0" * 128) # preamble is taken from this example https://pydicom.github.io/pydicom/dev/auto_examples/input_output/plot_write_dicom.html#sphx-glr-auto-examples-input-output-plot-write-dicom-py
|
288 |
+
|
289 |
+
# Patient info - will take it from the referenced CT image
|
290 |
+
ds.PatientName = refCT.PatientInfo.PatientName
|
291 |
+
ds.PatientID = refCT.PatientInfo.PatientID
|
292 |
+
ds.PatientBirthDate = refCT.PatientInfo.PatientBirthDate
|
293 |
+
ds.PatientSex = refCT.PatientInfo.PatientSex
|
294 |
+
|
295 |
+
# General Study
|
296 |
+
dt = datetime.datetime.now()
|
297 |
+
ds.StudyDate = dt.strftime('%Y%m%d')
|
298 |
+
ds.StudyTime = dt.strftime('%H%M%S.%f')
|
299 |
+
ds.AccessionNumber = '1' # A RIS/PACS (Radiology Information System/picture archiving and communication system) generated number that identifies the order for the Study.
|
300 |
+
ds.ReferringPhysicianName = 'NA'
|
301 |
+
ds.StudyInstanceUID = refCT.StudyInfo.StudyInstanceUID # get from reference CT to indicate that they belong to the same study
|
302 |
+
ds.StudyID = refCT.StudyInfo.StudyID # get from reference CT to indicate that they belong to the same study
|
303 |
+
|
304 |
+
# RT Series
|
305 |
+
#ds.SeriesDate # optional
|
306 |
+
#ds.SeriesTime # optional
|
307 |
+
ds.Modality = 'RTSTRUCT'
|
308 |
+
ds.SeriesDescription = 'AI-predicted' + dt.strftime('%Y%m%d') + dt.strftime('%H%M%S.%f')
|
309 |
+
ds.OperatorsName = 'MIRO AI team'
|
310 |
+
ds.SeriesInstanceUID = pydicom.uid.generate_uid() # if we have a uid_base --> pydicom.uid.generate_uid(uid_base)
|
311 |
+
ds.SeriesNumber = '1'
|
312 |
+
|
313 |
+
# General Equipment
|
314 |
+
ds.Manufacturer = 'MIRO lab'
|
315 |
+
#ds.InstitutionName = 'MIRO lab' # optional
|
316 |
+
#ds.ManufacturerModelName = 'nnUNet' # optional, but can be a good tag to insert the model information or label
|
317 |
+
#ds.SoftwareVersions # optional, but can be used to insert the version of the code in ECHARP or the version of the model
|
318 |
+
|
319 |
+
# Frame of Reference
|
320 |
+
ds.FrameOfReferenceUID = refCT.FrameOfReferenceUID
|
321 |
+
ds.PositionReferenceIndicator = '' # empty if unknown - info here https://dicom.innolitics.com/ciods/rt-structure-set/frame-of-reference/00201040
|
322 |
+
|
323 |
+
# Structure Set
|
324 |
+
ds.StructureSetLabel = 'AI predicted' # do not use - or spetial characters or the Dicom Validation in Raystation will give a warning
|
325 |
+
#ds.StructureSetName # optional
|
326 |
+
#ds.StructureSetDescription # optional
|
327 |
+
ds.StructureSetDate = dt.strftime('%Y%m%d')
|
328 |
+
ds.StructureSetTime = dt.strftime('%H%M%S.%f')
|
329 |
+
ds.ReferencedFrameOfReferenceSequence = pydicom.Sequence()# optional
|
330 |
+
# we assume there is only one, the CT
|
331 |
+
dssr = pydicom.Dataset()
|
332 |
+
dssr.FrameOfReferenceUID = refCT.FrameOfReferenceUID
|
333 |
+
dssr.RTReferencedStudySequence = pydicom.Sequence()
|
334 |
+
# fill in sequence
|
335 |
+
dssr_refStudy = pydicom.Dataset()
|
336 |
+
dssr_refStudy.ReferencedSOPClassUID = '1.2.840.10008.3.1.2.3.1' # Study Management Detached
|
337 |
+
dssr_refStudy.ReferencedSOPInstanceUID = refCT.StudyInfo.StudyInstanceUID
|
338 |
+
dssr_refStudy.RTReferencedSeriesSequence = pydicom.Sequence()
|
339 |
+
#initialize
|
340 |
+
dssr_refStudy_series = pydicom.Dataset()
|
341 |
+
dssr_refStudy_series.SeriesInstanceUID = refCT.SeriesInstanceUID
|
342 |
+
dssr_refStudy_series.ContourImageSequence = pydicom.Sequence()
|
343 |
+
# loop over slices of CT
|
344 |
+
for slc in range(len(refCT.SOPInstanceUIDs)):
|
345 |
+
dssr_refStudy_series_slc = pydicom.Dataset()
|
346 |
+
dssr_refStudy_series_slc.ReferencedSOPClassUID = refCT.SOPClassUID
|
347 |
+
dssr_refStudy_series_slc.ReferencedSOPInstanceUID = refCT.SOPInstanceUIDs[slc]
|
348 |
+
# append
|
349 |
+
dssr_refStudy_series.ContourImageSequence.append(dssr_refStudy_series_slc)
|
350 |
+
|
351 |
+
# append
|
352 |
+
dssr_refStudy.RTReferencedSeriesSequence.append(dssr_refStudy_series)
|
353 |
+
# append
|
354 |
+
dssr.RTReferencedStudySequence.append(dssr_refStudy)
|
355 |
+
#append
|
356 |
+
ds.ReferencedFrameOfReferenceSequence.append(dssr)
|
357 |
+
#
|
358 |
+
ds.StructureSetROISequence = pydicom.Sequence()
|
359 |
+
# loop over the ROIs to fill in the fields
|
360 |
+
for iroi in range(self.NumContours):
|
361 |
+
# initialize the Dataset
|
362 |
+
dssr = pydicom.Dataset()
|
363 |
+
dssr.ROINumber = iroi + 1 # because iroi starts at zero and ROINumber cannot be zero
|
364 |
+
dssr.ReferencedFrameOfReferenceUID = ds.FrameOfReferenceUID # coming from refCT
|
365 |
+
dssr.ROIName = self.Contours[iroi].ROIName
|
366 |
+
#dssr.ROIDescription # optional
|
367 |
+
dssr.ROIGenerationAlgorithm = 'AUTOMATIC' # can also be 'SEMIAUTOMATIC' OR 'MANUAL', info here https://dicom.innolitics.com/ciods/rt-structure-set/structure-set/30060020/30060036
|
368 |
+
#TODO enable a function to tell us which type of GenerationAlgorithm we have
|
369 |
+
ds.StructureSetROISequence.append(dssr)
|
370 |
+
|
371 |
+
# delete to remove space
|
372 |
+
del dssr
|
373 |
+
|
374 |
+
#TODO merge all loops into one to be faster, although like this the code is easier to follow I find
|
375 |
+
|
376 |
+
# ROI Contour
|
377 |
+
ds.ROIContourSequence = pydicom.Sequence()
|
378 |
+
# loop over the ROIs to fill in the fields
|
379 |
+
for iroi in range(self.NumContours):
|
380 |
+
# initialize the Dataset
|
381 |
+
dssr = pydicom.Dataset()
|
382 |
+
dssr.ROIDisplayColor = self.Contours[iroi].ROIDisplayColor
|
383 |
+
dssr.ReferencedROINumber = iroi + 1 # because iroi starts at zero and ReferencedROINumber cannot be zero
|
384 |
+
dssr.ContourSequence = pydicom.Sequence()
|
385 |
+
# mask to polygon
|
386 |
+
polygonMeshList = self.Contours[iroi].getROIContour()
|
387 |
+
# get z vector
|
388 |
+
z_coords = list(np.arange(self.Contours[iroi].Mask_Offset[2],self.Contours[iroi].Mask_Offset[2]+self.Contours[iroi].Mask_GridSize[2]*self.Contours[iroi].Mask_PixelSpacing[2], self.Contours[iroi].Mask_PixelSpacing[2]))
|
389 |
+
# loop over the polygonMeshList to fill in ContourSequence
|
390 |
+
for polygon in polygonMeshList:
|
391 |
+
|
392 |
+
# initialize the Dataset
|
393 |
+
dssr_slc = pydicom.Dataset()
|
394 |
+
dssr_slc.ContourGeometricType = 'CLOSED_PLANAR' # can also be 'POINT', 'OPEN_PLANAR', 'OPEN_NONPLANAR', info here https://dicom.innolitics.com/ciods/rt-structure-set/roi-contour/30060039/30060040/30060042
|
395 |
+
#TODO enable the proper selection of the ContourGeometricType
|
396 |
+
|
397 |
+
# fill in contour points and data
|
398 |
+
dssr_slc.NumberOfContourPoints = len(polygon[0::3])
|
399 |
+
#dssr_slc.ContourNumber # optional
|
400 |
+
# Smooth contour
|
401 |
+
smoothed_array_2D = Taubin_smoothing(np.transpose(np.array([polygon[0::3],polygon[1::3]])))
|
402 |
+
# fill in smoothed contour
|
403 |
+
polygon[0::3] = smoothed_array_2D[:,0]
|
404 |
+
polygon[1::3] = smoothed_array_2D[:,1]
|
405 |
+
dssr_slc.ContourData = polygon
|
406 |
+
|
407 |
+
#get slice
|
408 |
+
polygon_z = polygon[2]
|
409 |
+
slc = z_coords.index(polygon_z)
|
410 |
+
# fill in ContourImageSequence
|
411 |
+
dssr_slc.ContourImageSequence = pydicom.Sequence() # Sequence of images containing the contour
|
412 |
+
# in our case, we assume we only have one, the reference CT (refCT)
|
413 |
+
dssr_slc_ref = pydicom.Dataset()
|
414 |
+
dssr_slc_ref.ReferencedSOPClassUID = refCT.SOPClassUID
|
415 |
+
dssr_slc_ref.ReferencedSOPInstanceUID = refCT.SOPInstanceUIDs[slc]
|
416 |
+
dssr_slc.ContourImageSequence.append(dssr_slc_ref)
|
417 |
+
|
418 |
+
# append Dataset to Sequence
|
419 |
+
dssr.ContourSequence.append(dssr_slc)
|
420 |
+
|
421 |
+
# append Dataset
|
422 |
+
ds.ROIContourSequence.append(dssr)
|
423 |
+
|
424 |
+
# RT ROI Observations
|
425 |
+
ds.RTROIObservationsSequence = pydicom.Sequence()
|
426 |
+
# loop over the ROIs to fill in the fields
|
427 |
+
for iroi in range(self.NumContours):
|
428 |
+
# initialize the Dataset
|
429 |
+
dssr = pydicom.Dataset()
|
430 |
+
dssr.ObservationNumber = iroi + 1 # because iroi starts at zero and ReferencedROINumber cannot be zero
|
431 |
+
dssr.ReferencedROINumber = iroi + 1 ## because iroi starts at zero and ReferencedROINumber cannot be zero
|
432 |
+
dssr.ROIObservationLabel = self.Contours[iroi].ROIName #optional
|
433 |
+
dssr.RTROIInterpretedType = 'ORGAN' # we can have many types, see here https://dicom.innolitics.com/ciods/rt-structure-set/rt-roi-observations/30060080/300600a4
|
434 |
+
# TODO enable a better fill in of the RTROIInterpretedType
|
435 |
+
dssr.ROIInterpreter = '' # empty if unknown
|
436 |
+
# append Dataset
|
437 |
+
ds.RTROIObservationsSequence.append(dssr)
|
438 |
+
|
439 |
+
# Approval
|
440 |
+
ds.ApprovalStatus = 'UNAPPROVED'#'APPROVED'
|
441 |
+
# if ds.ApprovalStatus = 'APPROVED', then we need to fill in the reviewer information
|
442 |
+
#ds.ReviewDate = dt.strftime('%Y%m%d')
|
443 |
+
#ds.ReviewTime = dt.strftime('%H%M%S.%f')
|
444 |
+
#ds.ReviewerName = 'MIRO AI team'
|
445 |
+
|
446 |
+
# SOP common
|
447 |
+
ds.SpecificCharacterSet = 'ISO_IR 100' # conditionally required - see info here https://dicom.innolitics.com/ciods/rt-structure-set/sop-common/00080005
|
448 |
+
#ds.InstanceCreationDate # optional
|
449 |
+
#ds.InstanceCreationTime # optional
|
450 |
+
ds.SOPClassUID = '1.2.840.10008.5.1.4.1.1.481.3' #RTSTRUCT file
|
451 |
+
ds.SOPInstanceUID = SOPInstanceUID# Siri's --> pydicom.uid.generate_uid(uid_base)
|
452 |
+
#ds.InstanceNumber # optional
|
453 |
+
|
454 |
+
# save dicom file
|
455 |
+
print("Export dicom RTSTRUCT: " + outputFile)
|
456 |
+
ds.save_as(outputFile)
|
457 |
+
|
458 |
+
|
459 |
+
|
460 |
+
|
461 |
+
class ROIcontour:
|
462 |
+
|
463 |
+
def __init__(self):
|
464 |
+
self.SeriesInstanceUID = ""
|
465 |
+
self.ROIName = ""
|
466 |
+
self.ContourSequence = []
|
467 |
+
|
468 |
+
def getROIContour(self): # this is from new version of OpenTPS, I(ana) have adapted it to work with old version of self.Contours[i].Mask
|
469 |
+
|
470 |
+
try:
|
471 |
+
from skimage.measure import label, find_contours
|
472 |
+
from skimage.segmentation import find_boundaries
|
473 |
+
except:
|
474 |
+
print('Module skimage (scikit-image) not installed, ROIMask cannot be converted to ROIContour')
|
475 |
+
return 0
|
476 |
+
|
477 |
+
polygonMeshList = []
|
478 |
+
for zSlice in range(self.Mask.shape[2]):
|
479 |
+
|
480 |
+
labeledImg, numberOfLabel = label(self.Mask[:, :, zSlice], return_num=True)
|
481 |
+
|
482 |
+
for i in range(1, numberOfLabel + 1):
|
483 |
+
|
484 |
+
singleLabelImg = labeledImg == i
|
485 |
+
contours = find_contours(singleLabelImg.astype(np.uint8), level=0.6)
|
486 |
+
|
487 |
+
if len(contours) > 0:
|
488 |
+
|
489 |
+
if len(contours) == 2:
|
490 |
+
|
491 |
+
## use a different threshold in the case of an interior contour
|
492 |
+
contours2 = find_contours(singleLabelImg.astype(np.uint8), level=0.4)
|
493 |
+
|
494 |
+
interiorContour = contours2[1]
|
495 |
+
polygonMesh = []
|
496 |
+
for point in interiorContour:
|
497 |
+
|
498 |
+
#xCoord = np.round(point[1]) * self.Mask_PixelSpacing[1] + self.Mask_Offset[1] # original Damien in OpenTPS
|
499 |
+
#yCoord = np.round(point[0]) * self.Mask_PixelSpacing[0] + self.Mask_Offset[0] # original Damien in OpenTPS
|
500 |
+
xCoord = np.round(point[1]) * self.Mask_PixelSpacing[0] + self.Mask_Offset[0] #AB
|
501 |
+
yCoord = np.round(point[0]) * self.Mask_PixelSpacing[1] + self.Mask_Offset[1] #AB
|
502 |
+
zCoord = zSlice * self.Mask_PixelSpacing[2] + self.Mask_Offset[2]
|
503 |
+
|
504 |
+
#polygonMesh.append(yCoord) # original Damien in OpenTPS
|
505 |
+
#polygonMesh.append(xCoord) # original Damien in OpenTPS
|
506 |
+
polygonMesh.append(xCoord) # AB
|
507 |
+
polygonMesh.append(yCoord) # AB
|
508 |
+
polygonMesh.append(zCoord)
|
509 |
+
|
510 |
+
polygonMeshList.append(polygonMesh)
|
511 |
+
|
512 |
+
contour = contours[0]
|
513 |
+
|
514 |
+
polygonMesh = []
|
515 |
+
for point in contour:
|
516 |
+
|
517 |
+
#xCoord = np.round(point[1]) * self.Mask_PixelSpacing[1] + self.Mask_Offset[1] # original Damien in OpenTPS
|
518 |
+
#yCoord = np.round(point[0]) * self.Mask_PixelSpacing[0] + self.Mask_Offset[0] # original Damien in OpenTPS
|
519 |
+
xCoord = np.round(point[1]) * self.Mask_PixelSpacing[0] + self.Mask_Offset[0] #AB
|
520 |
+
yCoord = np.round(point[0]) * self.Mask_PixelSpacing[1] + self.Mask_Offset[1] #AB
|
521 |
+
zCoord = zSlice * self.Mask_PixelSpacing[2] + self.Mask_Offset[2]
|
522 |
+
|
523 |
+
polygonMesh.append(xCoord) # AB
|
524 |
+
polygonMesh.append(yCoord) # AB
|
525 |
+
#polygonMesh.append(yCoord) # original Damien in OpenTPS
|
526 |
+
#polygonMesh.append(xCoord) # original Damien in OpenTPS
|
527 |
+
polygonMesh.append(zCoord)
|
528 |
+
|
529 |
+
polygonMeshList.append(polygonMesh)
|
530 |
+
|
531 |
+
## I (ana) will comment this part since I will not use the class ROIContour for simplicity ###
|
532 |
+
#from opentps.core.data._roiContour import ROIContour ## this is done here to avoir circular imports issue
|
533 |
+
#contour = ROIContour(name=self.ROIName, displayColor=self.ROIDisplayColor)
|
534 |
+
#contour.polygonMesh = polygonMeshList
|
535 |
+
|
536 |
+
#return contour
|
537 |
+
|
538 |
+
# instead returning the polygonMeshList directly
|
539 |
+
return polygonMeshList
|
540 |
+
|
541 |
+
|
542 |
+
|
libraries/utils_nii_dicom.py
ADDED
@@ -0,0 +1,130 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#!/usr/bin/env python2
|
2 |
+
# -*- coding: utf-8 -*-
|
3 |
+
"""
|
4 |
+
Created on Sat Sep 5 20:34:46 2020
|
5 |
+
|
6 |
+
@author: ana
|
7 |
+
"""
|
8 |
+
|
9 |
+
|
10 |
+
# import general libraries
|
11 |
+
import os
|
12 |
+
import numpy as np
|
13 |
+
import pandas as pd
|
14 |
+
import scipy
|
15 |
+
import nibabel as nib
|
16 |
+
import pydicom
|
17 |
+
import glob
|
18 |
+
import warnings
|
19 |
+
from copy import deepcopy
|
20 |
+
from scipy.ndimage import find_objects
|
21 |
+
from scipy.ndimage.morphology import binary_fill_holes
|
22 |
+
from skimage import measure
|
23 |
+
from matplotlib.patches import Polygon
|
24 |
+
|
25 |
+
|
26 |
+
###############################################################################
|
27 |
+
################################### FUNCTIONS #############################
|
28 |
+
###############################################################################
|
29 |
+
|
30 |
+
def get_dict(dict_path):
|
31 |
+
"""
|
32 |
+
get dictionary in NAS/public_info
|
33 |
+
:param : dict_path(string) location of the dictionary
|
34 |
+
:return: dictionary in pandas format
|
35 |
+
"""
|
36 |
+
if dict_path == 'default':
|
37 |
+
dictionary = pd.read_excel(r'/home/ana/NAS_database/public_info/RT_dictionary.xlsx', engine='openpyxl')
|
38 |
+
else:
|
39 |
+
dictionary = pd.read_excel(dict_path, engine='openpyxl')
|
40 |
+
|
41 |
+
return dictionary
|
42 |
+
|
43 |
+
|
44 |
+
def set_header_info(nii_file, voxelsize, image_position_patient, contours_exist = None):
|
45 |
+
nii_file.header['pixdim'][1] = voxelsize[0]
|
46 |
+
nii_file.header['pixdim'][2] = voxelsize[1]
|
47 |
+
nii_file.header['pixdim'][3] = voxelsize[2]
|
48 |
+
|
49 |
+
#affine - voxelsize
|
50 |
+
nii_file.affine[0][0] = voxelsize[0]
|
51 |
+
nii_file.affine[1][1] = voxelsize[1]
|
52 |
+
nii_file.affine[2][2] = voxelsize[2]
|
53 |
+
#affine - imagecorner
|
54 |
+
nii_file.affine[0][3] = image_position_patient[0]
|
55 |
+
nii_file.affine[1][3] = image_position_patient[1]
|
56 |
+
nii_file.affine[2][3] = image_position_patient[2]
|
57 |
+
if contours_exist is not None:
|
58 |
+
nii_file.header.extensions.append(nib.nifti1.Nifti1Extension(0, bytearray(contours_exist)))
|
59 |
+
return nii_file
|
60 |
+
|
61 |
+
|
62 |
+
def get_struct_and_contoursexist(roi,arrayshape):
|
63 |
+
struct = np.zeros((arrayshape[0],arrayshape[1],arrayshape[2]))
|
64 |
+
roi_names = list(roi.keys())
|
65 |
+
contoursexist = []
|
66 |
+
|
67 |
+
for i in range(len(roi_names)):
|
68 |
+
if(not(roi[roi_names[i]] is None)):
|
69 |
+
contoursexist.append(1)
|
70 |
+
struct += (2**i)*roi[roi_names[i]]
|
71 |
+
else:
|
72 |
+
contoursexist.append(0)
|
73 |
+
return struct, contoursexist
|
74 |
+
|
75 |
+
|
76 |
+
def save_images(dst_dir, voxelsize, image_position_patient, image, image_type, roi=None, dose=None, bdoses=None, prior_knowledge = None, spotmap = None):
|
77 |
+
|
78 |
+
# encode in nii and save at dst_dir
|
79 |
+
# IMPORTANT WE NEED TO CONFIRM THE SIGNS OF THE ENTRIES IN THE AFFINE,
|
80 |
+
# ALTHOUGH MAYBE AT THE END THE IMPORTANCE IS HOW WE WILL USE THIS DATA ....
|
81 |
+
# also instead of changing field by field, the pixdim and affine can be encoded
|
82 |
+
# using the set_sform method --> info here: https://nipy.org/nibabel/nifti_images.html
|
83 |
+
|
84 |
+
# IMAGE (CT, MR ...)
|
85 |
+
image_shape = image.shape
|
86 |
+
image_nii = nib.Nifti1Image(image, affine=np.eye(4)) # for Nifti1 header, change for a Nifti2 type of header
|
87 |
+
# Update header fields
|
88 |
+
image_nii = set_header_info(image_nii, voxelsize, image_position_patient)
|
89 |
+
# Save nii
|
90 |
+
nib.save(image_nii, os.path.join(dst_dir,image_type.lower()+'.nii.gz'))
|
91 |
+
|
92 |
+
# DOSE
|
93 |
+
if dose is not None:
|
94 |
+
dose_nii = nib.Nifti1Image(dose, affine=np.eye(4)) # for Nifti1 header, change for a Nifti2 type of header
|
95 |
+
# Update header fields
|
96 |
+
dose_nii = set_header_info(dose_nii, voxelsize, image_position_patient)
|
97 |
+
# Save nii
|
98 |
+
nib.save(dose_nii, os.path.join(dst_dir,'dose.nii.gz'))
|
99 |
+
|
100 |
+
# BDOSES
|
101 |
+
if bdoses is not None:
|
102 |
+
for b in range(len(bdoses)):
|
103 |
+
bdose_nii = nib.Nifti1Image(bdoses[b].Image, affine=np.eye(4)) # for Nifti1 header, change for a Nifti2 type of header
|
104 |
+
# Update header fields
|
105 |
+
bdose_nii = set_header_info(bdose_nii, voxelsize, image_position_patient)
|
106 |
+
# Save nii
|
107 |
+
nib.save(bdose_nii, os.path.join(dst_dir,'dose_b{}.nii.gz'.format(b+1)))
|
108 |
+
|
109 |
+
# PRIOR KNOWLEDGE
|
110 |
+
if prior_knowledge is not None:
|
111 |
+
for charact, pk in prior_knowledge.items():
|
112 |
+
prior_knowledge_nii = nib.Nifti1Image(pk, affine=np.eye(4)) # for Nifti1 header, change for a Nifti2 type of header
|
113 |
+
# Update header fields
|
114 |
+
prior_knowledge_nii = set_header_info(prior_knowledge_nii, voxelsize, image_position_patient)
|
115 |
+
# Save nii
|
116 |
+
nib.save(prior_knowledge_nii, os.path.join(dst_dir,'prior_knowledge_{}.nii.gz'.format(charact.lower())))
|
117 |
+
|
118 |
+
# RTSTRUCT
|
119 |
+
if roi is not None:
|
120 |
+
struct_compressed, contours_exist = get_struct_and_contoursexist(roi, image_shape)
|
121 |
+
|
122 |
+
struct_nii_compressed = nib.Nifti1Image(struct_compressed, affine=np.eye(4)) # for Nifti1 header, change for a Nifti2 type of header
|
123 |
+
# struct_nii_compressed.set_data_dtype('smallest')
|
124 |
+
# Update header fields
|
125 |
+
struct_nii_compressed = set_header_info(struct_nii_compressed, voxelsize, image_position_patient, contours_exist=contours_exist)
|
126 |
+
# Save nii
|
127 |
+
nib.save(struct_nii_compressed, os.path.join(dst_dir,'struct.nii.gz'))
|
128 |
+
|
129 |
+
|
130 |
+
|
libraries/utils_preprocess.py
ADDED
@@ -0,0 +1,148 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#!/usr/bin/env python2
|
2 |
+
# -*- coding: utf-8 -*-
|
3 |
+
"""
|
4 |
+
Created on Sat Sep 5 20:34:46 2020
|
5 |
+
|
6 |
+
@author: ana
|
7 |
+
"""
|
8 |
+
|
9 |
+
|
10 |
+
# import general libraries
|
11 |
+
import os
|
12 |
+
import numpy as np
|
13 |
+
import pandas as pd
|
14 |
+
import scipy
|
15 |
+
import nibabel as nib
|
16 |
+
import pydicom
|
17 |
+
import glob
|
18 |
+
import warnings
|
19 |
+
from copy import deepcopy
|
20 |
+
from scipy.ndimage import find_objects
|
21 |
+
from scipy.ndimage.morphology import binary_fill_holes
|
22 |
+
from skimage import measure
|
23 |
+
from matplotlib.patches import Polygon
|
24 |
+
from libraries.utils_nii_dicom import set_header_info
|
25 |
+
|
26 |
+
|
27 |
+
###############################################################################
|
28 |
+
################################### FUNCTIONS #############################
|
29 |
+
###############################################################################
|
30 |
+
|
31 |
+
def delete_all(obj):
|
32 |
+
for i in vars(obj):
|
33 |
+
del obj.i
|
34 |
+
|
35 |
+
def overwrite_ct_threshold(ct_image, body = None, artefact = None, contrast = None):
|
36 |
+
# Change the HU out of the body to air: -1000
|
37 |
+
if body is not None:
|
38 |
+
# Change the HU outside the body to -1000
|
39 |
+
ct_image[body==0]=-1000
|
40 |
+
if artefact is not None:
|
41 |
+
# Change the HU to muscle: 14
|
42 |
+
ct_image[artefact==1]=14
|
43 |
+
if contrast is not None:
|
44 |
+
# Change the HU to water: 0 Houndsfield Unit: CT unit
|
45 |
+
ct_image[contrast==1]=0
|
46 |
+
# Threshold above 1560HU
|
47 |
+
ct_image[ct_image > 1560] = 1560
|
48 |
+
return ct_image
|
49 |
+
|
50 |
+
def remove_dose_outside_mask(dose, mask = None):
|
51 |
+
# Put dose = 0 outside the mask (typically the body)
|
52 |
+
if mask is not None:
|
53 |
+
dose[mask==0]=0
|
54 |
+
return dose
|
55 |
+
|
56 |
+
def get_and_save_tv(roi, nib_header, tv_names, dst_dir):
|
57 |
+
tv = np.zeros(nib_header['dim'][1:4])
|
58 |
+
roi_names = list(roi.keys())
|
59 |
+
if tv_names is not None:
|
60 |
+
for c in tv_names:
|
61 |
+
if c in roi_names:
|
62 |
+
tv[roi[c]>0]=int(c.split('_')[1])/100
|
63 |
+
|
64 |
+
# create nii, update header and save
|
65 |
+
save_nii_image(tv, nib_header,dst_dir, 'struct_tv')
|
66 |
+
|
67 |
+
return tv
|
68 |
+
|
69 |
+
def get_and_save_oars(roi, nib_header, oar_names, dst_dir):
|
70 |
+
oars = np.zeros(nib_header['dim'][1:4])
|
71 |
+
|
72 |
+
roi_names = list(roi.keys())
|
73 |
+
contoursexist = []
|
74 |
+
|
75 |
+
if oar_names is not None:
|
76 |
+
for i in range(len(oar_names)):
|
77 |
+
if oar_names[i] in roi_names:
|
78 |
+
if roi[oar_names[i]] is not None:
|
79 |
+
contoursexist.append(1)
|
80 |
+
oars += (2**i)*roi[oar_names[i]]
|
81 |
+
else:
|
82 |
+
contoursexist.append(0)
|
83 |
+
|
84 |
+
# create nii, update header and save
|
85 |
+
save_nii_image(oars, nib_header,dst_dir, 'struct_oar', contours_exist = contoursexist)
|
86 |
+
|
87 |
+
|
88 |
+
def get_and_save_sample_probability(tv,dst_dir):
|
89 |
+
# get sample probability for patch-based training
|
90 |
+
bufftv=np.zeros_like(tv)
|
91 |
+
bufftv[tv!=0]=1
|
92 |
+
m=bufftv.sum(axis=0).sum(axis=0).sum()/1000
|
93 |
+
sample_probability_slc=(bufftv.sum(axis=0).sum(axis=0)+m)/(bufftv.sum(axis=0).sum(axis=0)+m).sum()
|
94 |
+
sample_probability_row=(bufftv.sum(axis=1).sum(axis=1)+m)/(bufftv.sum(axis=1).sum(axis=1)+m).sum()
|
95 |
+
sample_probability_col=(bufftv.sum(axis=0).sum(axis=-1)+m)/(bufftv.sum(axis=0).sum(axis=-1)+m).sum()
|
96 |
+
|
97 |
+
np.savez_compressed(os.path.join(dst_dir,'sample_probability_row.npz'),sample_probability_row)
|
98 |
+
np.savez_compressed(os.path.join(dst_dir,'sample_probability_col.npz'),sample_probability_col)
|
99 |
+
np.savez_compressed(os.path.join(dst_dir,'sample_probability_slc.npz'),sample_probability_slc)
|
100 |
+
|
101 |
+
|
102 |
+
def decompress_struct(struct_nib,struct_list):
|
103 |
+
|
104 |
+
struct_data = struct_nib.get_fdata()
|
105 |
+
roi = dict.fromkeys(struct_list, None)
|
106 |
+
# get contourexists from header
|
107 |
+
contoursexist = list(struct_nib.header.extensions[0].get_content())
|
108 |
+
for i in range(len(contoursexist)):
|
109 |
+
if contoursexist[i] == 1:
|
110 |
+
roi[struct_list[i]] = np.bitwise_and(struct_data.astype(int), 2 ** i).astype(bool)
|
111 |
+
|
112 |
+
return roi
|
113 |
+
|
114 |
+
def binary_to_integers(struct,select_list):
|
115 |
+
|
116 |
+
roi_names = list(struct.keys())
|
117 |
+
# get shape
|
118 |
+
for k in roi_names:
|
119 |
+
if struct[k] is not None:
|
120 |
+
struct_shape = struct[k].shape
|
121 |
+
# initialize roi
|
122 |
+
roi = np.zeros(struct_shape)
|
123 |
+
|
124 |
+
# initialize label
|
125 |
+
label = 0
|
126 |
+
for i in range(len(select_list)):
|
127 |
+
label = label + 1
|
128 |
+
if select_list[i] in roi_names:
|
129 |
+
if struct[select_list[i]] is not None:
|
130 |
+
# populate roi matrix with selected rois in select_list
|
131 |
+
roi[struct[select_list[i]]] = label
|
132 |
+
|
133 |
+
return roi
|
134 |
+
|
135 |
+
def save_nii_image(nib_img, nib_header,dst_dir, image_name, contours_exist = None):
|
136 |
+
|
137 |
+
image_nii = nib.Nifti1Image(nib_img, affine=np.eye(4)) # for Nifti1 header, change for a Nifti2 type of header
|
138 |
+
# Update header fields
|
139 |
+
if contours_exist is not None:
|
140 |
+
image_nii = set_header_info(image_nii, nib_header['pixdim'][1:4], [nib_header['qoffset_x'],nib_header['qoffset_y'],nib_header['qoffset_z']], contours_exist = contours_exist)
|
141 |
+
else:
|
142 |
+
image_nii = set_header_info(image_nii, nib_header['pixdim'][1:4], [nib_header['qoffset_x'],nib_header['qoffset_y'],nib_header['qoffset_z']])
|
143 |
+
# Save nii
|
144 |
+
nib.save(image_nii, os.path.join(dst_dir,image_name +'.nii.gz'))
|
145 |
+
|
146 |
+
|
147 |
+
|
148 |
+
|