Margerie commited on
Commit
b51cd04
1 Parent(s): 24271ba

Upload Process library

Browse files
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
+