import pydicom import datetime import numpy as np import scipy import nibabel as nib class PatientInfo: def __init__(self): self.PatientID = '' self.PatientName = '' self.PatientBirthDate = '' self.PatientSex = '' class StudyInfo: def __init__(self): self.StudyInstanceUID = '' self.StudyID = '' self.StudyDate = '' self.StudyTime = '' class RTdose: def __init__(self): self.SeriesInstanceUID = "" self.SOPInstanceUID = "" self.PatientInfo = PatientInfo() self.StudyInfo = StudyInfo() self.CT_SeriesInstanceUID = "" self.Plan_SOPInstanceUID = "" self.FrameOfReferenceUID = "" self.ImgName = "" self.beam_number = "" # Beam number (str) or PLAN if sum of all self.DcmFile = "" self.isLoaded = 0 def print_dose_info(self, prefix=""): print(prefix + "Dose: " + self.SOPInstanceUID) print(prefix + " " + self.DcmFile) def import_Dicom_dose(self, CT): if(self.isLoaded == 1): print("Warning: Dose image " + self.SOPInstanceUID + " is already loaded") return dcm = pydicom.dcmread(self.DcmFile) self.CT_SeriesInstanceUID = CT.SeriesInstanceUID # self.Plan_SOPInstanceUID = dcm.ReferencedRTPlanSequence[0].ReferencedSOPInstanceUID if(dcm.BitsStored == 16 and dcm.PixelRepresentation == 0): dt = np.dtype('uint16') elif(dcm.BitsStored == 16 and dcm.PixelRepresentation == 1): dt = np.dtype('int16') elif(dcm.BitsStored == 32 and dcm.PixelRepresentation == 0): dt = np.dtype('uint32') elif(dcm.BitsStored == 32 and dcm.PixelRepresentation == 1): dt = np.dtype('int32') else: print("Error: Unknown data type for " + self.DcmFile) return if(dcm.HighBit == dcm.BitsStored-1): dt = dt.newbyteorder('L') else: dt = dt.newbyteorder('B') dose_image = np.frombuffer(dcm.PixelData, dtype=dt) dose_image = dose_image.reshape((dcm.Columns, dcm.Rows, dcm.NumberOfFrames), order='F').transpose(1,0,2) dose_image = dose_image * dcm.DoseGridScaling self.Image = dose_image self.FrameOfReferenceUID = dcm.FrameOfReferenceUID self.ImagePositionPatient = dcm.ImagePositionPatient if dcm.SliceThickness is not None: self.PixelSpacing = [dcm.PixelSpacing[0], dcm.PixelSpacing[1], dcm.SliceThickness] else: self.PixelSpacing = [dcm.PixelSpacing[0], dcm.PixelSpacing[1], dcm.GridFrameOffsetVector[1]-dcm.GridFrameOffsetVector[0]] self.GridSize = [dcm.Columns, dcm.Rows, dcm.NumberOfFrames] self.NumVoxels = self.GridSize[0] * self.GridSize[1] * self.GridSize[2] if hasattr(dcm, 'GridFrameOffsetVector'): if(dcm.GridFrameOffsetVector[1] - dcm.GridFrameOffsetVector[0] < 0): self.Image = np.flip(self.Image, 2) self.ImagePositionPatient[2] = self.ImagePositionPatient[2] - self.GridSize[2]*self.PixelSpacing[2] self.resample_to_CT_grid(CT) self.isLoaded = 1 def euclidean_dist(self, v1, v2): return sum((p-q)**2 for p, q in zip(v1, v2)) ** .5 def resample_to_CT_grid(self, CT): 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): return else: # anti-aliasing filter sigma = [0, 0, 0] if(CT.PixelSpacing[0] > self.PixelSpacing[0]): sigma[0] = 0.4 * (CT.PixelSpacing[0]/self.PixelSpacing[0]) if(CT.PixelSpacing[1] > self.PixelSpacing[1]): sigma[1] = 0.4 * (CT.PixelSpacing[1]/self.PixelSpacing[1]) if(CT.PixelSpacing[2] > self.PixelSpacing[2]): sigma[2] = 0.4 * (CT.PixelSpacing[2]/self.PixelSpacing[2]) if(sigma != [0, 0, 0]): print("Image is filtered before downsampling") self.Image = scipy.ndimage.gaussian_filter(self.Image, sigma) print('Resample dose image to CT grid.') x = self.ImagePositionPatient[1] + np.arange(self.GridSize[1]) * self.PixelSpacing[1] y = self.ImagePositionPatient[0] + np.arange(self.GridSize[0]) * self.PixelSpacing[0] z = self.ImagePositionPatient[2] + np.arange(self.GridSize[2]) * self.PixelSpacing[2] xi = np.array(np.meshgrid(CT.VoxelY, CT.VoxelX, CT.VoxelZ)) xi = np.rollaxis(xi, 0, 4) xi = xi.reshape((xi.size // 3, 3)) self.Image = scipy.interpolate.interpn((x,y,z), self.Image, xi, method='linear', fill_value=0, bounds_error=False) self.Image = self.Image.reshape((CT.GridSize[0], CT.GridSize[1], CT.GridSize[2])).transpose(1,0,2) self.ImagePositionPatient = CT.ImagePositionPatient self.PixelSpacing = CT.PixelSpacing self.GridSize = CT.GridSize self.NumVoxels = CT.NumVoxels def load_from_nii(self, dose_nii): # load the nii image img = nib.load(dose_nii) self.Image = img.get_fdata() ### SHOULD I TRANSPOSE? self.GridSize = self.Image.shape self.PixelSpacing = [img.header['pixdim'][1], img.header['pixdim'][2], img.header['pixdim'][3]] self.ImagePositionPatient = [ img.affine[0][3], img.affine[1][3], img.affine[2][3]] def export_Dicom(self, refCT, OutputFile): # meta data SOPInstanceUID = pydicom.uid.generate_uid() meta = pydicom.dataset.FileMetaDataset() meta.MediaStorageSOPClassUID = '1.2.840.10008.5.1.4.1.1.481.2' # UID class for RTDOSE meta.MediaStorageSOPInstanceUID = SOPInstanceUID #meta.ImplementationClassUID = '1.2.826.0.1.3680043.1.2.100.5.7.0.47' # from RayStation #meta.ImplementationClassUID = '1.2.826.0.1.3680043.5.5.100.5.7.0.03' # modified OpenTPS meta.ImplementationClassUID = '1.2.826.0.1.3680043.1.2.100.6.40.0.76'# from Halcyon? st. luc breast patients #meta.TransferSyntaxUID = '1.2.840.10008.1.2' meta.FileMetaInformationGroupLength = 200 #meta.FileMetaInformationVersion = #meta.ImplementationVersionName = 'DicomObjects.NET' # dcm_file.ImplementationVersionName = # dcm_file.SoftwareVersion = # dicom dataset dcm_file = pydicom.dataset.FileDataset(OutputFile, {}, file_meta=meta, preamble=b"\0" * 128) # CONFIRM WHAT IS OUTPUTFILE AND WHAT THIS LINE IS DOING # transfer syntax dcm_file.file_meta.TransferSyntaxUID = pydicom.uid.ExplicitVRLittleEndian print(dcm_file.file_meta.TransferSyntaxUID) dcm_file.is_little_endian = True dcm_file.is_implicit_VR = False # Patient dcm_file.PatientName = refCT.PatientInfo.PatientName #self.PatientInfo.PatientName dcm_file.PatientID = refCT.PatientInfo.PatientID #self.PatientInfo.PatientID dcm_file.PatientBirthDate = refCT.PatientInfo.PatientBirthDate #self.PatientInfo.PatientBirthDate dcm_file.PatientSex = refCT.PatientInfo.PatientSex #self.PatientInfo.PatientSex # General Study dt = datetime.datetime.now() dcm_file.StudyDate = dt.strftime('%Y%m%d') dcm_file.StudyTime = dt.strftime('%H%M%S.%f') dcm_file.AccessionNumber = '1' # A RIS/PACS (Radiology Information System/picture archiving and communication system) generated number that identifies the order for the Study. dcm_file.ReferringPhysicianName = 'NA' dcm_file.StudyInstanceUID = refCT.StudyInfo.StudyInstanceUID # get from reference CT to indicate that they belong to the same study#self.StudyInfo.StudyInstanceUID dcm_file.StudyID = refCT.StudyInfo.StudyID # get from reference CT to indicate that they belong to the same study # RT Series dcm_file.Modality = 'RTDOSE' dcm_file.SeriesDescription = 'AI-predicted' + dt.strftime('%Y%m%d') + dt.strftime('%H%M%S.%f')#self.ImgName dcm_file.OperatorsName = 'MIRO' dcm_file.SeriesInstanceUID = pydicom.uid.generate_uid() # if we have a uid_base --> pydicom.uid.generate_uid(uid_base) dcm_file.SeriesNumber = 1 # Frame of Reference dcm_file.FrameOfReferenceUID = refCT.FrameOfReferenceUID # pydicom.uid.generate_uid() dcm_file.PositionReferenceIndicator = '' #empty if unknown https://dicom.innolitics.com/ciods/rt-dose/frame-of-reference/00201040 # General Equipment dcm_file.Manufacturer = 'echarp' #dcm_file.ManufacturerModelName = 'echarp' #dcm_file.PixelPaddingValue = # conditionally required! https://dicom.innolitics.com/ciods/rt-dose/general-equipment/00280120 # General Image dcm_file.ContentDate = dt.strftime('%Y%m%d') dcm_file.ContentTime = dt.strftime('%H%M%S.%f') dcm_file.InstanceNumber = 1 dcm_file.PatientOrientation = '' # Image Plane dcm_file.SliceThickness = self.PixelSpacing[2] dcm_file.ImagePositionPatient = self.ImagePositionPatient 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 dcm_file.PixelSpacing = self.PixelSpacing[0:2] # Image pixel dcm_file.SamplesPerPixel = 1 dcm_file.PhotometricInterpretation = 'MONOCHROME2' dcm_file.Rows = self.GridSize[1] dcm_file.Columns = self.GridSize[0] dcm_file.BitsAllocated = 16 dcm_file.BitsStored = 16 dcm_file.HighBit = 15 dcm_file.BitDepth = 16 dcm_file.PixelRepresentation = 0 # 0=unsigned, 1=signed #dcm_file.ColorType = 'grayscale' # multi-frame dcm_file.NumberOfFrames = self.GridSize[2] dcm_file.FrameIncrementPointer = pydicom.tag.Tag((0x3004, 0x000c)) # RT Dose dcm_file.DoseUnits = 'GY' dcm_file.DoseType = 'PHYSICAL' # or 'EFFECTIVE' for RBE dose (but RayStation exports physical dose even if 1.1 factor is already taken into account) dcm_file.DoseSummationType = 'PLAN' dcm_file.GridFrameOffsetVector = list(np.arange(0, self.GridSize[2]*self.PixelSpacing[2], self.PixelSpacing[2])) dcm_file.DoseGridScaling = self.Image.max()/(2**dcm_file.BitDepth - 1) # pixel data dcm_file.PixelData = (self.Image/dcm_file.DoseGridScaling).astype(np.uint16).transpose(2,0,1).tostring() # ALTERNATIVE: self.Image.tobytes() #dcm_file.TissueHeterogeneityCorrection = 'IMAGE,ROI_OVERRIDE' # ReferencedPlan = pydicom.dataset.Dataset() # ReferencedPlan.ReferencedSOPClassUID = "1.2.840.10008.5.1.4.1.1.481.8" # ion plan # if(plan_uid == []): ReferencedPlan.ReferencedSOPInstanceUID = self.Plan_SOPInstanceUID # else: ReferencedPlan.ReferencedSOPInstanceUID = plan_uid # dcm_file.ReferencedRTPlanSequence = pydicom.sequence.Sequence([ReferencedPlan]) # SOP common dcm_file.SpecificCharacterSet = 'ISO_IR 100' dcm_file.InstanceCreationDate = dt.strftime('%Y%m%d') dcm_file.InstanceCreationTime = dt.strftime('%H%M%S.%f') dcm_file.SOPClassUID = meta.MediaStorageSOPClassUID dcm_file.SOPInstanceUID = SOPInstanceUID # save dicom file print("Export dicom RTDOSE: " + OutputFile) dcm_file.save_as(OutputFile)