def create_dicom_base(self): if _dicom_loaded is False: raise ModuleNotLoadedError("Dicom") if self.header_set is False: raise InputError("Header not loaded") meta = Dataset() meta.MediaStorageSOPClassUID = '1.2.840.10008.5.1.4.1.1.2' # CT Image Storage meta.MediaStorageSOPInstanceUID = "1.2.3" meta.ImplementationClassUID = "1.2.3.4" meta.TransferSyntaxUID = UID.ImplicitVRLittleEndian # Implicit VR Little Endian - Default Transfer Syntax ds = FileDataset("file", {}, file_meta=meta, preamble=b"\0" * 128) ds.PatientsName = self.patient_name ds.PatientID = "123456" ds.PatientsSex = '0' ds.PatientsBirthDate = '19010101' ds.SpecificCharacterSet = 'ISO_IR 100' ds.AccessionNumber = '' ds.is_little_endian = True ds.is_implicit_VR = True ds.SOPClassUID = '1.2.3' # !!!!!!!! ds.SOPInstanceUID = '1.2.3' # !!!!!!!!!! ds.StudyInstanceUID = '1.2.3' # !!!!!!!!!! ds.FrameofReferenceUID = '1.2.3' # !!!!!!!!! ds.StudyDate = '19010101' # !!!!!!! ds.StudyTime = '000000' # !!!!!!!!!! ds.PhotometricInterpretation = 'MONOCHROME2' ds.SamplesPerPixel = 1 ds.ImageOrientationPatient = ['1', '0', '0', '0', '1', '0'] ds.Rows = self.dimx ds.Columns = self.dimy ds.SliceThickness = str(self.slice_distance) ds.PixelSpacing = [self.pixel_size, self.pixel_size] return ds
def set_dose(self, dose): try: dose = float(dose) if dose < 0: raise Exception() self.dose = dose except Exception: raise InputError("Dose should be a number larger or equal to 0")
def set_zsteps(self, zsteps): try: zsteps = float(zsteps) if zsteps < 0: raise Exception() self.zsteps = zsteps except Exception: raise InputError("ZSteps should be larger 0")
def set_doseextension(self, doseextension): try: doseextension = float(doseextension) if doseextension < 0: raise Exception() self.doseextension = doseextension except Exception: raise InputError("Doseextension should be larger 0")
def set_contourextension(self, contourextension): try: contourextension = float(contourextension) if contourextension < 0: raise Exception() self.contourextension = contourextension except Exception: raise InputError("Contourextension should be larger 0")
def _read_trip_data_file( self, datafile_path, header_path, multiply_by_2=False): # TODO: could be made private? #126 """Read TRiP98 formatted data. If header file was not previously loaded, it will be attempted first. Due to an issue in VIRTUOS, sometimes DosCube data have been reduced with a factor of 2. Setting multiply_by_2 to True, will restore the true values, in this case. :param datafile_path: Path to TRiP formatted data. :param multiply_by_2: The data read will automatically be multiplied with a factor of 2. """ # fill header data if self.header is empty if not self.header_set: self._read_trip_header_file(header_path) # raise exception if reading header failed if not self.header_set: raise InputError("Header file not loaded") # preparation data_dtype = np.dtype(self.format_str) data_count = self.dimx * self.dimy * self.dimz # load data from data file (gzipped or not) logger.info("Opening file: " + datafile_path) if datafile_path.endswith('.gz'): import gzip with gzip.open(datafile_path, "rb") as f: s = f.read(data_dtype.itemsize * data_count) cube = np.frombuffer(s, dtype=data_dtype, count=data_count) else: cube = np.fromfile(datafile_path, dtype=data_dtype) if self.byte_order == "aix": logger.info("AIX big-endian data.") # byteswapping is not needed anymore, handled by "<" ">" in dtype # sanity check logger.info("Cube data points : {:d}".format(len(cube))) if len(cube) != self.dimx * self.dimy * self.dimz: logger.error("Header size and cube size dont match.") logger.error("Cube data points : {:d}".format(len(cube))) logger.error("Header says : {:d} = {:d} * {:d} * {:d}".format( self.dimx * self.dimy * self.dimz, self.dimx, self.dimy, self.dimz)) raise IOError("Header data and dose cube size are not consistent.") cube = np.reshape(cube, (self.dimz, self.dimy, self.dimx)) if multiply_by_2: logger.warning( "Cube was previously rescaled to 50%. Now multiplying with 2.") cube *= 2 self.cube = cube
def set_hu_value(self, value): if len(value) is 0: self.hu_value = None return try: value = float(value) self.hu_value = value except Exception: raise InputError("HU Value should be a number")
def set_max_dose_fraction(self, max_dose_fraction): try: max_dose_fraction = float(max_dose_fraction) if max_dose_fraction < 0: raise Exception() self.max_dose_fraction = max_dose_fraction except Exception: raise InputError("Max dose fraction should be " "a number between 0 and 1")
def create_dicom(self): """ Creates a Dicom RT-Dose object from self. This function can be used to convert a TRiP98 Dose file to Dicom format. :returns: a Dicom RT-Dose object. """ if not _dicom_loaded: raise ModuleNotLoadedError("Dicom") if not self.header_set: raise InputError("Header not loaded") ds = self.create_dicom_base() ds.Modality = 'RTDOSE' ds.SamplesperPixel = 1 ds.BitsAllocated = self.num_bytes * 8 ds.BitsStored = self.num_bytes * 8 ds.AccessionNumber = '' ds.SeriesDescription = 'RT Dose' ds.DoseUnits = 'GY' ds.DoseType = 'PHYSICAL' ds.DoseGridScaling = self.target_dose / 10**5 ds.DoseSummationType = 'PLAN' ds.SliceThickness = '' ds.InstanceCreationDate = '19010101' ds.InstanceCreationTime = '000000' ds.NumberOfFrames = len(self.cube) ds.PixelRepresentation = 0 ds.StudyID = '1' ds.SeriesNumber = 14 ds.GridFrameOffsetVector = [ x * self.slice_distance for x in range(self.dimz) ] ds.InstanceNumber = '' ds.NumberofFrames = len(self.cube) ds.PositionReferenceIndicator = "RF" ds.TissueHeterogeneityCorrection = ['IMAGE', 'ROI_OVERRIDE'] ds.ImagePositionPatient = [ "%.3f" % (self.xoffset * self.pixel_size), "%.3f" % (self.yoffset * self.pixel_size), "%.3f" % (self.slice_pos[0]) ] ds.SOPClassUID = '1.2.840.10008.5.1.4.1.1.481.2' ds.SOPInstanceUID = '1.2.246.352.71.7.320687012.47206.20090603085223' ds.SeriesInstanceUID = '1.2.246.352.71.2.320687012.28240.20090603082420' # Bind to rtplan rt_set = Dataset() rt_set.RefdSOPClassUID = '1.2.840.10008.5.1.4.1.1.481.5' rt_set.RefdSOPInstanceUID = '1.2.3' ds.ReferencedRTPlans = Sequence([rt_set]) pixel_array = np.zeros((len(self.cube), ds.Rows, ds.Columns), dtype=self.pydata_type) pixel_array[:][:][:] = self.cube[:][:][:] ds.PixelData = pixel_array.tostring() return ds
def set_fwhm(self, fwhm): try: fwhm = float(fwhm) if fwhm <= 0: raise Exception() self.fwhm = fwhm except Exception: raise InputError("Fwhm shoud be a number and larger than 0")
def set_rasterstep(self, a, b): try: a = float(a) b = float(b) if a < 0 or b < 0: raise Exception() self.rasterstep = [a, b] except Exception: raise InputError("Rastersteps should be " "larger than 0 and numbers")
def get_voi_by_name(self, name): """ Returns a Voi object by its name. :param str name: Name of voi to be returned. :returns: the Voi which has exactly this name, else raise an Error. """ for voi in self.vois: if voi.name.lower() == name.lower(): return voi raise InputError("Voi doesn't exist")
def set_couch(self, angle): if type(angle) is str and not len(angle): angle = 0 try: angle = (float(angle) + 360) % 360 if angle < 0 or angle > 360: raise Exception() self.couch = angle except Exception: raise InputError("Couch angle shoud be " "a number between 0 and 360")
def set_gantry(self, angle): if type(angle) is str and not len(angle): angle = 0 try: angle = (float(angle) + 360) % 360 if angle < 0 or angle > 360: raise Exception() self.gantry = angle except Exception: raise InputError("Gantry angle shoud be a " "number between 0 and 360")
def read_dicom(self, dcm): """ Imports the dose distribution from DICOM object. :param DICOM dcm: a DICOM object """ if "rtdose" not in dcm: raise InputError("Data doesn't contain dose information") if self.header_set is False: self._set_header_from_dicom(dcm) self.cube = np.zeros((self.dimz, self.dimy, self.dimx)) for i, item in enumerate(dcm["rtdose"].pixel_array): self.cube[i][:][:] = item
def set_target(self, target): if len(target) is 0: self.target = [] return target = target.split(",") if len(target) is 3: try: self.target = [float(target[0]), float(target[1]), float(target[2])] return except Exception: # TODO fix that ! pass raise InputError("Target should be empty " "or in the format x,y,z")
def read_dicom(self, data, structure_ids=None): """ Reads structures from a Dicom RTSS Object. :param Dicom data: A Dicom RTSS object. :param structure_ids: (TODO: undocumented) """ if "rtss" not in data: raise InputError("Input is not a valid rtss structure") dcm = data["rtss"] self.version = "2.0" for i, item in enumerate(dcm.ROIContours): if structure_ids is None or item.RefdROINumber in structure_ids: v = Voi(dcm.RTROIObservations[i].ROIObservationLabel.decode('iso-8859-1'), self.cube) v.read_dicom(dcm.RTROIObservations[i], item) self.add_voi(v) self.cube.xoffset = 0 self.cube.yoffset = 0 self.cube.zoffset = 0 """shift = min(self.cube.slice_pos)
def read_dicom(self, dcm): """ Imports CT-images from Dicom object. :param Dicom dcm: a Dicom object """ if "images" not in dcm: raise InputError("Data doesn't contain ct data") if not self.header_set: self.read_dicom_header(dcm) self.cube = np.zeros((self.dimz, self.dimy, self.dimx), dtype=np.int16) intersect = float(dcm["images"][0].RescaleIntercept) slope = float(dcm["images"][0].RescaleSlope) for i in range(len(dcm["images"])): data = np.array(dcm["images"][i].pixel_array) * slope + intersect self.cube[i][:][:] = data if self.slice_pos[1] < self.slice_pos[0]: self.slice_pos.reverse() self.zoffset = self.slice_pos[0] self.cube = self.cube[::-1]
def set_projectile(self, projectile): if projectile not in ['H', 'C', 'O', 'Ne']: raise InputError("Projectile not allowed") self.projectile = projectile
def split_plan(self, plan=None): self.targets = [] self.oar_list = [] dose = 0 for voi in self.plan.get_vois(): if voi.is_oar(): self.oar_list.append(voi) if voi.is_target(): self.targets.append(voi) if voi.get_dose() > dose: dose = voi.get_dose() if not len(self.targets): raise InputError("No targets") if not len(self.plan.get_fields()): raise InputError("No fields") self.target_dose = dose if plan is None: plan = self.plan proj = [] self.projectile_dose_level = {} for field in plan.fields: if field.get_projectile() not in proj: proj.append(field.get_projectile()) self.projectile_dose_level[field.get_projectile()] = 0 if len(proj) > 1: self.mult_proj = True else: self.mult_proj = False if self.mult_proj: self.projectiles = {} for field in plan.fields: if field.get_projectile() not in self.projectiles.keys(): self.projectiles[field.get_projectile()] = { "target_dos": DosCube(self.images), "fields": [field], "name": field.get_projectile(), "projectile": field.get_projectile() } else: self.projectiles[field.get_projectile()]["fields"].append( field) self.target_dos = DosCube(self.images) for i, voi in enumerate(self.targets): temp = DosCube(self.images) voi_dose_level = int(voi.get_dose() / dose * 1000) temp.load_from_structure(voi.get_voi().get_voi_data(), 1) for projectile, data in self.projectiles.items(): dose_percent = self.plan.get_dose_percent(projectile) if not voi.get_dose_percent(projectile) is None: dose_percent = voi.get_dose_percent(projectile) proj_dose_lvl = int(voi.get_dose() / self.target_dose * dose_percent * 10) if self.projectile_dose_level[projectile] < proj_dose_lvl: self.projectile_dose_level[projectile] = proj_dose_lvl if proj_dose_lvl == 0: proj_dose_lvl = -1 if i == 0: data["target_dos"] = temp * proj_dose_lvl else: data["target_dos"].merge_zero(temp * proj_dose_lvl) if i == 0: self.target_dos = temp * voi_dose_level else: self.target_dos.merge_zero(temp * voi_dose_level) for projectile, data in self.projectiles.items(): data["target_dos"].cube[data["target_dos"].cube == -1] = int(0) self.plan.add_dose(data["target_dos"], "target_%s" % projectile) self.rest_dose = copy.deepcopy(self.target_dos)
def create_dicom(self): """ Creates a DICOM RT-Dose object from self. This function can be used to convert a TRiP98 Dose file to DICOM format. :returns: a DICOM RT-Dose object. """ if not _dicom_loaded: raise ModuleNotLoadedError("DICOM") if not self.header_set: raise InputError("Header not loaded") ds = self.create_dicom_base() ds.Modality = 'RTDOSE' ds.SamplesPerPixel = 1 ds.BitsAllocated = self.num_bytes * 8 ds.BitsStored = self.num_bytes * 8 ds.AccessionNumber = '' ds.SeriesDescription = 'RT Dose' ds.DoseUnits = 'GY' ds.DoseType = 'PHYSICAL' ds.DoseGridScaling = self.target_dose / 10**5 ds.DoseSummationType = 'PLAN' ds.SliceThickness = '' ds.InstanceCreationDate = '19010101' ds.InstanceCreationTime = '000000' ds.NumberOfFrames = len(self.cube) ds.PixelRepresentation = 0 ds.StudyID = '1' ds.SeriesNumber = '14' # SeriesNumber tag 0x0020,0x0011 (type IS - Integer String) ds.GridFrameOffsetVector = [ x * self.slice_distance for x in range(self.dimz) ] ds.InstanceNumber = '' ds.PositionReferenceIndicator = "RF" ds.TissueHeterogeneityCorrection = ['IMAGE', 'ROI_OVERRIDE'] ds.ImagePositionPatient = [ "%.3f" % (self.xoffset * self.pixel_size), "%.3f" % (self.yoffset * self.pixel_size), "%.3f" % (self.slice_pos[0]) ] ds.SOPClassUID = '1.2.840.10008.5.1.4.1.1.481.2' ds.SOPInstanceUID = '1.2.246.352.71.7.320687012.47206.20090603085223' # Study Instance UID tag 0x0020,0x000D (type UI - Unique Identifier) # self._dicom_study_instance_uid may be either set in __init__ when creating new object # or set when import a DICOM file # Study Instance UID for structures is the same as Study Instance UID for CTs ds.StudyInstanceUID = self._dicom_study_instance_uid # Series Instance UID tag 0x0020,0x000E (type UI - Unique Identifier) # self._dose_dicom_series_instance_uid may be either set in __init__ when creating new object # Series Instance UID might be different than Series Instance UID for CTs ds.SeriesInstanceUID = self._dose_dicom_series_instance_uid # Bind to rtplan rt_set = Dataset() rt_set.RefdSOPClassUID = '1.2.840.10008.5.1.4.1.1.481.5' rt_set.RefdSOPInstanceUID = '1.2.3' ds.ReferencedRTPlanSequence = Sequence([rt_set]) pixel_array = np.zeros((len(self.cube), ds.Rows, ds.Columns), dtype=self.pydata_type) pixel_array[:][:][:] = self.cube[:][:][:] ds.PixelData = pixel_array.tostring() return ds
def create_dicom_base(self): if _dicom_loaded is False: raise ModuleNotLoadedError("Dicom") if self.header_set is False: raise InputError("Header not loaded") # TODO tags + code datatypes are described here: # https://www.dabsoft.ch/dicom/6/6/#(0020,0012) # datatype codes are described here: # ftp://dicom.nema.org/medical/DICOM/2013/output/chtml/part05/sect_6.2.html meta = Dataset() meta.MediaStorageSOPClassUID = '1.2.840.10008.5.1.4.1.1.2' # CT Image Storage # Media Storage SOP Instance UID tag 0x0002,0x0003 (type UI - Unique Identifier) meta.MediaStorageSOPInstanceUID = self._ct_sop_instance_uid meta.ImplementationClassUID = "1.2.3.4" meta.TransferSyntaxUID = UID.ImplicitVRLittleEndian # Implicit VR Little Endian - Default Transfer Syntax ds = FileDataset("file", {}, file_meta=meta, preamble=b"\0" * 128) ds.PatientsName = self.patient_name if self.patient_id in (None, ''): ds.PatientID = datetime.datetime.today().strftime('%Y%m%d-%H%M%S') else: ds.PatientID = self.patient_id # Patient ID tag 0x0010,0x0020 (type LO - Long String) ds.PatientsSex = '' # Patient's Sex tag 0x0010,0x0040 (type CS - Code String) # Enumerated Values: M = male F = female O = other. ds.PatientsBirthDate = '19010101' ds.SpecificCharacterSet = 'ISO_IR 100' ds.AccessionNumber = '' ds.is_little_endian = True ds.is_implicit_VR = True ds.SOPClassUID = '1.2.3' # !!!!!!!! # SOP Instance UID tag 0x0008,0x0018 (type UI - Unique Identifier) ds.SOPInstanceUID = self._ct_sop_instance_uid # Study Instance UID tag 0x0020,0x000D (type UI - Unique Identifier) # self._dicom_study_instance_uid may be either set in __init__ when creating new object # or set when import a DICOM file # Study Instance UID for structures is the same as Study Instance UID for CTs ds.StudyInstanceUID = self._dicom_study_instance_uid # Series Instance UID tag 0x0020,0x000E (type UI - Unique Identifier) # self._ct_dicom_series_instance_uid may be either set in __init__ when creating new object # or set when import a DICOM file # Series Instance UID for structures might be different than Series Instance UID for CTs ds.SeriesInstanceUID = self._ct_dicom_series_instance_uid # Study Instance UID tag 0x0020,0x000D (type UI - Unique Identifier) ds.FrameofReferenceUID = '1.2.3' # !!!!!!!!! ds.StudyDate = datetime.datetime.today().strftime('%Y%m%d') ds.StudyTime = datetime.datetime.today().strftime('%H%M%S') ds.PhotometricInterpretation = 'MONOCHROME2' ds.SamplesPerPixel = 1 ds.ImageOrientationPatient = ['1', '0', '0', '0', '1', '0'] ds.Rows = self.dimx ds.Columns = self.dimy ds.SliceThickness = str(self.slice_distance) ds.PixelSpacing = [self.pixel_size, self.pixel_size] # Add eclipse friendly IDs ds.StudyID = '1' # Study ID tag 0x0020,0x0010 (type SH - Short String) ds.ReferringPhysiciansName = 'py^trip' # Referring Physician's Name tag 0x0008,0x0090 (type PN - Person Name) ds.PositionReferenceIndicator = '' # Position Reference Indicator tag 0x0020,0x1040 ds.SeriesNumber = '1' # SeriesNumber tag 0x0020,0x0011 (type IS - Integer String) return ds