Download libraries/Process/RTplan.py from AI4MIRO/nnUNet_Brain_EPTN: direct link, hf CLI and curl.
- Browser
- Download file 18.1 kB
-
https://huggingface.co/AI4MIRO/nnUNet_Brain_EPTN/resolve/refs%2Fpr%2F1/libraries/Process/RTplan.py
- Command line
-
hf download hf://AI4MIRO/nnUNet_Brain_EPTN@refs/pr/1/libraries/Process/RTplan.py
-
curl -L -o RTplan.py https://huggingface.co/AI4MIRO/nnUNet_Brain_EPTN/resolve/refs%2Fpr%2F1/libraries/Process/RTplan.py
18.1 kB
| import pydicom | |
| import numpy as np | |
| import math | |
| import time | |
| import pickle, scipy | |
| class RTplan: | |
| def __init__(self): | |
| self.SeriesInstanceUID = "" | |
| self.SOPInstanceUID = "" | |
| self.PatientInfo = {} | |
| self.StudyInfo = {} | |
| self.DcmFile = "" | |
| self.Modality = "" | |
| self.RadiationType = "" | |
| self.ScanMode = "" | |
| self.TreatmentMachineName = "" | |
| self.NumberOfFractionsPlanned = 1 | |
| self.NumberOfSpots = 0 | |
| self.Beams = [] | |
| self.TotalMeterset = 0.0 | |
| self.PlanName = "" | |
| self.isLoaded = 0 | |
| self.beamlets = [] | |
| self.OriginalDicomDataset = [] | |
| def print_plan_info(self, prefix=""): | |
| print(prefix + "Plan: " + self.SeriesInstanceUID) | |
| print(prefix + " " + self.DcmFile) | |
| def import_Dicom_plan(self): | |
| if(self.isLoaded == 1): | |
| print("Warning: RTplan " + self.SeriesInstanceUID + " is already loaded") | |
| return | |
| dcm = pydicom.dcmread(self.DcmFile) | |
| self.OriginalDicomDataset = dcm | |
| # Photon plan | |
| if dcm.SOPClassUID == "1.2.840.10008.5.1.4.1.1.481.5": | |
| print("ERROR: Conventional radiotherapy (photon) plans are not supported") | |
| self.Modality = "Radiotherapy" | |
| return | |
| # Ion plan | |
| elif dcm.SOPClassUID == "1.2.840.10008.5.1.4.1.1.481.8": | |
| self.Modality = "Ion therapy" | |
| if dcm.IonBeamSequence[0].RadiationType == "PROTON": | |
| self.RadiationType = "Proton" | |
| else: | |
| print("ERROR: Radiation type " + dcm.IonBeamSequence[0].RadiationType + " not supported") | |
| self.RadiationType = dcm.IonBeamSequence[0].RadiationType | |
| return | |
| if dcm.IonBeamSequence[0].ScanMode == "MODULATED": | |
| self.ScanMode = "MODULATED" # PBS | |
| else: | |
| print("ERROR: Scan mode " + dcm.IonBeamSequence[0].ScanMode + " not supported") | |
| self.ScanMode = dcm.IonBeamSequence[0].ScanMode | |
| return | |
| # Other | |
| else: | |
| print("ERROR: Unknown SOPClassUID " + dcm.SOPClassUID + " for file " + self.DcmFile) | |
| self.Modality = "Unknown" | |
| return | |
| # Start parsing PBS plan | |
| self.SOPInstanceUID = dcm.SOPInstanceUID | |
| self.NumberOfFractionsPlanned = int(dcm.FractionGroupSequence[0].NumberOfFractionsPlanned) | |
| self.NumberOfSpots = 0 | |
| self.TotalMeterset = 0 | |
| if(hasattr(dcm.IonBeamSequence[0], 'TreatmentMachineName')): | |
| self.TreatmentMachineName = dcm.IonBeamSequence[0].TreatmentMachineName | |
| else: | |
| self.TreatmentMachineName = "" | |
| for dcm_beam in dcm.IonBeamSequence: | |
| if dcm_beam.TreatmentDeliveryType != "TREATMENT": | |
| continue | |
| first_layer = dcm_beam.IonControlPointSequence[0] | |
| beam = Plan_IonBeam() | |
| beam.SeriesInstanceUID = self.SeriesInstanceUID | |
| beam.BeamName = dcm_beam.BeamName | |
| beam.IsocenterPosition = [float(first_layer.IsocenterPosition[0]), float(first_layer.IsocenterPosition[1]), float(first_layer.IsocenterPosition[2])] | |
| beam.GantryAngle = float(first_layer.GantryAngle) | |
| beam.PatientSupportAngle = float(first_layer.PatientSupportAngle) | |
| beam.FinalCumulativeMetersetWeight = float(dcm_beam.FinalCumulativeMetersetWeight) | |
| # find corresponding beam in FractionGroupSequence (beam order may be different from IonBeamSequence) | |
| ReferencedBeam_id = next((x for x, val in enumerate(dcm.FractionGroupSequence[0].ReferencedBeamSequence) if val.ReferencedBeamNumber == dcm_beam.BeamNumber), -1) | |
| if ReferencedBeam_id == -1: | |
| print("ERROR: Beam number " + dcm_beam.BeamNumber + " not found in FractionGroupSequence.") | |
| print("This beam is therefore discarded.") | |
| continue | |
| else: beam.BeamMeterset = float(dcm.FractionGroupSequence[0].ReferencedBeamSequence[ReferencedBeam_id].BeamMeterset) | |
| self.TotalMeterset += beam.BeamMeterset | |
| if dcm_beam.NumberOfRangeShifters == 0: | |
| beam.RangeShifterID = "" | |
| beam.RangeShifterType = "none" | |
| elif dcm_beam.NumberOfRangeShifters == 1: | |
| beam.RangeShifterID = dcm_beam.RangeShifterSequence[0].RangeShifterID | |
| if dcm_beam.RangeShifterSequence[0].RangeShifterType == "BINARY": | |
| beam.RangeShifterType = "binary" | |
| elif dcm_beam.RangeShifterSequence[0].RangeShifterType == "ANALOG": | |
| beam.RangeShifterType = "analog" | |
| else: | |
| print("ERROR: Unknown range shifter type for beam " + dcm_beam.BeamName) | |
| beam.RangeShifterType = "none" | |
| else: | |
| print("ERROR: More than one range shifter defined for beam " + dcm_beam.BeamName) | |
| beam.RangeShifterID = "" | |
| beam.RangeShifterType = "none" | |
| SnoutPosition = 0 | |
| if hasattr(first_layer, 'SnoutPosition'): | |
| SnoutPosition = float(first_layer.SnoutPosition) | |
| IsocenterToRangeShifterDistance = SnoutPosition | |
| RangeShifterWaterEquivalentThickness = "" | |
| RangeShifterSetting = "OUT" | |
| ReferencedRangeShifterNumber = 0 | |
| if hasattr(first_layer, 'RangeShifterSettingsSequence'): | |
| if hasattr(first_layer.RangeShifterSettingsSequence[0], 'IsocenterToRangeShifterDistance'): | |
| IsocenterToRangeShifterDistance = float(first_layer.RangeShifterSettingsSequence[0].IsocenterToRangeShifterDistance) | |
| if hasattr(first_layer.RangeShifterSettingsSequence[0], 'RangeShifterWaterEquivalentThickness'): | |
| RangeShifterWaterEquivalentThickness = float(first_layer.RangeShifterSettingsSequence[0].RangeShifterWaterEquivalentThickness) | |
| if hasattr(first_layer.RangeShifterSettingsSequence[0], 'RangeShifterSetting'): | |
| RangeShifterSetting = first_layer.RangeShifterSettingsSequence[0].RangeShifterSetting | |
| if hasattr(first_layer.RangeShifterSettingsSequence[0], 'ReferencedRangeShifterNumber'): | |
| ReferencedRangeShifterNumber = int(first_layer.RangeShifterSettingsSequence[0].ReferencedRangeShifterNumber) | |
| CumulativeMeterset = 0 | |
| for dcm_layer in dcm_beam.IonControlPointSequence: | |
| if dcm_layer.NumberOfScanSpotPositions == 1: sum_weights = dcm_layer.ScanSpotMetersetWeights | |
| else: sum_weights = sum(dcm_layer.ScanSpotMetersetWeights) | |
| if sum_weights == 0.0: | |
| continue | |
| layer = Plan_IonLayer() | |
| layer.SeriesInstanceUID = self.SeriesInstanceUID | |
| if hasattr(dcm_layer, 'SnoutPosition'): | |
| SnoutPosition = float(dcm_layer.SnoutPosition) | |
| if hasattr(dcm_layer, 'NumberOfPaintings'): layer.NumberOfPaintings = int(dcm_layer.NumberOfPaintings) | |
| else: layer.NumberOfPaintings = 1 | |
| layer.NominalBeamEnergy = float(dcm_layer.NominalBeamEnergy) | |
| layer.ScanSpotPositionMap_x = dcm_layer.ScanSpotPositionMap[0::2] | |
| layer.ScanSpotPositionMap_y = dcm_layer.ScanSpotPositionMap[1::2] | |
| layer.ScanSpotMetersetWeights = dcm_layer.ScanSpotMetersetWeights | |
| layer.SpotMU = np.array(dcm_layer.ScanSpotMetersetWeights) * beam.BeamMeterset / beam.FinalCumulativeMetersetWeight # spot weights are converted to MU | |
| if layer.SpotMU.size == 1: layer.SpotMU = [layer.SpotMU] | |
| else: layer.SpotMU = layer.SpotMU.tolist() | |
| self.NumberOfSpots += len(layer.SpotMU) | |
| CumulativeMeterset += sum(layer.SpotMU) | |
| layer.CumulativeMeterset = CumulativeMeterset | |
| if beam.RangeShifterType != "none": | |
| if hasattr(dcm_layer, 'RangeShifterSettingsSequence'): | |
| RangeShifterSetting = dcm_layer.RangeShifterSettingsSequence[0].RangeShifterSetting | |
| ReferencedRangeShifterNumber = dcm_layer.RangeShifterSettingsSequence[0].ReferencedRangeShifterNumber | |
| if hasattr(dcm_layer.RangeShifterSettingsSequence[0], 'IsocenterToRangeShifterDistance'): | |
| IsocenterToRangeShifterDistance = dcm_layer.RangeShifterSettingsSequence[0].IsocenterToRangeShifterDistance | |
| if hasattr(dcm_layer.RangeShifterSettingsSequence[0], 'RangeShifterWaterEquivalentThickness'): | |
| RangeShifterWaterEquivalentThickness = dcm_layer.RangeShifterSettingsSequence[0].RangeShifterWaterEquivalentThickness | |
| layer.RangeShifterSetting = RangeShifterSetting | |
| layer.IsocenterToRangeShifterDistance = IsocenterToRangeShifterDistance | |
| layer.RangeShifterWaterEquivalentThickness = RangeShifterWaterEquivalentThickness | |
| layer.ReferencedRangeShifterNumber = ReferencedRangeShifterNumber | |
| beam.Layers.append(layer) | |
| self.Beams.append(beam) | |
| self.isLoaded = 1 | |
| def export_Dicom_with_new_UID(self, OutputFile): | |
| # generate new uid | |
| initial_uid = self.OriginalDicomDataset.SOPInstanceUID | |
| new_uid = pydicom.uid.generate_uid() | |
| self.OriginalDicomDataset.SOPInstanceUID = new_uid | |
| # save dicom file | |
| print("Export dicom RTPLAN: " + OutputFile) | |
| self.OriginalDicomDataset.save_as(OutputFile) | |
| # restore initial uid | |
| self.OriginalDicomDataset.SOPInstanceUID = initial_uid | |
| return new_uid | |
| def save(self, file_path): | |
| beamlets = self.beamlets | |
| self.beamlets = [] | |
| with open(file_path, 'wb') as fid: | |
| pickle.dump(self.__dict__, fid) | |
| self.beamlets = beamlets | |
| def load(self, file_path): | |
| with open(file_path, 'rb') as fid: | |
| tmp = pickle.load(fid) | |
| self.__dict__.update(tmp) | |
| def compute_cartesian_coordinates(self, CT, Scanner, beams, RangeShifters=[]): | |
| time_start = time.time() | |
| SPR = SPRimage() | |
| SPR.convert_CT_to_SPR(CT, Scanner) | |
| CTborders_x = [SPR.ImagePositionPatient[0], SPR.ImagePositionPatient[0] + SPR.GridSize[0] * SPR.PixelSpacing[0]] | |
| CTborders_y = [SPR.ImagePositionPatient[1], SPR.ImagePositionPatient[1] + SPR.GridSize[1] * SPR.PixelSpacing[1]] | |
| CTborders_z = [SPR.ImagePositionPatient[2], SPR.ImagePositionPatient[2] + SPR.GridSize[2] * SPR.PixelSpacing[2]] | |
| spot_positions = [] | |
| spot_directions = [] | |
| spot_ranges = [] | |
| # initialize spot info for raytracing | |
| for beam in self.Beams: | |
| #beam = self.Beams[b] | |
| RangeShifter = -1 | |
| if beam.RangeShifterType == "binary": | |
| RangeShifter = next((RS for RS in RangeShifters if RS.ID == beam.RangeShifterID), -1) | |
| for layer in beam.Layers: | |
| range_in_water = SPR.energyToRange(layer.NominalBeamEnergy)*10 | |
| if(layer.RangeShifterSetting == 'IN'): | |
| if(layer.RangeShifterWaterEquivalentThickness != ""): RangeShifter_WET = layer.RangeShifterWaterEquivalentThickness | |
| elif(RangeShifter != -1): RangeShifter_WET = RangeShifter.WET | |
| else: RangeShifter_WET = 0.0 | |
| if(RangeShifter_WET > 0.0): range_in_water -= RangeShifter_WET | |
| for s in range(len(layer.ScanSpotPositionMap_x)): | |
| # BEV coordinates to 3D coordinates: position (x,y,z) and direction (u,v,w) | |
| x,y,z = layer.ScanSpotPositionMap_x[s], 0, layer.ScanSpotPositionMap_y[s] | |
| u,v,w = 1e-10, 1.0, 1e-10 | |
| # rotation for gantry angle (around Z axis) | |
| angle = math.radians(beam.GantryAngle) | |
| [x,y,z] = self.Rotate_vector([x,y,z], angle, 'z') | |
| [u,v,w] = self.Rotate_vector([u,v,w], angle, 'z') | |
| # rotation for couch angle (around Y axis) | |
| angle = math.radians(beam.PatientSupportAngle) | |
| [x,y,z] = self.Rotate_vector([x,y,z], angle, 'y') | |
| [u,v,w] = self.Rotate_vector([u,v,w], angle, 'y') | |
| # Dicom CT coordinates | |
| x = x + beam.IsocenterPosition[0] | |
| y = y + beam.IsocenterPosition[1] | |
| z = z + beam.IsocenterPosition[2] | |
| # translate initial position at the CT image border | |
| Translation = np.array([1.0, 1.0, 1.0]) | |
| Translation[0] = (x - CTborders_x[int(u<0)]) / u | |
| Translation[1] = (y - CTborders_y[int(v<0)]) / v | |
| Translation[2] = (z - CTborders_z[int(w<0)]) / w | |
| Translation = Translation[np.argmin(np.absolute(Translation))] | |
| x = x - Translation * u | |
| y = y - Translation * v | |
| z = z - Translation * w | |
| # append data to the list of spots to process | |
| spot_positions.append([x,y,z]) | |
| spot_directions.append([u,v,w]) | |
| spot_ranges.append(range_in_water) | |
| CartesianSpotPositions = compute_position_from_range(SPR, spot_positions, spot_directions, spot_ranges) | |
| print("Spot RayTracing: " + str(time.time()-time_start) + " sec") | |
| return CartesianSpotPositions | |
| #TO ADAPT | |
| def compute_spot_maps(self, CT, plan, Struct, RangeShifters): | |
| # Find BODY | |
| for ROI in Struct.Contours: | |
| if ROI.ROIName == 'BODY': | |
| BODY = ROI | |
| break | |
| # Initialize Spot Maps | |
| SpotMapBinary = np.full((CT.GridSize[0], CT.GridSize[1], CT.GridSize[2], len(plan.Beams)), False) | |
| SpotMapWeights = np.zeros((CT.GridSize[0], CT.GridSize[1], CT.GridSize[2], len(plan.Beams))) | |
| #Compute cartesian coordinates | |
| CartesianCoordinates = plan.compute_cartesian_coordinates(CT, 'UCL_Toshiba', plan.Beams, RangeShifters) | |
| # Initialize SpotID | |
| SpotsID = np.zeros((plan.NumberOfSpots, 4),dtype=int) | |
| SpotNumber = 0 | |
| SpotsOutsideBody = 0 | |
| # Repeat = [] | |
| # SpotsID_repeat = [] | |
| print('ImagePositionPatient',CT.ImagePositionPatient) | |
| # Compute SpotsID | |
| print('\n') | |
| counter = 0 | |
| for beamNumber, beam in enumerate(plan.Beams): | |
| print('nb of layers',len(beam.Layers)) | |
| for layer in beam.Layers: | |
| for weight in layer.SpotMU: | |
| #print('SpotNumber',SpotNumber) | |
| #print('CartesianCoordinates',CartesianCoordinates[SpotNumber]) | |
| #print('ImagePositionPatient',CT.ImagePositionPatient) | |
| #print('PixelSpacing',CT.PixelSpacing) | |
| # x = int((CartesianCoordinates[SpotNumber][0] - CT.ImagePositionPatient[0])/CT.PixelSpacing[0])-1 | |
| # y = int((CartesianCoordinates[SpotNumber][1] - CT.ImagePositionPatient[1])/CT.PixelSpacing[1])-1 | |
| # z = int((CartesianCoordinates[SpotNumber][2] - CT.ImagePositionPatient[2])/CT.PixelSpacing[2])-1 | |
| x = int(CartesianCoordinates[SpotNumber][0]/CT.PixelSpacing[0]) | |
| y = int(CartesianCoordinates[SpotNumber][1]/CT.PixelSpacing[1]) | |
| z = int(CartesianCoordinates[SpotNumber][2]/CT.PixelSpacing[2]) | |
| SpotsID[SpotNumber,:] = [x,y,z,beamNumber] | |
| #print('coordinates',[x,y,z,beamNumber]) | |
| #print('Spots ID',SpotsID[SpotNumber,:]) | |
| SpotNumber += 1 | |
| if BODY.Mask[x,y,z]==False: # avoid spots outside BODY | |
| SpotsOutsideBody += 1 | |
| print('This spot is outside the body',[x,y,z]) | |
| continue | |
| # if (x,y,z,beamNumber) in SpotsID_repeat: | |
| # Repeat.append((x,y,z,beamNumber)) | |
| # SpotsID_repeat.append((x,y,z,beamNumber)) | |
| SpotMapBinary[x,y,z,beamNumber] = True | |
| SpotMapWeights[x,y,z,beamNumber] = weight | |
| counter+=1 | |
| # We can get SpotMapBinary as boolean SpotMapWeights | |
| # plan.SpotMapBinary = SpotMapBinary | |
| plan.SpotMapWeights = SpotMapWeights | |
| # print('SpotsID beam_' + str(beamNumber) + ' computed...') | |
| # print('nb of iteration',counter) | |
| # save_file_binary = os.path.join(dst_dir, 'SpotMapBinary_beams.npz') | |
| # np.savez_compressed(save_file_binary,SpotMapBinary) | |
| # print('\nSpot Map Binary saved = ' + save_file_binary) | |
| # save_file_weights = os.path.join(dst_dir, 'SpotMapWeights_beams.npz') | |
| # np.savez_compressed(save_file_weights, SpotMapWeights.astype(np.float16)) | |
| # print('Spot Map Weights saved = ' + save_file_weights) | |
| # save_file_ID = os.path.join(dst_dir, 'spotsID.npz') | |
| # np.savez_compressed(save_file_ID, SpotsID) | |
| # print('SpotsID saved = ' + save_file_ID) | |
| # print('\nNumber of Spots in plan = ' + str(plan.NumberOfSpots)) | |
| # print('Number of Spots in binary mask = ' + str(np.ndarray.flatten(SpotMapBinary).tolist().count(True))) | |
| # print('Spots placed outside the body = ' + (str(SpotsOutsideBody))) | |
| # if len(Repeat)!=0: | |
| # spots_repeated_by_patient.append(['Patient_' + str(PatientNumber) + ' (' + Patient.PatientInfo.PatientName + ')', len(Repeat)]) | |
| # print('\nAt this point, there are ' + str(len(spots_repeated_by_patient)) + ' patients with spots repeated') | |
| # for repeat in spots_repeated_by_patient: | |
| # print(repeat[0] + ' has ' + str(repeat[1]) + ' spots repeated') | |
| # print('\n') | |
| def Rotate_vector(self, vec, angle, axis): | |
| if axis == 'x': | |
| x = vec[0] | |
| y = vec[1] * math.cos(angle) - vec[2] * math.sin(angle) | |
| z = vec[1] * math.sin(angle) + vec[2] * math.cos(angle) | |
| elif axis == 'y': | |
| x = vec[0] * math.cos(angle) + vec[2] * math.sin(angle) | |
| y = vec[1] | |
| z = -vec[0] * math.sin(angle) + vec[2] * math.cos(angle) | |
| elif axis == 'z': | |
| x = vec[0] * math.cos(angle) - vec[1] * math.sin(angle) | |
| y = vec[0] * math.sin(angle) + vec[1] * math.cos(angle) | |
| z = vec[2] | |
| return [x,y,z] | |
| class Plan_IonBeam: | |
| def __init__(self): | |
| self.SeriesInstanceUID = "" | |
| self.BeamName = "" | |
| self.IsocenterPosition = [0,0,0] | |
| self.GantryAngle = 0.0 | |
| self.PatientSupportAngle = 0.0 | |
| self.FinalCumulativeMetersetWeight = 0.0 | |
| self.BeamMeterset = 0.0 | |
| self.RangeShifter = "none" | |
| self.Layers = [] | |
| class Plan_IonLayer: | |
| def __init__(self): | |
| self.SeriesInstanceUID = "" | |
| self.NumberOfPaintings = 1 | |
| self.NominalBeamEnergy = 0.0 | |
| self.ScanSpotPositionMap_x = [] | |
| self.ScanSpotPositionMap_y = [] | |
| self.ScanSpotMetersetWeights = [] | |
| self.SpotMU = [] | |
| self.CumulativeMeterset = 0.0 | |
| self.RangeShifterSetting = 'OUT' | |
| self.IsocenterToRangeShifterDistance = 0.0 | |
| self.RangeShifterWaterEquivalentThickness = 0.0 | |
| self.ReferencedRangeShifterNumber = 0 | |