From 8562af1c13de34184044ccb97b02130149eef644 Mon Sep 17 00:00:00 2001 From: Clara Date: Tue, 28 Feb 2023 01:25:55 +0000 Subject: [PATCH 01/12] test --- Configs/SettingsRTOG_Multi.ini.bak | 59 +++ Configs/SettingsRTOG_Multi10.ini~ | 69 ++++ ConvertNii.py | 101 +++++ Count.py | 68 ++++ DataGenerator/DataGenerator.py | 558 ++++++++++++++-------------- DefaultConfiguration.ini | 65 ---- FormMasks.py | 70 ++++ Models/Classifier.py | 29 +- Models/Linear.py | 13 +- Models/MixModel.py | 10 +- SegCT.py | 24 ++ Training/#InnerEye.py# | 73 ---- Training/Regression.py | 39 +- Training/ShapAnalysis.py | 125 +++++++ Training/{val2.py => Validation.py} | 49 ++- Training/test.py | 42 +++ Training/val.py | 157 -------- Utils/DicomTools.py | 46 +-- Utils/PredictionReports.py | 57 ++- scripts/CropMask.py | 32 ++ scripts/DICOM2NIFTY.py | 66 ++++ scripts/DoseNii.py | 71 ++++ scripts/FMaskDcm.py | 75 ++++ scripts/PTV.py | 103 +++++ scripts/RTOG.py | 54 +++ scripts/RTOG_nii.py | 38 ++ scripts/RenameDicomPatientID.py | 48 +++ check.py => scripts/check.py | 0 renameCT.py => scripts/renameCT.py | 0 scripts/sub_dose_analysis_total.py | 88 +++++ scripts/test.py | 52 +++ scripts/xnat_data_uniformization.py | 95 +++++ submitjob.pbs | 39 ++ submitjob2.pbs | 39 ++ 34 files changed, 1768 insertions(+), 686 deletions(-) create mode 100644 Configs/SettingsRTOG_Multi.ini.bak create mode 100644 Configs/SettingsRTOG_Multi10.ini~ create mode 100644 ConvertNii.py create mode 100644 Count.py delete mode 100644 DefaultConfiguration.ini create mode 100644 FormMasks.py create mode 100644 SegCT.py delete mode 100644 Training/#InnerEye.py# create mode 100644 Training/ShapAnalysis.py rename Training/{val2.py => Validation.py} (79%) create mode 100644 Training/test.py delete mode 100644 Training/val.py create mode 100644 scripts/CropMask.py create mode 100644 scripts/DICOM2NIFTY.py create mode 100644 scripts/DoseNii.py create mode 100644 scripts/FMaskDcm.py create mode 100644 scripts/PTV.py create mode 100644 scripts/RTOG.py create mode 100644 scripts/RTOG_nii.py create mode 100644 scripts/RenameDicomPatientID.py rename check.py => scripts/check.py (100%) rename renameCT.py => scripts/renameCT.py (100%) create mode 100644 scripts/sub_dose_analysis_total.py create mode 100644 scripts/test.py create mode 100644 scripts/xnat_data_uniformization.py create mode 100644 submitjob.pbs create mode 100644 submitjob2.pbs diff --git a/Configs/SettingsRTOG_Multi.ini.bak b/Configs/SettingsRTOG_Multi.ini.bak new file mode 100644 index 0000000..0cc94e0 --- /dev/null +++ b/Configs/SettingsRTOG_Multi.ini.bak @@ -0,0 +1,59 @@ +[MODEL] +Activation = 'Sigmoid' +Backbone = 'DenseNet' +Records_Backbone = 'Linear' +batch_size = 10 +Prediction_type = 'Classification' +CT_spatial_dims = 3 +Loss_Function = 'BCEWithLogitsLoss' + +[MODEL_PARAMETERS] +spatial_dims = 3 +in_channels = 10 +out_channels = 1 +block_config = [1,2,4,2] +dropout_prob = 0.3 + +[MODALITY] +CT = '1' +Dose = '1' +Structs = '1' + +[DATA] +Nifty = false +n_classes = 1 +Multichannel = true +dim = [128,128,32] +threshold = 24 +Structs = ['esophagus','lung','trachea','heart_atrium_left', 'heart_atrium_right', 'heart_myocardium', + 'heart_ventricle_left', 'heart_ventricle_right', 'pulmonary_artery','ptv'] +DataFolder = '/home/dgs1/data/OutcomePrediction/' +vis = [0] +train_size = 0.7 +val_size = 0.3 +target = 'survival_months' + +[Records] +category_feat = ['arm', 'gender', 'race', 'ethnicity', 'zubrod', + 'histology','ajcc_stage_grp', 'rt_technique', + 'smoke_hx', 'rx_terminated_ae', 'received_conc_chemo','rt_compliance_ptv90', + 'received_cons_chemo','rt_dose','pet_staging','received_rt'] +numerical_feat = ['age', 'volume_ptv'] + + +[CHECKPOINT] +monitor = "val_loss" #"val_acc_epoch" +mode = "max" +matrix = ['ROC', 'Specificity','Sensitivity','Accuracy','Precision'] + + +[SERVER] +Address = 'http://128.16.11.124:8080/xnat' +Projects = ["RTOG_0617"] +User = "yzhan" +Password = "yzhan" +[CRITERIA] +analysis_inclusion = 1 +[FILTER] +patient_id = ['0617-789552', '0617-607275', '0617-568854', '0617-559978', '0617-805653','0617-693977', '0617-658810', '0617-605230','0617-689511','0617-444138','0617-449451','0617-755214','0617-763022','0617-759543','0617-629653','0617-761615','0617-761493','0617-511240', '0617-608184', '0617-704484', '0617-469897', '0617-664444', '0617-647889', '0617-669208', '0617-619079', '0617-657744', '0617-457079', '0617-592873', '0617-673681', '0617-698612', '0617-529851', '0617-674729', '0617-466966', '0617-747279', '0617-619629', '0617-637689', '0617-682647', '0617-727320', '0617-596122', '0617-714162', '0617-381906', '0617-575441', '0617-540476', '0617-647078', '0617-654574', '0617-457446', '0617-752117', '0617-635346', '0617-714849', '0617-712281', '0617-438343', '0617-617572', '0617-536867', '0617-579384', '0617-656006', '0617-716741', '0617-553859', '0617-730769', '0617-446756', '0617-445118', '0617-621065', '0617-602544', '0617-483282', '0617-548132', '0617-739625'] + diff --git a/Configs/SettingsRTOG_Multi10.ini~ b/Configs/SettingsRTOG_Multi10.ini~ new file mode 100644 index 0000000..8834c22 --- /dev/null +++ b/Configs/SettingsRTOG_Multi10.ini~ @@ -0,0 +1,69 @@ +[MODEL] +Activation = 'Identity' +Backbone = 'DenseNet' +Records_Backbone = 'Linear' +batch_size = 10 +Prediction_type = 'Classification' +CT_spatial_dims = 3 +Loss_Function = 'BCEWithLogitsLoss' + +[MODEL_PARAMETERS] +spatial_dims = 3 +in_channels = 3 +out_channels = 512 +block_config = [1,2,4,2] +dropout_prob = 0.3 + +[MODALITY] +CT = '1' +Dose = '1' +Structs = '1' + +[DATA] +Nifty = true +n_classes = 1 +Multichannel = true +dim = [128,128,32] +threshold = 24 +Structs = ['esophagus','lung', 'heart', 'ptv'] +DataFolder = '/home/dgs1/data/Segmentation/nii_root_folder/RTOG/' +vis = [0] +train_size = 0.7 +val_size = 0.3 +target = 'survival_months' + +[Records] +category_feat = ['arm', 'gender', 'race', 'ethnicity', 'zubrod', + 'histology', 'nonsquam_squam', 'ajcc_stage_grp', 'rt_technique', + 'smoke_hx', 'rx_terminated_ae', 'received_conc_chemo','rt_compliance_ptv90', + 'received_cons_chemo','rt_dose','pet_staging','received_rt'] +numerical_feat = ['age', 'volume_ptv','v5_lung','v20_lung','dmean_lung', + 'v5_heart','v30_heart','v20_esophagus','v60_esophagus'] + + +[CHECKPOINT] +monitor = "val_loss" #"val_acc_epoch" +mode = "max" +matrix = ['ROC', 'Specificity','Sensitivity','Accuracy','Precision'] + + +[SERVER] +Address = 'http://128.16.11.124:8080/xnat' +Projects = ["RTOG_0617"] +User = "yzhan" +Password = "yzhan" +[CRITERIA] +analysis_inclusion = 1 +[FILTER] +patient_id = ['0617-529851', '0617-446756', '0617-647889', '0617-608184', '0617-483479', '0617-381906', '0617-637689', '0617-568854', '0617-444138', + '0617-619629', '0617-698612', '0617-647078', '0617-704484', '0617-482383', '0617-704274', '0617-727320', '0617-674729', '0617-583972', + '0617-309598', '0617-511240', '0617-656006', '0617-658810', '0617-712281', '0617-559978', '0617-761615', '0617-673098', '0617-673681', + '0617-605230', '0617-669208', '0617-607275', '0617-457079', '0617-730769', '0617-449451', '0617-752117', '0617-635346', '0617-657744', + '0617-739625', '0617-736925', '0617-620721', '0617-617572', '0617-438343', '0617-553859', '0617-483282', '0617-690485', '0617-689511', + '0617-575441', '0617-789552', '0617-682647', '0617-445118', '0617-536867', '0617-457446', '0617-592873', '0617-540476', '0617-619079', + '0617-548132', '0617-535250', '0617-654574', '0617-621065', '0617-714849', '0617-613919', '0617-714162', '0617-540662', '0617-711646', + '0617-602544', '0617-693977', '0617-596122', '0617-716741', '0617-466966', '0617-579384', '0617-664444', '0617-469897', '0617-761493', + '0617-747279', '0617-763022', '0617-805653', '0617-640690'] + + + diff --git a/ConvertNii.py b/ConvertNii.py new file mode 100644 index 0000000..9413f5e --- /dev/null +++ b/ConvertNii.py @@ -0,0 +1,101 @@ +import sys +#sys.path.insert(0, '/home/dgs1/Software/OutcomePrediction/') +import toml +import glob +import nibabel as nib +import numpy as np +from Utils.DicomTools import * +from pathlib import Path +from DataGenerator.DataGenerator import QuerySubjectList, SynchronizeData, QuerySubjectInfo +from Utils.DicomTools import * +import os +from rt_utils import RTStructBuilder +import xnat +from Utils.FixRTSS import * +session = xnat.connect('http://128.16.11.124:8080/xnat', user='admin', password='mortavar1977') +import csv +from monai.transforms import Spacing, LoadImage, EnsureChannelFirst +from monai.data import MetaTensor +import itk +config = toml.load(sys.argv[1]) +from scipy import ndimage +session = xnat.connect(config['SERVER']['Address'], user=config['SERVER']['User'],password=config['SERVER']['Password']) +SubjectList = QuerySubjectList(config, session) +print(SubjectList) +SynchronizeData(config, SubjectList) + +for key in config['MODALITY'].keys(): + SubjectList[key+'_Path'] = "" +QuerySubjectInfo(config, SubjectList, session) +# +# roi_series = ['Heart', 'Oesophagus', 'Spinal Canal', 'Prox bronch tree', 'Proximal trachea', 'Cwall & ribs', 'L Lung', +# 'R Lung', 'Brachial Plexus'] +# roi_name = ['HEART', 'ESOPHAGUS', 'SPINAL_CORD', 'PROX_BRONCH_TREE', 'PROXIMAL_TRACHEA', 'CWALL_RIBS', 'LUNG_LEFT', +# 'LUNG_RIGHT', 'BRACHIAL_PLEXUS'] +# roi_series = ['Heart', 'Esophagus', 'Lung_L', 'Lung_R','SpinalCord'] +# roi_name = ['HEART', 'ESOPHAGUS', 'LUNG_LEFT','LUNG_RIGHT', 'SPINAL_CORD'] + +# roi_series = ['HEART', 'PTV', 'LUNG_IPSI', 'LUNG_CNTR', 'SPINAL_CORD', 'ESOPHAGUS'] +# roi_name = ['HEART', 'PTV', 'LUNG_IPSI', 'LUNG_CNTR', 'SPINAL_CORD', 'ESOPHAGUS'] +roi_name = 'gtv' + +path = config['DATA']['NiiFolder'] +sPatient = SubjectList + +se = ndimage.generate_binary_structure(3, 3) + +for i in range(0, len(SubjectList), 1): + subjectid = sPatient.loc[i, 'subjectid'] + subject_label = sPatient.loc[i,'subject_label'] + CTPath = SubjectList[SubjectList.subjectid == subjectid]['CT_Path'] + CTArray, meta = LoadImage()(CTPath) + ct = EnsureChannelFirst()(CTArray) + ct = Spacing(pixdim=(1, 1, 3))(ct) + + names_generator = itk.GDCMSeriesFileNames.New() + names_generator.SetUseSeriesDetails(True) + names_generator.AddSeriesRestriction("0008|0021") # Series Date + names_generator.SetDirectory(list(CTPath)[0]) + series_uid = names_generator.GetSeriesUIDs() + print('No.{}:'.format(i) + str(subject_label)) + + if len(series_uid) > 1: + print(series_uid) + else: + ct_array = ct.array.squeeze() + # First define the ROI based on target + RSPath = SubjectList[SubjectList.subjectid == subjectid]['Structs_Path'] + # FixRTSS(RSPath[0], list(CTPath)[0]) + RS = RTStructBuilder.create_from(dicom_series_path=list(CTPath)[0], rt_struct_path=list(RSPath)[0]) + #%% + roi_names = RS.get_roi_names() + strList = [x.lower() for x in roi_names] + ni_img = nib.Nifti1Image(np.double(ct_array), affine=ct.affine) + spath = Path(path, subject_label) + + if not os.path.isdir(spath): + os.mkdir(spath) + nib.save(ni_img, Path(spath, 'ct.nii.gz')) + + r1 = [r for r in strList if roi_name in r] + + mask = np.zeros_like(CTArray) + + for j in range(len(r1)): + roi = r1[j] + index = strList.index(roi.lower()) + mask_img = RS.get_roi_mask_by_name(roi_names[index]) + mask_img = np.rot90(mask_img) + mask_img = np.flip(mask_img, 0) + mask = mask + mask_img + + mask = ndimage.binary_dilation(mask, structure=se, iterations=3) + mask = MetaTensor(mask.copy(), meta=meta) + mask = EnsureChannelFirst()(mask) + mask = Spacing(pixdim=(1, 1, 3))(mask) + mask_array = mask.array.squeeze() + ni_mask = nib.Nifti1Image(mask_array.astype('int'), affine=mask.affine, dtype='uint8') + nib.save(ni_mask, Path(spath, 'AI_target.nii.gz')) + + + diff --git a/Count.py b/Count.py new file mode 100644 index 0000000..fb24ac7 --- /dev/null +++ b/Count.py @@ -0,0 +1,68 @@ +import sys +import toml +import glob +import nibabel as nib +import itertools +import numpy as np +from Utils.DicomTools import * +from pathlib import Path +from DataGenerator.DataGenerator import QuerySubjectList, SynchronizeData, QuerySubjectInfo +from Utils.DicomTools import * +import os +from rt_utils import RTStructBuilder +import xnat +from Utils.FixRTSS import * +session = xnat.connect('http://128.16.11.124:8080/xnat', user='admin', password='mortavar1977') +import csv +from monai.transforms import Spacing, LoadImage, EnsureChannelFirst +from monai.data import MetaTensor +import itk +from collections import Counter +config = toml.load(sys.argv[1]) + +session = xnat.connect(config['SERVER']['Address'], user=config['SERVER']['User'],password=config['SERVER']['Password']) +SubjectList = QuerySubjectList(config, session) +SubjectList.dropna(subset=['xnat_subjectdata_field_map_survival_months'], inplace=True) +print(SubjectList) +SynchronizeData(config, SubjectList) + +for key in config['MODALITY'].keys(): + SubjectList[key+'_Path'] = "" +QuerySubjectInfo(config, SubjectList, session) +roi_series = [] + +path = config['DATA']['NiiFolder'] +sPatient = SubjectList +r1 = [x.lower() for x in roi_series] + +for i in range(0, len(SubjectList), 1): + print(i) + subjectid = sPatient.loc[i, 'subjectid'] + subject_label = sPatient.loc[i,'subject_label'] + CTPath = SubjectList[SubjectList.subjectid == subjectid]['CT_Path'] + CTArray, meta = LoadImage()(CTPath) + ct = EnsureChannelFirst()(CTArray) + ct = Spacing(pixdim=(1, 1, 3))(ct) + + names_generator = itk.GDCMSeriesFileNames.New() + names_generator.SetUseSeriesDetails(True) + names_generator.AddSeriesRestriction("0008|0021") # Series Date + names_generator.SetDirectory(list(CTPath)[0]) + series_uid = names_generator.GetSeriesUIDs() + if len(series_uid) > 1: + print(subject_label) + else: + ct_array = ct.array.squeeze() + # First define the ROI based on target + RSPath = SubjectList[SubjectList.subjectid == subjectid]['Structs_Path'] + # FixRTSS(list(RSPath)[0], list(CTPath)[0]) + try: + RS = RTStructBuilder.create_from(dicom_series_path=list(CTPath)[0], rt_struct_path=list(RSPath)[0]) + roi_names = RS.get_roi_names() + strList = [x.lower() for x in roi_names] + roi_series.append(strList) + except: + print('RS error: pid', subject_label) + +letter_counts = Counter(itertools.chain.from_iterable(roi_series)) +print(letter_counts) diff --git a/DataGenerator/DataGenerator.py b/DataGenerator/DataGenerator.py index 2988877..7f85528 100644 --- a/DataGenerator/DataGenerator.py +++ b/DataGenerator/DataGenerator.py @@ -7,7 +7,7 @@ import xnat import matplotlib.pyplot as plt from monai.transforms import ( - LoadImage, EnsureChannelFirstd, ResampleToMatchd, ResizeWithPadOrCropd + LoadImage, EnsureChannelFirstd, ResampleToMatchd, ResizeWithPadOrCropd ) from Utils.DicomTools import * from Utils.XNATXML import XMLCreator @@ -24,302 +24,312 @@ class DataGenerator(torch.utils.data.Dataset): - def __init__(self, SubjectList,config=None, keys=['CT'], transform=None, inference=False, - clinical_cols=None, session=None, **kwargs): - super().__init__() - self.config = config - self.session = session - self.SubjectList = SubjectList - self.keys = keys - self.transform = transform - self.inference = inference - self.clinical_cols = clinical_cols - - def __len__(self): - return int(self.SubjectList.shape[0]) - - def __getitem__(self, i): - - data = {} - meta = {} - subject_id = self.SubjectList.loc[i, 'subjectid'] - slabel = self.SubjectList.loc[i, 'subject_label'] - ## Load CT - if 'CT' in self.keys: - CTPath = self.SubjectList.loc[i, 'CT_Path'] - if self.config['DATA']['Nifty']: - CTPath = Path(CTPath, 'ct.nii.gz') - data['CT'], meta['CT'] = LoadImage()(CTPath) - else: - data['CT'], meta['CT'] = LoadImage()(CTPath) - CTSession = ReadDicom(CTPath) - CTArray = sitk.GetArrayFromImage(CTSession) - if not(CTArray.shape == data['CT'].shape): - CTArray = CTArray.transpose((2, 1, 0)) - CTArray = np.flip(CTArray, axis=2) - mCT = MetaTensor(CTArray.copy(), meta=meta['CT']) - data['CT'] = mCT - - ## Load Dose - if 'Dose' in self.keys: - DosePath = self.SubjectList.loc[i, 'Dose_Path'] - if self.config['DATA']['Nifty']: - DosePath = Path(DosePath, 'dose.nii.gz') - data['Dose'], meta['Dose'] = LoadImage()(DosePath) - data['Dose'] = data['Dose']/67 - if not self.config['DATA']['Nifty']: - data['Dose'] = data['Dose'] * np.double(meta['Dose']['3004|000e'])/67 - - ## Load PET - if 'PET' in self.keys: - PETPath = self.SubjectList.loc[i, 'PET_Path'] - if self.config['DATA']['Nifty']: - PETPath = Path(PETPath, 'dose.nii.gz') - data['PET'], meta['PET'] = LoadImage()(PETPath) - - ## Load Mask - if 'Structs' in self.keys: - RSPath = self.SubjectList.loc[i, 'Structs_Path'] - if self.config['DATA']['Nifty']: - #for roi in self.config['DATA']['Structs']: - # data['Struct_' + roi], meta['Struct_' + roi] = LoadImage()(Path(RSPath,roi+'.nii.gz')) - # dt = distance_transform_edt(data['Struct_' + roi]) - # data['Struct_' + roi] = MetaTensor(dt, meta = meta['CT']) - masks_img = np.zeros_like(data['CT']) - masks_img = get_nii_masks(slabel, masks_img, RSPath, self.config['DATA']['Structs']) - masks_img = MetaTensor(masks_img.copy(), meta=meta['CT']) - data['Structs'] = masks_img - else: - ## mask in multichannel - RS = RTStructBuilder.create_from(dicom_series_path=CTPath, rt_struct_path=RSPath) - #roi_names = RS.get_roi_names() - #for roi in self.config['DATA']['Structs']: - # if roi in roi_names: - # mask_img = RS.get_roi_mask_by_name(roi) - # mask_img = distance_transform_edt(mask_img) - # else: - # message = "No ROI of name " + self.targetROI + " found in RTStruct" - # raise ValueError(message) - # mask_img = np.rot90(mask_img) - # mask_img = np.flip(mask_img, 2) - # mask_img = np.flip(mask_img, 0) - # mask = MetaTensor(mask_img.copy(), meta = meta['CT']) - # data['Struct_' + roi] = mask - - ### masks images - masks_img = np.zeros_like(data['CT']) - masks_img = get_RS_masks(slabel, CTPath, masks_img, RSPath, self.config['DATA']['Structs']) - masks_img = np.rot90(masks_img) - masks_img = np.flip(masks_img, 0) - masks_img = MetaTensor(masks_img.copy(), meta = meta['CT']) - data['Structs'] = masks_img - else: - data['Structs'] = np.ones_like(data['CT']) ## No ROI target defined - - ## Apply transforms on all - if self.transform: data = self.transform(data) - - # mask_imgs = np.zeros_like(CTArray) - # for key in data.keys(): - # if 'Mask' in key: - # mask_imgs = mask_imgs + data[key] - - #for key in data.keys(): - # data[key] = get_masked_img_voxel(data[key], data['Mask']) - # Decide between multi-branch single-channel/multi-channel single-branch - if self.config['DATA']['Multichannel']: - old_keys = list(data.keys()) - data['Image'] = np.concatenate([data[key] for key in data.keys()], axis=0) - for key in old_keys: data.pop(key) - else: - data.pop('Structs') ## No need for mask in single-channel multi-branch - - #data = ResizeWithPadOrCropd(keys=data.keys(), spatial_size=self.config['DATA']['dim'])(data) - - ## Add clinical record at the end - if 'Records' in self.config.keys(): data['Records'] = torch.tensor(self.SubjectList.loc[i, self.clinical_cols], - dtype=torch.float32) - if self.inference: - return data - else: - label = torch.tensor(np.float(self.SubjectList.loc[i, "xnat_subjectdata_field_map_" + self.config['DATA']['target']])) - if self.config['DATA']['threshold'] is not None: label = torch.where( - label > self.config['DATA']['threshold'], 1, 0) - label = torch.as_tensor(label, dtype=torch.float32) - return data, label + def __init__(self, SubjectList, config=None, keys=['CT'], transform=None, inference=False, + clinical_cols=None, session=None, **kwargs): + super().__init__() + self.config = config + self.SubjectList = SubjectList + self.keys = keys + self.transform = transform + self.inference = inference + self.clinical_cols = clinical_cols + + def __len__(self): + return int(self.SubjectList.shape[0]) + + def __getitem__(self, i): + + data = {} + meta = {} + subject_id = self.SubjectList.loc[i, 'subjectid'] + slabel = self.SubjectList.loc[i, 'subject_label'] + ## Load CT + if 'CT' in self.keys: + CTPath = self.SubjectList.loc[i, 'CT_Path'] + if self.config['DATA']['Nifty']: + CTPath = Path(CTPath, 'ct.nii.gz') + data['CT'], meta['CT'] = LoadImage()(CTPath) + else: + data['CT'], meta['CT'] = LoadImage()(CTPath) + CTSession = ReadDicom(CTPath) + CTArray = sitk.GetArrayFromImage(CTSession) + if not (CTArray.shape == data['CT'].shape): + CTArray = CTArray.transpose((2, 1, 0)) + CTArray = np.flip(CTArray, axis=2) + mCT = MetaTensor(CTArray.copy(), meta=meta['CT']) + data['CT'] = mCT + + ## Load Dose + if 'Dose' in self.keys: + DosePath = self.SubjectList.loc[i, 'Dose_Path'] + if self.config['DATA']['Nifty']: + DosePath = Path(DosePath, 'dose.nii.gz') + data['Dose'], meta['Dose'] = LoadImage()(DosePath) + data['Dose'] = data['Dose'] / 67 + if not self.config['DATA']['Nifty']: + data['Dose'] = data['Dose'] * np.double(meta['Dose']['3004|000e']) / 67 + + ## Load PET + if 'PET' in self.keys: + PETPath = self.SubjectList.loc[i, 'PET_Path'] + if self.config['DATA']['Nifty']: + PETPath = Path(PETPath, 'dose.nii.gz') + data['PET'], meta['PET'] = LoadImage()(PETPath) + + ## Load Mask + if 'Structs' in self.keys: + RSPath = self.SubjectList.loc[i, 'Structs_Path'] + if self.config['DATA']['Nifty']: + data['Structs'], meta['Structs'] = LoadImage()(Path(RSPath, 'masks.nii.gz')) + + # for roi in self.config['DATA']['Structs']: + # data['Struct_' + roi], meta['Struct_' + roi] = LoadImage()(Path(RSPath,roi+'.nii.gz')) + # dt = distance_transform_edt(data['Struct_' + roi]) + # data['Struct_' + roi] = MetaTensor(dt, meta = meta['CT']) + + # masks_img = np.zeros_like(data['CT']) + # masks_img = get_nii_masks(slabel, masks_img, RSPath, self.config['DATA']['Structs']) + # masks_img = MetaTensor(masks_img.copy(), meta=meta['CT']) + # data['Structs'] = masks_img + else: + ## mask in multichannels + RS = RTStructBuilder.create_from(dicom_series_path=CTPath, rt_struct_path=RSPath) + # roi_names = RS.get_roi_names() + # for roi in self.config['DATA']['Structs']: + # if roi in roi_names: + # mask_img = RS.get_roi_mask_by_name(roi) + # mask_img = distance_transform_edt(mask_img) + # else: + # message = "No ROI of name " + self.targetROI + " found in RTStruct" + # raise ValueError(message) + # mask_img = np.rot90(mask_img) + # mask_img = np.flip(mask_img, 2) + # mask_img = np.flip(mask_img, 0) + # mask = MetaTensor(mask_img.copy(), meta = meta['CT']) + # data['Struct_' + roi] = mask + + ### masks images + masks_img = np.zeros_like(data['CT']) + masks_img = get_RS_masks(slabel, CTPath, masks_img, RSPath, self.config['DATA']['Structs']) + masks_img = np.rot90(masks_img) + masks_img = np.flip(masks_img, 0) + masks_img = MetaTensor(masks_img.copy(), meta=meta['CT']) + data['Structs'] = masks_img + else: + data['Structs'] = np.ones_like(data['CT']) ## No ROI target defined + + ## Apply transforms on all + if self.transform: data = self.transform(data) + + # mask_imgs = np.zeros_like(CTArray) + # for key in data.keys(): + # if 'Mask' in key: + # mask_imgs = mask_imgs + data[key] + + # for key in data.keys(): + # data[key] = get_masked_img_voxel(data[key], data['Mask']) + # Decide between multi-branch single-channel/multi-channel single-branch + if self.config['DATA']['Multichannel']: + old_keys = list(data.keys()) + data['Image'] = np.concatenate([data[key] for key in data.keys()], axis=0) + for key in old_keys: data.pop(key) + else: + data.pop('Structs') ## No need for mask in single-channel multi-branch + + # data = ResizeWithPadOrCropd(keys=data.keys(), spatial_size=self.config['DATA']['dim'])(data) + + ## Add clinical record at the end + if 'Records' in self.config.keys(): data['Records'] = torch.tensor(self.SubjectList.loc[i, self.clinical_cols], + dtype=torch.float32) + if self.inference: + return data + else: + label = torch.tensor( + np.float(self.SubjectList.loc[i, "xnat_subjectdata_field_map_" + self.config['DATA']['target']])) + if self.config['DATA']['threshold'] is not None: label = torch.where( + label > self.config['DATA']['threshold'], 1, 0) + label = torch.as_tensor(label, dtype=torch.float32) + return data, label ### DataLoader class DataModule(LightningDataModule): - def __init__(self, SubjectList, config=None, train_transform=None, val_transform=None, train_size=0.7, - val_size=0.2, test_size=0.1, num_workers=10, **kwargs): - super().__init__() - self.batch_size = config['MODEL']['batch_size'] - self.num_workers = num_workers - data_trans = class_stratify(SubjectList, config) - ## Split Test with fixed seed - train_val_list, test_list = train_test_split(SubjectList, test_size=0.15, random_state=42, stratify=data_trans) + def __init__(self, SubjectList, config=None, train_transform=None, val_transform=None, train_size=0.85, num_workers=10, **kwargs): + super().__init__() + self.batch_size = config['MODEL']['batch_size'] + self.num_workers = num_workers + data_trans = class_stratify(SubjectList, config) + ## Split Test with fixed seed + train_val_list, test_list = train_test_split(SubjectList, train_size=train_size, random_state=75, + stratify=data_trans) - data_trans = class_stratify(train_val_list, config) - ## Split train-val with random seed - train_list, val_list = train_test_split(train_val_list, test_size=0.15, random_state=np.random.randint(10000), - stratify=data_trans) + data_trans = class_stratify(train_val_list, config) + ## Split train-val with random seed + train_list, val_list = train_test_split(train_val_list, train_size=train_size, + random_state=np.random.randint(10000), + stratify=data_trans) - train_list = train_list.reset_index(drop=True) - val_list = val_list.reset_index(drop=True) - test_list = test_list.reset_index(drop=True) + train_list = train_list.reset_index(drop=True) + val_list = val_list.reset_index(drop=True) + test_list = test_list.reset_index(drop=True) - self.train_data = DataGenerator(train_list, config=config, transform=train_transform, **kwargs) - self.val_data = DataGenerator(val_list,config=config, transform=val_transform, **kwargs) - self.test_data = DataGenerator(test_list, config=config, transform=val_transform, **kwargs) + self.train_data = DataGenerator(train_list, config=config, transform=train_transform, **kwargs) + self.val_data = DataGenerator(val_list, config=config, transform=val_transform, **kwargs) + self.test_data = DataGenerator(test_list, config=config, transform=val_transform, **kwargs) - def train_dataloader(self): return DataLoader(self.train_data, batch_size=self.batch_size, - num_workers=self.num_workers, pin_memory=True, drop_last=True, - shuffle=True) + def train_dataloader(self): return DataLoader(self.train_data, batch_size=self.batch_size, + num_workers=self.num_workers, pin_memory=True, drop_last=True, + shuffle=True) - def val_dataloader(self): return DataLoader(self.val_data, batch_size=self.batch_size, - num_workers=self.num_workers, pin_memory=True, drop_last=True, - shuffle=False) + def val_dataloader(self): return DataLoader(self.val_data, batch_size=self.batch_size, + num_workers=self.num_workers, pin_memory=True, drop_last=True, + shuffle=False) - def test_dataloader(self): return DataLoader(self.test_data, batch_size=self.batch_size, - num_workers=self.num_workers, pin_memory=True, drop_last=True) + def test_dataloader(self): return DataLoader(self.test_data, batch_size=self.batch_size, + num_workers=self.num_workers, pin_memory=True, drop_last=True) def QuerySubjectList(config, session): - root_element = "xnat:subjectData" - XML = XMLCreator(root_element) # , search_field, search_where) - print("Querying from Server") - ## Target - XML.Add_search_field( - {"element_name": "xnat:subjectData", "field_ID": "XNAT_SUBJECTDATA_FIELD_MAP=" + str(config['DATA']['target']), - "sequence": "1", "type": "int"}) - ## Label - XML.Add_search_field( - {"element_name": "xnat:subjectData", "field_ID": "SUBJECT_LABEL", "sequence": "1", "type": "string"}) - - if 'Records' in config.keys(): - feats_list = [item for sublist in config['Records'].values() for item in sublist] - for value in feats_list: - dict_temp = {"element_name": "xnat:subjectData", "field_ID": "XNAT_SUBJECTDATA_FIELD_MAP=" + str(value), - "sequence": "1", "type": "int"} - XML.Add_search_field(dict_temp) - - ## Where Condition - templist = [] - for value in config['SERVER']['Projects']: - templist.append({"schema_field": "xnat:subjectData.PROJECT", "comparison_type": "=", "value": str(value)}) - if (len(templist)): XML.Add_search_where(templist, "OR") - - templist = [] - for key, value in config['CRITERIA'].items(): - templist.append({"schema_field": "xnat:subjectData.XNAT_SUBJECTDATA_FIELD_MAP=" + key, "comparison_type": "=", - "value": str(value)}) - if (len(templist)): XML.Add_search_where(templist, "AND") ## if any items in here - - templist = [] - for key, value in config['MODALITY'].items(): - templist.append( - {"schema_field": "xnat:ctSessionData.SCAN_COUNT_TYPE=" + key, "comparison_type": ">=", "value": str(value)}) - if (len(templist)): XML.Add_search_where(templist, "AND") ## if any items in here - - templist = [] - if (config['FILTER']): - for value in config['FILTER']['patient_id']: - dict_temp = {"schema_field": "xnat:subjectData.SUBJECT_LABEL", "comparison_type": "!=", "value": str(value)} - - templist.append(dict_temp) - if (len(templist)): XML.Add_search_where(templist, "AND") - - xmlstr = XML.ConstructTree() - response = session.post(config['SERVER']['Address'] + '/data/search/', data=xmlstr, format='csv') - SubjectList = pd.read_csv(StringIO(response.text), dtype=str) - # print('Query: ', SubjectList) - return SubjectList + root_element = "xnat:subjectData" + XML = XMLCreator(root_element) # , search_field, search_where) + print("Querying from Server") + ## Target + XML.Add_search_field( + {"element_name": "xnat:subjectData", "field_ID": "XNAT_SUBJECTDATA_FIELD_MAP=" + str(config['DATA']['target']), + "sequence": "1", "type": "int"}) + ## Label + XML.Add_search_field( + {"element_name": "xnat:subjectData", "field_ID": "SUBJECT_LABEL", "sequence": "1", "type": "string"}) + + if 'Records' in config.keys(): + feats_list = [item for sublist in config['Records'].values() for item in sublist] + for value in feats_list: + dict_temp = {"element_name": "xnat:subjectData", "field_ID": "XNAT_SUBJECTDATA_FIELD_MAP=" + str(value), + "sequence": "1", "type": "int"} + XML.Add_search_field(dict_temp) + + ## Where Condition + templist = [] + for value in config['SERVER']['Projects']: + templist.append({"schema_field": "xnat:subjectData.PROJECT", "comparison_type": "=", "value": str(value)}) + if (len(templist)): XML.Add_search_where(templist, "OR") + + templist = [] + for key, value in config['CRITERIA'].items(): + templist.append({"schema_field": "xnat:subjectData.XNAT_SUBJECTDATA_FIELD_MAP=" + key, "comparison_type": "=", + "value": str(value)}) + if (len(templist)): XML.Add_search_where(templist, "AND") ## if any items in here + + templist = [] + for key, value in config['MODALITY'].items(): + templist.append( + {"schema_field": "xnat:ctSessionData.SCAN_COUNT_TYPE=" + key, "comparison_type": ">=", "value": str(value)}) + if (len(templist)): XML.Add_search_where(templist, "AND") ## if any items in here + + templist = [] + if (config['FILTER']): + for value in config['FILTER']['patient_id']: + dict_temp = {"schema_field": "xnat:subjectData.SUBJECT_LABEL", "comparison_type": "!=", "value": str(value)} + + templist.append(dict_temp) + if (len(templist)): XML.Add_search_where(templist, "AND") + + xmlstr = XML.ConstructTree() + response = session.post(config['SERVER']['Address'] + '/data/search/', data=xmlstr, format='csv') + SubjectList = pd.read_csv(StringIO(response.text), dtype=str) + # print('Query: ', SubjectList) + return SubjectList def SynchronizeData(config, SubjectList): - session = xnat.connect(config['SERVER']['Address'], user=config['SERVER']['User'], - password=config['SERVER']['Password']) - for subjectlabel, subjectid in zip(SubjectList['subject_label'], SubjectList['subjectid']): - if (not Path(config['DATA']['DataFolder'], subjectlabel).is_dir()): - xnatsubject = session.create_object('/data/subjects/' + subjectid) - print("Synchronizing ", subjectid, subjectlabel) - xnatsubject.download_dir(config['DATA']['DataFolder']) ## Download data + session = xnat.connect(config['SERVER']['Address'], user=config['SERVER']['User'], + password=config['SERVER']['Password']) + for subjectlabel, subjectid in zip(SubjectList['subject_label'], SubjectList['subjectid']): + if (not Path(config['DATA']['DataFolder'], subjectlabel).is_dir()): + xnatsubject = session.create_object('/data/subjects/' + subjectid) + print("Synchronizing ", subjectid, subjectlabel) + xnatsubject.download_dir(config['DATA']['DataFolder']) ## Download data def get_subject_info(config, session, subjectid): - r = session.get(config['SERVER']['Address'] + '/data/subjects/' + subjectid, format='xml') - data = xmltodict.parse(r.text, force_list=True) - return data + r = session.get(config['SERVER']['Address'] + '/data/subjects/' + subjectid, format='xml') + data = xmltodict.parse(r.text, force_list=True) + return data def QuerySubjectInfo(config, SubjectList, session): - if config['DATA']['Nifty']: - for i in range(len(SubjectList)): - subject_label = SubjectList.loc[i,'subject_label'] - for key in config['MODALITY'].keys(): - SubjectList.loc[i, key + '_Path'] = Path(config['DATA']['DataFolder'], subject_label) - else: - with ThreadPoolExecutor(max_workers=10) as executor: - future_to_url = {executor.submit(get_subject_info, config, session, subjectid) for subjectid in - SubjectList['subjectid']} - executor.shutdown(wait=True) - for future in concurrent.futures.as_completed(future_to_url): - subjectdata = future.result() - subjectid = subjectdata["xnat:Subject"][0]["@ID"] - for key in config['MODALITY'].keys(): - path = GeneratePath(subjectdata, Modality=key, config=config) - if key == 'CT': - SubjectList.loc[SubjectList.subjectid == subjectid, key + '_Path'] = path - else: - spath = glob.glob(path + '/*dcm') - SubjectList.loc[SubjectList.subjectid == subjectid, key + '_Path'] = spath[0] + if config['DATA']['Nifty']: + for i in range(len(SubjectList)): + subject_label = SubjectList.loc[i, 'subject_label'] + for key in config['MODALITY'].keys(): + #if key == 'Structs': + # SubjectList.loc[i, key + '_Path'] = Path(config['DATA']['DataFolder'], subject_label, 'struct_TS') + #else: + SubjectList.loc[i, key + '_Path'] = Path(config['DATA']['DataFolder'], subject_label) + else: + with ThreadPoolExecutor(max_workers=10) as executor: + future_to_url = {executor.submit(get_subject_info, config, session, subjectid) for subjectid in + SubjectList['subjectid']} + executor.shutdown(wait=True) + for future in concurrent.futures.as_completed(future_to_url): + subjectdata = future.result() + subjectid = subjectdata["xnat:Subject"][0]["@ID"] + for key in config['MODALITY'].keys(): + path = GeneratePath(subjectdata, Modality=key, config=config) + if key == 'CT': + SubjectList.loc[SubjectList.subjectid == subjectid, key + '_Path'] = path + else: + spath = glob.glob(path + '/*dcm') + SubjectList.loc[SubjectList.subjectid == subjectid, key + '_Path'] = spath[0] + def GeneratePath(subjectdata, Modality, config): - subject = subjectdata['xnat:Subject'][0] - subject_label = subject['@label'] - experiments = subject['xnat:experiments'][0]['xnat:experiment'] - - ## Won't work with many experiments yet - for experiment in experiments: - experiment_label = experiment['@label'] - scans = experiment['xnat:scans'][0]['xnat:scan'] - for scan in scans: - if (scan['@type'] in Modality): - scan_label = scan['@ID'] + '-' + scan['@type'] - resources_label = scan['xnat:file'][0]['@label'] - if resources_label == 'SNAPSHOTS': - resources_label = scan['xnat:file'][1]['@label'] - path = os.path.join(config['DATA']['DataFolder'], subject_label, experiment_label, 'scans', - scan_label, 'resources', resources_label, 'files') - return path + subject = subjectdata['xnat:Subject'][0] + subject_label = subject['@label'] + experiments = subject['xnat:experiments'][0]['xnat:experiment'] + + ## Won't work with many experiments yet + for experiment in experiments: + experiment_label = experiment['@label'] + scans = experiment['xnat:scans'][0]['xnat:scan'] + for scan in scans: + if (scan['@type'] in Modality): + scan_label = scan['@ID'] + '-' + scan['@type'] + resources_label = scan['xnat:file'][0]['@label'] + if resources_label == 'SNAPSHOTS': + resources_label = scan['xnat:file'][1]['@label'] + path = os.path.join(config['DATA']['DataFolder'], subject_label, experiment_label, 'scans', + scan_label, 'resources', resources_label, 'files') + return path + def LoadClinicalData(config, PatientList): - category_cols = [] - numerical_cols = [] - for col in config['Records']['category_feat']: - category_cols.append('xnat_subjectdata_field_map_' + col) - - for row in config['Records']['numerical_feat']: - numerical_cols.append('xnat_subjectdata_field_map_' + row) - - target = config['DATA']['target'] - - ct = ColumnTransformer( - [("CatTrans", OneHotEncoder(), category_cols), - ("NumTrans", MinMaxScaler(), numerical_cols), ]) - - X = PatientList.loc[:, category_cols + numerical_cols] - yc = X[category_cols].astype('float32') - X[category_cols] = yc.fillna(yc.mean().astype('int')) - yn = X[numerical_cols].astype('float32') - X[numerical_cols] = yn.fillna(yn.mean()) #X.loc[:, numerical_cols] = yn.fillna(yn.mean()) - X_trans = ct.fit_transform(X) - if not isinstance(X_trans, (np.ndarray, np.generic)): X_trans = X_trans.toarray() - - df_trans = pd.DataFrame(X_trans, index=X.index, columns=ct.get_feature_names_out()) - clinical_col = list(df_trans.columns) - df_trans['xnat_subjectdata_field_map_' + target] = PatientList.loc[:, 'xnat_subjectdata_field_map_' + target] - df_trans['subject_label'] = PatientList.loc[:, 'subject_label'] - df_trans['subjectid'] = PatientList.loc[:, 'subjectid'] - return df_trans, clinical_col + category_cols = [] + numerical_cols = [] + for col in config['Records']['category_feat']: + category_cols.append('xnat_subjectdata_field_map_' + col) + + for row in config['Records']['numerical_feat']: + numerical_cols.append('xnat_subjectdata_field_map_' + row) + + target = config['DATA']['target'] + + ct = ColumnTransformer( + [("CatTrans", OneHotEncoder(), category_cols), + ("NumTrans", MinMaxScaler(), numerical_cols), ]) + + X = PatientList.loc[:, category_cols + numerical_cols] + yc = X[category_cols].astype('float32') + X[category_cols] = yc.fillna(yc.mean().astype('int')) + yn = X[numerical_cols].astype('float32') + X[numerical_cols] = yn.fillna(yn.mean()) # X.loc[:, numerical_cols] = yn.fillna(yn.mean()) + X_trans = ct.fit_transform(X) + if not isinstance(X_trans, (np.ndarray, np.generic)): X_trans = X_trans.toarray() + + df_trans = pd.DataFrame(X_trans, index=X.index, columns=ct.get_feature_names_out()) + clinical_col = list(df_trans.columns) + df_trans['xnat_subjectdata_field_map_' + target] = PatientList.loc[:, 'xnat_subjectdata_field_map_' + target] + df_trans['subject_label'] = PatientList.loc[:, 'subject_label'] + df_trans['subjectid'] = PatientList.loc[:, 'subjectid'] + return df_trans, clinical_col + diff --git a/DefaultConfiguration.ini b/DefaultConfiguration.ini deleted file mode 100644 index 7f9a696..0000000 --- a/DefaultConfiguration.ini +++ /dev/null @@ -1,65 +0,0 @@ -[MODEL] -BaseModel = "AutoEncoder" -Model_Save_Path = "./" -batch_size = 2 -RANDOM_SEED = 42 -Loss_Function = "CrossEntropyLoss"#"MSELoss" -Activation = "Sigmoid" -Max_Epochs = 2 -Precision = 32 -Backbone = "densenet121" -Pretrained = true -Drop_Rate = 0.1 -wf = 4 -depth = 6 -activation = "Identity" -inference = true -emb_size = 1000 - -[MODEL_PARAMETERS] -spatial_dims = 3 -block_config = [1, 2, 4, 1] -in_channels = 1 -out_channels = 1 - -[Dose_MODEL_PARAMETERS] -in_channels = 1 -wf = 3 -depth = 3 - -[MODALITY] -CT = 1 -Dose = 1 - -[SERVER] -Address = 'http://128.16.11.124:8080/xnat' -Projects = ["RTOG_0617"] -User = "***" -Password = "***" - -[DATA] -DataFolder = "./Data" -n_per_sample = 5000 -n_classes = 2 -n_channel = 3 -sub_patch_size = 16 -dim = [100,256, 256] -vis = [0] -train_size = 0.7 -val_size = 0.3 -target = "survival_months" -threshold = 20 -#Mask = '' -Multichannel = false - -[CRITERIA] -#survival_status = 1 -#arm = 1 - -[CHECKPOINT] -monitor = "val_loss" #"val_acc_epoch" -mode = "max" -matrix = ['ROC', 'Specificity'] - -[FILTER] -#patient_id = ['0617-444138','0617-449451'] diff --git a/FormMasks.py b/FormMasks.py new file mode 100644 index 0000000..71f5c43 --- /dev/null +++ b/FormMasks.py @@ -0,0 +1,70 @@ +import torch +import torchvision +from torch import nn +from pytorch_lightning.callbacks import ModelCheckpoint, EarlyStopping +from pytorch_lightning.strategies import DDPStrategy +from pytorch_lightning import LightningDataModule, LightningModule, Trainer, seed_everything +import sys, os +#import torchio as tio +import monai +torch.cuda.empty_cache() +## Module - Dataloaders +from DataGenerator.DataGenerator import * +from Models.Classifier import Classifier +from Models.Linear import Linear +from Models.MixModel import MixModel +from monai.transforms import EnsureChannelFirstd, ScaleIntensityd, ResampleToMatchd +## Main +from sklearn.preprocessing import StandardScaler, OneHotEncoder +import toml +from Utils.GenerateSmoothLabel import get_smoothed_label_distribution, get_module +from Utils.PredictionReports import PredictionReports +from pathlib import Path +#import torchio as tio +from torchmetrics import ConfusionMatrix +import torchmetrics +import nibabel as nib + +config = toml.load(sys.argv[1]) +## First Connect to XNAT +session = xnat.connect(config['SERVER']['Address'], user=config['SERVER']['User'],password=config['SERVER']['Password']) + + +SubjectList = QuerySubjectList(config, session) +SynchronizeData(config, SubjectList) +## GeneratePath +for key in config['MODALITY'].keys(): + SubjectList[key+'_Path'] = "" +QuerySubjectInfo(config, SubjectList, session) + +path = '/home/dgs1/data/Nifty_Data/RTOG0617/' + +for i in range(len(SubjectList)): + print(i) + sub = SubjectList.iloc[i] + print(sub) + patient_label = sub['subject_label'] + RSPath = sub['Structs_Path'] + for idx, roi in enumerate(config['DATA']['Structs']): + try: + data, meta = LoadImage()(Path(RSPath, roi + '.nii.gz')) + print(data.shape) + if idx == 0: + masks_img = np.zeros_like(data) + except: + raise ValueError(patient_label + " has no ROI of name " + roi + " found in RTStruct") + masks_img = BitSet(masks_img, idx * np.ones_like(masks_img), data) + + ni_img = nib.Nifti1Image(np.double(masks_img), affine=meta['affine']) + spath = Path(path, patient_label, 'struct_TS') + nib.save(ni_img, Path(spath, 'maskswc.nii.gz')) + + + +def BitSet(n, p, b): + p = p.astype(int) + n = n.astype(int) + b = b.astype(int) + mask = 1 << p + bm = b << p + return (n & ~mask) | bm diff --git a/Models/Classifier.py b/Models/Classifier.py index c341b87..0f254a7 100644 --- a/Models/Classifier.py +++ b/Models/Classifier.py @@ -14,7 +14,6 @@ def __init__(self, config, module_str): model = config['MODEL']['Backbone'] parameters = config['MODEL_PARAMETERS'] - # only use network for features if model == 'torchvision': model_name = config['MODEL'][module_str + '_model_name'] @@ -24,23 +23,25 @@ def __init__(self, config, module_str): model_str = 'nets.' + model + '(**parameters)' self.backbone = eval(model_str) - layers = list(self.backbone.children())[:-1] - self.model = nn.Sequential(*layers) - - self.flatten = nn.Sequential( - # nn.Dropout(0.3), - # nn.AdaptiveAvgPool3d(output_size=(4, 4, 4)), - nn.Dropout(0.3), - nn.AdaptiveAvgPool3d(output_size=(1, 1, 1)), - nn.Flatten(), - ) - self.model.apply(self.weights_init) + #layers = list(self.backbone.children())[:-1] + # self.model = nn.Sequential(*layers) + + # self.flatten = nn.Sequential( + # # nn.Dropout(0.3), + # # nn.AdaptiveAvgPool3d(output_size=(4, 4, 4)), + # nn.Dropout(0.3), + # nn.AdaptiveAvgPool3d(output_size=(1, 1, 1)), + # nn.Flatten(), + # ) + # self.model.apply(self.weights_init) + self.out_feat = config['MODEL_PARAMETERS']['out_channels'] self.accuracy = torchmetrics.AUROC(task="binary") self.loss_fcn = torch.nn.BCEWithLogitsLoss() def forward(self, x): - features = self.model(x) - return self.flatten(features) + return self.backbone(x) + # features = self.model(x) + # return self.flatten(features) def weights_init(self, m): if isinstance(m, nn.Conv3d) or isinstance(m, nn.Linear): diff --git a/Models/Linear.py b/Models/Linear.py index 21eb750..0cf2600 100644 --- a/Models/Linear.py +++ b/Models/Linear.py @@ -17,16 +17,17 @@ ## Model class Linear(pl.LightningModule): - def __init__(self): + def __init__(self, out_feat=42, in_feat=58): super().__init__() + self.loss_fcn = nn.CrossEntropyLoss() + self.out_feat = out_feat + self.model = nn.Sequential( - nn.Linear(51, 42), + nn.Linear(in_feat, out_feat), nn.Dropout(0.3), - nn.LayerNorm(42), + nn.LayerNorm(out_feat), nn.ReLU(), - ) - self.loss_fcn = nn.CrossEntropyLoss() - + ) def forward(self, x): return self.model(x.float()) diff --git a/Models/MixModel.py b/Models/MixModel.py index 41a3eca..c8d55c5 100644 --- a/Models/MixModel.py +++ b/Models/MixModel.py @@ -1,5 +1,6 @@ import matplotlib.pyplot as plt import torch +import numpy as np import copy from torch import nn from pytorch_lightning import LightningDataModule, LightningModule, Trainer, seed_everything @@ -11,18 +12,18 @@ class MixModel(LightningModule): def __init__(self, module_dict, config, loss_fcn=torch.nn.BCEWithLogitsLoss()): super().__init__() self.module_dict = module_dict + out_feat = np.sum([model.out_feat for model in module_dict.values()]) self.config = config - self.loss_fcn = getattr(torch.nn, self.config["MODEL"]["Loss_Function"])(pos_weight=torch.tensor(1.21)) + self.loss_fcn = getattr(torch.nn, self.config["MODEL"]["Loss_Function"])(pos_weight=torch.tensor(1.31)) self.activation = getattr(torch.nn, self.config["MODEL"]["Activation"])() self.classifier = nn.Sequential( - nn.Linear(198, 120), + nn.Linear(out_feat, 120), nn.Dropout(0.3), nn.Linear(120, 40), nn.Dropout(0.3), nn.Linear(40, config['DATA']['n_classes']), - #self.activation + self.activation ) - self.classifier.apply(self.weights_init) def forward(self, data_dict): features = torch.cat([self.module_dict[key](data_dict[key]) for key in self.module_dict.keys()], dim=1) @@ -49,6 +50,7 @@ def training_epoch_end(self, step_outputs): self.logger.report_epoch(prediction, labels, step_outputs,self.current_epoch, 'train_epoch_') def validation_step(self, batch, batch_idx): + print('val step') out = {} data_dict, label = batch prediction = self.forward(data_dict) diff --git a/SegCT.py b/SegCT.py new file mode 100644 index 0000000..7449597 --- /dev/null +++ b/SegCT.py @@ -0,0 +1,24 @@ +import os +from pydicom import dcmread +from pathlib import Path +import shutil +from dcmrtstruct2nii import dcmrtstruct2nii, list_rt_structs + +path = '/home/dgs1/data/IDEAL/IDEAL/' +subjects = os.listdir(path) +for i, sub in enumerate(subjects): + print(i) + subpath = Path(path, sub) + if not str(subpath).endswith('.DS_Store'): + plan_dir = Path(subpath, 'planning_data') + ## + sub_plan_dir = os.listdir(plan_dir) + + if len(sub_plan_dir) < 8: + sub_plan_dir_planning = Path(plan_dir, 'planning') + ddf = os.listdir(sub_plan_dir_planning) + + sub_structs_dir_planning = Path(plan_dir, 'structures') + struct_dcm = os.listdir(sub_structs_dir_planning)[0] + + diff --git a/Training/#InnerEye.py# b/Training/#InnerEye.py# deleted file mode 100644 index ff8e824..0000000 --- a/Training/#InnerEye.py# +++ /dev/null @@ -1,73 +0,0 @@ -import sys -from DataGenerator.DataGenerator import QuerySubjectList, QuerySubjectInfo, GeneratePath, SynchronizeData -import toml -import nibabel as nib -from Utils.DicomTools import * -from pathlib import Path -import os -from rt_utils import RTStructBuilder -import xnat -session = xnat.connect('http://128.16.11.124:8080/xnat', user='admin', password='mortavar1977') -import csv -from monai.transforms import Spacing, LoadImage, EnsureChannelFirst -from monai.data import MetaTensor -import itk -config = toml.load(sys.argv[1]) -SubjectList = QuerySubjectList(config) -#SynchronizeData(config, SubjectList) -SubjectInfo = QuerySubjectInfo(config, SubjectList) -# roi = ['Heart', 'Oesophagus', 'Spinal Canal', 'Prox bronch tree', 'Proximal trachea', 'Cwall & ribs', 'L Lung', -# 'R Lung', 'Brachial Plexus'] -# roi_name = ['heart', 'oesophagus', 'spinal_canal', 'prox_bronch_tree', 'proximal_trachea', 'cwall_ribs', 'lt_lung', -# 'rt_lung', 'brachial_plexus'] - -roi_series = ['HEART', 'PTV', 'LUNG_IPSI', 'LUNG_CNTR', 'SPINAL_CORD', 'ESOPHAGUS'] -roi_name = ['HEART', 'PTV', 'LUNG_IPSI', 'LUNG_CNTR', 'SPINAL_CORD', 'ESOPHAGUS'] - -path = config['DATA']['NiiFolder'] -sPatient = SubjectList -r1 = [x.lower() for x in roi_series] - -for i in range(0, len(sPatient), 1): - subject_id = sPatient.loc[i, 'subjectid'] - subject_label = sPatient.loc[i,'subject_label'] - - print('No.{}:'.format(i) + subject_label) - - CTPath = GeneratePath(SubjectInfo, config, subject_id, 'CT') - CTArray, meta = LoadImage(reader='PydicomReader')(CTPath) - #ct = EnsureChannelFirst()(CTArray) - #ct = Spacing(pixdim=(1, 1, 3))(ct) - - """ - ct_array = ct.array.squeeze() - # First define the ROI based on target - RSPath = glob.glob(GeneratePath(SubjectInfo, config, subject_id, 'Structs') + '/*dcm') - RS = RTStructBuilder.create_from(dicom_series_path=CTPath, rt_struct_path=RSPath[0]) - # print(RS) - #%% - roi_names = RS.get_roi_names() - strList = [x.lower() for x in roi_names] - ni_img = nib.Nifti1Image(np.double(ct_array), affine=ct.affine) - spath = Path(path, subject_label) - - if not os.path.isdir(spath): - os.mkdir(spath) - nib.save(ni_img, Path(spath, 'ct.nii.gz')) - - for i in range(len(r1)): - roi = r1[i] - if roi in strList: - index = strList.index(roi.lower()) - mask_img = RS.get_roi_mask_by_name(roi_names[index]) - mask_img = np.rot90(mask_img) - mask_img = np.flip(mask_img, 2) - mask_img = np.flip(mask_img, 0) - mask = MetaTensor(mask_img.copy(), meta=meta) - mask = EnsureChannelFirst()(mask) - mask = Spacing(pixdim=(1, 1, 3))(mask) - mask_array = mask.array.squeeze() - ni_mask = nib.Nifti1Image(mask_array.astype('int'), affine=mask.affine, dtype='uint8') - nib.save(ni_mask, Path(spath, roi_name[i].lower() + '.nii.gz')) - """ - diff --git a/Training/Regression.py b/Training/Regression.py index ccdbccb..16c078f 100644 --- a/Training/Regression.py +++ b/Training/Regression.py @@ -5,7 +5,6 @@ from pytorch_lightning.strategies import DDPStrategy from pytorch_lightning import LightningDataModule, LightningModule, Trainer, seed_everything import sys, os -#import torchio as tio import monai torch.cuda.empty_cache() ## Module - Dataloaders @@ -20,13 +19,10 @@ from Utils.GenerateSmoothLabel import get_smoothed_label_distribution, get_module from Utils.PredictionReports import PredictionReports from pathlib import Path -from Utils.DicomTools import img_train_transform, img_val_transform -#import torchio as tio from torchmetrics import ConfusionMatrix import torchmetrics config = toml.load(sys.argv[1]) -total_backbone = config['MODEL']['Backbone'] + '_bitset7_seed_42' ## 2D transform img_keys = list(config['MODALITY'].keys()) ## Multichannel masks @@ -64,6 +60,7 @@ SubjectList = QuerySubjectList(config, session) SynchronizeData(config, SubjectList) SubjectList.dropna(subset=['xnat_subjectdata_field_map_survival_months'], inplace=True) +# SubjectList.dropna(subset=['xnat_subjectdata_field_map_overall_stage'], inplace=True) module_dict = nn.ModuleDict() if config['DATA']['Multichannel']: ## Single-Model Multichannel learning @@ -74,9 +71,8 @@ module_dict[key] = Classifier(config, key) if 'Records' in config.keys(): - module_dict['Records'] = Linear() SubjectList, clinical_cols = LoadClinicalData(config, SubjectList) - + module_dict['Records'] = Linear(in_feat=len(clinical_cols), out_feat=42) else: clinical_cols = None @@ -87,32 +83,35 @@ print(SubjectList) threshold = config['DATA']['threshold'] -ckpt_path = Path('./', total_backbone + '_ckpt') -rd = [2300, 5700, 998, 24, 7865, 9273] -for iter in range(0,5,1): +# ckpt_path = Path('./lightning_logs', total_backbone, 'ckpt') +rd = [53414, 88536, 89901, 62594, 13787, 21781, 18215, 4182, 10695, 61645, 93967, 35446, 41063, 98435, 94558, 67665, + 98831, 76684, 33670, 66239, 24417, 29551, 68018, 52785, 41160, 60264, 75053, 58354, 55180, 58358, 51182, 8260] + +for iter in range(0, 32, 1): seed_everything(rd[iter]) + # seed_everything(42, workers=True) dataloader = DataModule(SubjectList, config=config, keys=config['MODALITY'].keys(), train_transform=train_transform, val_transform=val_transform, clinical_cols=clinical_cols, - inference=False, - session = session) + inference=False) model = MixModel(module_dict, config) model.apply(model.weights_reset) #full_ckpt_path = Path(ckpt_path, 'Iter_'+ str(iter) + '.ckpt') #model.load_state_dict(torch.load(full_ckpt_path)['state_dict']) - filename = total_backbone + filename = 'random_seed_75' logger = PredictionReports(config=config, save_dir='lightning_logs', name=filename) logger.log_text() + logger._version = iter callbacks = [ - ModelCheckpoint(dirpath=ckpt_path, + ModelCheckpoint(dirpath=Path(logger.log_dir, 'ckpt'), monitor='val_loss', filename='Iter_' + str(iter), - save_top_k=1, + save_top_k=5, mode='min'), # EarlyStopping(monitor='val_loss', # check_finite=True), @@ -125,8 +124,16 @@ strategy=DDPStrategy(find_unused_parameters=True), max_epochs=30, logger=logger, - callbacks=callbacks + callbacks=callbacks, ) #model = torch.compile(model) trainer.fit(model, dataloader) - torch.save({'state_dict': model.state_dict(),}, Path('ckpt_test_bitset7_r42', 'Iter_' + str(iter) + '.ckpt')) + torch.save({'state_dict': model.state_dict(),}, Path(logger.log_dir, 'Iter_' + str(iter) + '.ckpt')) + +with open(logger.root_dir + "/Config.ini", "w+") as toml_file: + toml.dump(config, toml_file) + toml_file.write("Train transform:\n") + toml_file.write(str(train_transform)) + toml_file.write("Val/Test transform:\n") + toml_file.write(str(val_transform)) + diff --git a/Training/ShapAnalysis.py b/Training/ShapAnalysis.py new file mode 100644 index 0000000..09ffa33 --- /dev/null +++ b/Training/ShapAnalysis.py @@ -0,0 +1,125 @@ +import torch +import torchvision +from torch import nn +import sys, os +import monai + +torch.cuda.empty_cache() +## Module - Dataloaders +from DataGenerator.DataGenerator import * +from Models.Classifier import Classifier +from Models.Linear import Linear +from Models.MixModel import MixModel +from monai.transforms import EnsureChannelFirstd, ScaleIntensityd, ResampleToMatchd +## Main +import toml +from pathlib import Path +from torchmetrics import ConfusionMatrix +import torchmetrics +#import shap + +config = toml.load(sys.argv[1]) +## 2D transform +img_keys = list(config['MODALITY'].keys()) +# img_keys.remove('Structs') +# if 'Structs' in config['DATA'].keys(): +# for roi in config['DATA']['Structs']: +# img_keys.append('Struct_' + roi) + +train_transform = torchvision.transforms.Compose([ + EnsureChannelFirstd(keys=img_keys), + # ResampleToMatchd(list(set(img_keys).difference(set(['CT']))), key_dst='CT'), + monai.transforms.ScaleIntensityd(keys=img_keys), + # monai.transforms.ResizeWithPadOrCropd(keys=img_keys, spatial_size=config['DATA']['dim']), + monai.transforms.Resized(keys=img_keys, spatial_size=config['DATA']['dim']), + monai.transforms.RandAffined(keys=img_keys), + monai.transforms.RandHistogramShiftd(keys=img_keys), + monai.transforms.RandAdjustContrastd(keys=img_keys), + monai.transforms.RandGaussianNoised(keys=img_keys), + +]) + +val_transform = torchvision.transforms.Compose([ + EnsureChannelFirstd(keys=img_keys), + # ResampleToMatchd(list(set(img_keys).difference(set(['CT']))), key_dst='CT'), + monai.transforms.ScaleIntensityd(img_keys), + # monai.transforms.ResizeWithPadOrCropd(img_keys, spatial_size=config['DATA']['dim']), + monai.transforms.Resized(keys=img_keys, spatial_size=config['DATA']['dim']), +]) + +## First Connect to XNAT +session = xnat.connect(config['SERVER']['Address'], user=config['SERVER']['User'], + password=config['SERVER']['Password']) + +SubjectList = QuerySubjectList(config, session) +SynchronizeData(config, SubjectList) +SubjectList.dropna(subset=['xnat_subjectdata_field_map_survival_months'], inplace=True) + +module_dict = nn.ModuleDict() +if config['DATA']['Multichannel']: ## Single-Model Multichannel learning + if config['MODALITY'].keys(): + module_dict['Image'] = Classifier(config, 'Image') +else: + for key in config['MODALITY'].keys(): # Multi-Model Single Channel learning + module_dict[key] = Classifier(config, key) + +if 'Records' in config.keys(): + SubjectList, clinical_cols = LoadClinicalData(config, SubjectList) + module_dict['Records'] = Linear(in_feat=len(clinical_cols), out_feat=42) +else: + clinical_cols = None + +## GeneratePath +for key in config['MODALITY'].keys(): + SubjectList[key + '_Path'] = "" +QuerySubjectInfo(config, SubjectList, session) + +threshold = config['DATA']['threshold'] +roc_list = [] +sp_list = [] +sensi_list = [] +acc_list = [] +pre_list = [] + +tprs = [] +roc = torchmetrics.ROC() +auroc = torchmetrics.AUROC() +fig = plt.figure() +base_fpr = np.linspace(0, 1, 39) +cm = ConfusionMatrix(num_classes=2) +prediction_labels_full_list = [] +bidx = [21, 25, 0, 4, 6] + +for it in range(0, 5, 1): + iter = bidx[it] + # seed_everything(4200) + dataloader = DataModule(SubjectList, + config=config, + keys=config['MODALITY'].keys(), + train_transform=train_transform, + val_transform=val_transform, + clinical_cols=clinical_cols, + inference=False, + train_size=0.85) + + model = MixModel(module_dict, config) + filename = 'lightning_logs/random_seed_75_Seg/version_' + str(iter) + full_ckpt_path = Path(filename, 'Iter_' + str(iter) + '.ckpt') + model.load_state_dict(torch.load(full_ckpt_path)['state_dict']) + + image = torch.cat([out for i, out in enumerate(dataloader.train_dataloader())], dim=0) + label = torch.cat([out[1] for i, out in enumerate(dataloader.train_dataloader())], dim=0) + + to_explain = dataloader.test_dataloader() + #e = shap.GradientExplainer((model, model.module_dict['Image'].backbone.features[7]), dataloader.train_dataloader()) + #shap_values, indexes = e.shap_values(to_explain, ranked_outputs=2, nsamples=200) + + # get the names for the classes + # index_names = np.vectorize(lambda x: class_names[str(x)][1])(indexes) + + # plot the explanations + #shap_values = [np.swapaxes(np.swapaxes(s, 2, 3), 1, -1) for s in shap_values] + + # shap.image_plot(shap_values, to_explain, index_names) + + diff --git a/Training/val2.py b/Training/Validation.py similarity index 79% rename from Training/val2.py rename to Training/Validation.py index 6a72d19..fe4f3b4 100644 --- a/Training/val2.py +++ b/Training/Validation.py @@ -5,8 +5,9 @@ from pytorch_lightning.strategies import DDPStrategy from pytorch_lightning import LightningDataModule, LightningModule, Trainer, seed_everything import sys, os -#import torchio as tio +# import torchio as tio import monai + torch.cuda.empty_cache() ## Module - Dataloaders from DataGenerator.DataGenerator import * @@ -20,23 +21,21 @@ from Utils.GenerateSmoothLabel import get_smoothed_label_distribution, get_module from Utils.PredictionReports import PredictionReports from pathlib import Path -from Utils.DicomTools import img_train_transform, img_val_transform -#import torchio as tio +# import torchio as tio from torchmetrics import ConfusionMatrix import torchmetrics config = toml.load(sys.argv[1]) -total_backbone = config['MODEL']['Backbone'] + '_bitset5_seed_42' ## 2D transform img_keys = list(config['MODALITY'].keys()) -#img_keys.remove('Structs') -#if 'Structs' in config['DATA'].keys(): +# img_keys.remove('Structs') +# if 'Structs' in config['DATA'].keys(): # for roi in config['DATA']['Structs']: # img_keys.append('Struct_' + roi) train_transform = torchvision.transforms.Compose([ EnsureChannelFirstd(keys=img_keys), - #ResampleToMatchd(list(set(img_keys).difference(set(['CT']))), key_dst='CT'), + # ResampleToMatchd(list(set(img_keys).difference(set(['CT']))), key_dst='CT'), monai.transforms.ScaleIntensityd(keys=img_keys), # monai.transforms.ResizeWithPadOrCropd(keys=img_keys, spatial_size=config['DATA']['dim']), monai.transforms.Resized(keys=img_keys, spatial_size=config['DATA']['dim']), @@ -49,42 +48,41 @@ val_transform = torchvision.transforms.Compose([ EnsureChannelFirstd(keys=img_keys), - #ResampleToMatchd(list(set(img_keys).difference(set(['CT']))), key_dst='CT'), + # ResampleToMatchd(list(set(img_keys).difference(set(['CT']))), key_dst='CT'), monai.transforms.ScaleIntensityd(img_keys), # monai.transforms.ResizeWithPadOrCropd(img_keys, spatial_size=config['DATA']['dim']), monai.transforms.Resized(keys=img_keys, spatial_size=config['DATA']['dim']), ]) - ## First Connect to XNAT -session = xnat.connect(config['SERVER']['Address'], user=config['SERVER']['User'],password=config['SERVER']['Password']) - +session = xnat.connect(config['SERVER']['Address'], user=config['SERVER']['User'], + password=config['SERVER']['Password']) SubjectList = QuerySubjectList(config, session) SynchronizeData(config, SubjectList) +SubjectList.dropna(subset=['xnat_subjectdata_field_map_survival_months'], inplace=True) module_dict = nn.ModuleDict() -if config['DATA']['Multichannel']: ## Single-Model Multichannel learning +if config['DATA']['Multichannel']: ## Single-Model Multichannel learning if config['MODALITY'].keys(): module_dict['Image'] = Classifier(config, 'Image') else: - for key in config['MODALITY'].keys():# Multi-Model Single Channel learning + for key in config['MODALITY'].keys(): # Multi-Model Single Channel learning module_dict[key] = Classifier(config, key) if 'Records' in config.keys(): - module_dict['Records'] = Linear() SubjectList, clinical_cols = LoadClinicalData(config, SubjectList) - + module_dict['Records'] = Linear(in_feat=len(clinical_cols), out_feat=42) else: clinical_cols = None +print('clinical_cols:', len(clinical_cols)) ## GeneratePath for key in config['MODALITY'].keys(): - SubjectList[key+'_Path'] = "" + SubjectList[key + '_Path'] = "" QuerySubjectInfo(config, SubjectList, session) threshold = config['DATA']['threshold'] -ckpt_path = Path('./', total_backbone + '_ckpt') roc_list = [] sp_list = [] sensi_list = [] @@ -98,8 +96,9 @@ base_fpr = np.linspace(0, 1, 39) cm = ConfusionMatrix(num_classes=2) prediction_labels_full_list = [] - -for iter in range(0, 1, 1): +bidx = [21, 25, 0, 4, 6] +for it in range(0, 5, 1): + iter = bidx[it] # seed_everything(4200) dataloader = DataModule(SubjectList, config=config, @@ -108,12 +107,13 @@ val_transform=val_transform, clinical_cols=clinical_cols, inference=False, - session = session) + train_size=0.85) model = MixModel(module_dict, config) - #full_ckpt_path = Path(ckpt_path, 'Iter_'+ str(iter) + '.ckpt') - #full_ckpt_path = Path('Classification_4_ckpt', 'Iter_' + str(iter) + '.ckpt') - full_ckpt_path = 'ckpt_test_bitset7_r42/Iter_' + str(iter) + '.ckpt' + filename = 'lightning_logs/random_seed_75_Seg/version_' + str(iter) + full_ckpt_path = Path(filename, 'Iter_' + str(iter) + '.ckpt') + # full_ckpt_path = Path(ckpt_path, 'ckpt', 'Iter_' + str(iter) + '.ckpt') + # full_ckpt_path = 'Iter_' + str(iter) + '.ckpt' model.load_state_dict(torch.load(full_ckpt_path)['state_dict']) # model.load_state_dict(torch.load(full_ckpt_path, map_location='cpu')['state_dict']) model.eval() @@ -130,7 +130,7 @@ validation_labels_full = torch.cat([out['label'] for i, out in enumerate(outs)], dim=0) prediction_labels_full = torch.cat([out['prediction'] for i, out in enumerate(outs)], dim=0) roc_i = auroc(prediction_labels_full, validation_labels_full.int()) - print('roc_'+str(iter), roc_i) + print('roc_' + str(iter), roc_i) prediction_labels_full_list.append(prediction_labels_full.tolist()) prediction_labels = torch.tensor(prediction_labels_full_list).mean(dim=0) @@ -152,4 +152,3 @@ print('avg_accuracy', str(acc)) print('avg_precision', str(precision)) print('finish test') - diff --git a/Training/test.py b/Training/test.py new file mode 100644 index 0000000..83a51d7 --- /dev/null +++ b/Training/test.py @@ -0,0 +1,42 @@ +import torch, torchvision +from torch import nn +from torchvision import transforms, models, datasets +import shap +import json +import numpy as np +mean = [0.485, 0.456, 0.406] +std = [0.229, 0.224, 0.225] + +def normalize(image): + if image.max() > 1: + image /= 255 + image = (image - mean) / std + # in addition, roll the axis so that they suit pytorch + return torch.tensor(image.swapaxes(-1, 1).swapaxes(2, 3)).float() + + +# load the model +model = models.vgg16(pretrained=True).eval() + +X,y = shap.datasets.imagenet50() + +X /= 255 + +to_explain = X[[39, 41]] + +# load the ImageNet class names +url = "https://s3.amazonaws.com/deep-learning-models/image-models/imagenet_class_index.json" +fname = shap.datasets.cache(url) +with open(fname) as f: + class_names = json.load(f) + +e = shap.GradientExplainer((model, model.features[7]), normalize(X)) +shap_values,indexes = e.shap_values(normalize(to_explain), ranked_outputs=2, nsamples=200) + +# get the names for the classes +index_names = np.vectorize(lambda x: class_names[str(x)][1])(indexes) + +# plot the explanations +shap_values = [np.swapaxes(np.swapaxes(s, 2, 3), 1, -1) for s in shap_values] + +shap.image_plot(shap_values, to_explain, index_names) diff --git a/Training/val.py b/Training/val.py deleted file mode 100644 index a57d4d3..0000000 --- a/Training/val.py +++ /dev/null @@ -1,157 +0,0 @@ -import torch -import torchvision -from torch import nn -from pytorch_lightning.callbacks import ModelCheckpoint, EarlyStopping -from pytorch_lightning.strategies import DDPStrategy -from pytorch_lightning import LightningDataModule, LightningModule, Trainer, seed_everything -import sys, os -import torchio as tio -import monai -torch.cuda.empty_cache() -## Module - Dataloaders -from DataGenerator.DataGenerator import * -from Models.Classifier import Classifier -from Models.Linear import Linear -from Models.MixModel import MixModel -from monai.transforms import EnsureChannelFirstd, ScaleIntensityd, ResampleToMatchd -## Main -from sklearn.preprocessing import StandardScaler, OneHotEncoder -import toml -from Utils.GenerateSmoothLabel import get_smoothed_label_distribution, get_module -from Utils.PredictionReports import PredictionReports -from pathlib import Path -from Utils.DicomTools import img_train_transform, img_val_transform -import torchio as tio -from torchmetrics import ConfusionMatrix -import torchmetrics - -config = toml.load(sys.argv[1]) -total_backbone = "" -## 2D transform -img_keys = list(config['MODALITY'].keys()) -img_keys.remove('Structs') -if 'Structs' in config['DATA'].keys(): - for roi in config['DATA']['Structs']: - img_keys.append('Struct_' + roi) - -#img_keys = list(config['MODALITY'].keys()) -#if 'Structs' in config['DATA'].keys(): -# img_keys.append('Mask') - -train_transform = torchvision.transforms.Compose([ - EnsureChannelFirstd(keys=img_keys), - ResampleToMatchd(list(set(img_keys).difference(set(['CT']))), key_dst='CT'), - monai.transforms.ScaleIntensityd(keys=img_keys), - # monai.transforms.ResizeWithPadOrCropd(keys=img_keys, spatial_size=config['DATA']['dim']), - monai.transforms.Resized(keys=img_keys, spatial_size=config['DATA']['dim']), - monai.transforms.RandAffined(keys=img_keys), - monai.transforms.RandHistogramShiftd(keys=img_keys), - monai.transforms.RandAdjustContrastd(keys=img_keys), - monai.transforms.RandGaussianNoised(keys=img_keys), - -]) - -val_transform = torchvision.transforms.Compose([ - EnsureChannelFirstd(keys=img_keys), - ResampleToMatchd(list(set(img_keys).difference(set(['CT']))), key_dst='CT'), - monai.transforms.ScaleIntensityd(img_keys), - # monai.transforms.ResizeWithPadOrCropd(img_keys, spatial_size=config['DATA']['dim']), - monai.transforms.Resized(keys=img_keys, spatial_size=config['DATA']['dim']), -]) - - -## First Connect to XNAT -session = xnat.connect(config['SERVER']['Address'], user=config['SERVER']['User'],password=config['SERVER']['Password']) - - -SubjectList = QuerySubjectList(config, session) -SynchronizeData(config, SubjectList) - -module_dict = nn.ModuleDict() -if config['DATA']['Multichannel']: ## Single-Model Multichannel learning - if config['MODALITY'].keys(): - module_dict['Image'] = Classifier(config, 'Image') -else: - for key in config['MODALITY'].keys():# Multi-Model Single Channel learning - module_dict[key] = Classifier(config, key) - -if 'Records' in config.keys(): - module_dict['Records'] = Linear() - SubjectList, clinical_cols = LoadClinicalData(config, SubjectList) - -else: - clinical_cols = None - -## GeneratePath -for key in config['MODALITY'].keys(): - SubjectList[key+'_Path'] = "" -QuerySubjectInfo(config, SubjectList, session) -print(SubjectList) - -threshold = config['DATA']['threshold'] -ckpt_path = Path('./', total_backbone + '_ckpt') -roc_list = [] -sp_list = [] -sensi_list = [] -acc_list = [] -pre_list = [] - -tprs = [] -roc = torchmetrics.ROC() -auroc = torchmetrics.AUROC() -fig = plt.figure() -base_fpr = np.linspace(0, 1, 39) -cm = ConfusionMatrix(num_classes=2) -prediction_labels_full_list = [] - -for iter in range(0,1,1): - # seed_everything(4200) - dataloader = DataModule(SubjectList, - config=config, - keys=config['MODALITY'].keys(), - train_transform=train_transform, - val_transform=val_transform, - clinical_cols=clinical_cols, - inference=False, - session = session) - - model = MixModel(module_dict, config) - full_ckpt_path = Path(ckpt_path, 'Iter_'+ str(iter) + '.ckpt') - # full_ckpt_path = Path('Classification_4_ckpt', 'Iter_' + str(iter) + '.ckpt') - # full_ckpt_path = 'ckpt_test/Iter_' + str(iter) + '.ckpt' - model.load_state_dict(torch.load(full_ckpt_path)['state_dict']) - # model.load_state_dict(torch.load(full_ckpt_path, map_location='cpu')['state_dict']) - model.eval() - print('start testing...') - worstCase = 0 - with torch.no_grad(): - outs = [] - for i, data in enumerate(dataloader.test_dataloader()): - truth = data[1] - x = data[0] - output = model.test_step(data, i) - outs.append(output) - - validation_labels_full = torch.cat([out['label'] for i, out in enumerate(outs)], dim=0) - prediction_labels_full = torch.cat([out['prediction'] for i, out in enumerate(outs)], dim=0) - prediction_labels_full_list.append(prediction_labels_full.tolist()) - -prediction_labels = torch.tensor(prediction_labels_full_list).mean(dim=0) -validation_labels = validation_labels_full -roc = auroc(prediction_labels, validation_labels.int()) -bcm = cm(prediction_labels.round(), validation_labels.int()) -tn = bcm[0][0] -tp = bcm[1][1] -fp = bcm[0][1] -fn = bcm[1][0] -acc = bcm.diag().sum() / bcm.sum() -sensitivity = tp / (tp + fn) -precision = tp / (tp + fp) -spec = tn / (tn + fp) - -print('avg_roc', str(roc)) -print('avg_specificity', str(spec)) -print('avg_sensitivity', str(sensitivity)) -print('avg_accuracy', str(acc)) -print('avg_precision', str(precision)) -print('finish test') diff --git a/Utils/DicomTools.py b/Utils/DicomTools.py index 5ad03a5..be33775 100644 --- a/Utils/DicomTools.py +++ b/Utils/DicomTools.py @@ -102,31 +102,31 @@ def get_masked_img_voxel(ImageVoxel, mask_voxel): return img_masked -def img_train_transform(img_dim): - transform = tio.Compose([ - tio.transforms.ZNormalization(), - tio.RandomAffine(), - tio.RandomFlip(), - tio.RandomNoise(), - tio.RandomMotion(), - tio.transforms.Resize(img_dim), - tio.RescaleIntensity(out_min_max=(0, 1)) - ]) - return transform - - -def img_val_transform(img_dim): - transform = tio.Compose([ - tio.transforms.ZNormalization(), - tio.transforms.Resize(img_dim), - tio.RescaleIntensity(out_min_max=(0, 1)) - ]) - return transform +# def img_train_transform(img_dim): +# transform = tio.Compose([ +# tio.transforms.ZNormalization(), +# tio.RandomAffine(), +# tio.RandomFlip(), +# tio.RandomNoise(), +# tio.RandomMotion(), +# tio.transforms.Resize(img_dim), +# tio.RescaleIntensity(out_min_max=(0, 1)) +# ]) +# return transform +# +# +# def img_val_transform(img_dim): +# transform = tio.Compose([ +# tio.transforms.ZNormalization(), +# tio.transforms.Resize(img_dim), +# tio.RescaleIntensity(out_min_max=(0, 1)) +# ]) +# return transform def class_stratify(SubjectList, config): ptarget = SubjectList['xnat_subjectdata_field_map_' + config['DATA']['target']] - kbins = KBinsDiscretizer(n_bins=5, encode='ordinal', strategy='uniform') + kbins = KBinsDiscretizer(n_bins=15, encode='ordinal', strategy='uniform') ptarget = np.array(ptarget).reshape((len(ptarget), 1)) data_trans = kbins.fit_transform(ptarget) return data_trans @@ -169,14 +169,14 @@ def get_nii_masks(slabel, mask_imgs, MPath, mask_names): raise ValueError(slabel + " has no ROI of name " + roi + " found in RTStruct") mask_imgs = BitSet(mask_imgs, idx * np.ones_like(mask_imgs), data) return mask_imgs - #for roi in mask_names: + # for roi in mask_names: # try: # data, meta = LoadImage()(Path(MPath, roi + '.nii.gz')) # except: # raise ValueError(slabel + " has no ROI of name " + roi + " found in RTStruct") # mask_img = distance_transform_edt(data) # mask_imgs = mask_imgs + mask_img - #return mask_imgs + # return mask_imgs def BitSet(n, p, b): diff --git a/Utils/PredictionReports.py b/Utils/PredictionReports.py index 714b6ba..02bfeb2 100644 --- a/Utils/PredictionReports.py +++ b/Utils/PredictionReports.py @@ -38,35 +38,34 @@ def __init__(self, config, def log_hyperparams(self, params: argparse.Namespace, *args, **kwargs): pass - @property - def version(self): - description = '' - for i, param in enumerate(self.config['CRITERIA'].keys()): - clinical_criteria = str(self.config['CRITERIA'][param]) - if i > 0: - description = description + '_' - description = description + param + '+' + '+'.join(clinical_criteria) - # Return the experiment version, int or str. - - sub_str = description + '_' + 'modalities' + '+' + '+'.join(self.config['MODALITY'].keys()) - self._version = self._get_next_version(sub_str) - description = description + '_' + 'modalities' + '+' + '+'.join(self.config['MODALITY'].keys()) + '_' + str(self._version) - return description - - def _get_next_version(self, sub_str): - root_dir = self.root_dir - listdir_info = self._fs.listdir(root_dir) - existing_versions = [] - for listing in listdir_info: - d = listing["name"] - bn = os.path.basename(d) - if self._fs.isdir(d) and bn.startswith(sub_str): - dir_ver = bn.split("_")[-1] - existing_versions.append(int(dir_ver)) - if len(existing_versions) == 0: - return 0 - - return max(existing_versions) + 1 + # @property + # def version(self): + # description = '' + # for i, param in enumerate(self.config['CRITERIA'].keys()): + # clinical_criteria = str(self.config['CRITERIA'][param]) + # if i > 0: + # description = description + '_' + # description = description + param + '+' + '+'.join(clinical_criteria) + # # Return the experiment version, int or str. + # + # sub_str = description + '_' + 'modalities' + '+' + '+'.join(self.config['MODALITY'].keys()) + # description = description + '_' + 'modalities' + '+' + '+'.join(self.config['MODALITY'].keys()) + '_' + str(self._get_next_version(sub_str)) + # return description + # + # def _get_next_version(self, sub_str): + # root_dir = self.root_dir + # listdir_info = self._fs.listdir(root_dir) + # existing_versions = [] + # for listing in listdir_info: + # d = listing["name"] + # bn = os.path.basename(d) + # if self._fs.isdir(d) and bn.startswith(sub_str): + # dir_ver = bn.split("_")[-1] + # existing_versions.append(int(dir_ver)) + # if len(existing_versions) == 0: + # return 0 + # + # return max(existing_versions) + 1 def log_metrics(self, metrics: Dict[str, float], step: Optional[int] = None): for k, v in metrics.items(): diff --git a/scripts/CropMask.py b/scripts/CropMask.py new file mode 100644 index 0000000..ecd4150 --- /dev/null +++ b/scripts/CropMask.py @@ -0,0 +1,32 @@ +import nibabel as nib +import sys, glob +import matplotlib.pyplot as plt +import numpy as np +import pandas as pd +from scipy import ndimage +from pathlib import Path + +path = '/home/dgs1/data/InnerEye/nii_root_folder' +data = pd.read_csv('/home/dgs1/data/InnerEye/CSV/dataset_lung_spinal.csv') +patient_id = data['subject'].unique() + +crop_oar = set(data['channel'].unique()).difference(set(['lung_left','lung_right','ct'])) + +for i in range(len(patient_id)): + pat = data[data.subject == i] + + mask_nii = pat[pat.channel == 'lung_left']['filePath'] + info = nib.load(Path(path, list(mask_nii)[0])) + temp = info.get_fdata() + + mask_nii = pat[pat.channel == 'lung_right']['filePath'] + info = nib.load(Path(path, list(mask_nii)[0])) + temp = temp + info.get_fdata() + + Zm = np.sum(temp, axis=(0,1)) + index = np.argwhere(Zm == 0).squeeze() + for oar in crop_oar: + mask_nii = pat[pat.channel == oar]['filePath'] + info = nib.load(Path(path, list(mask_nii)[0])) + img = info.get_fdata() + img[:, :, index] = np.nan diff --git a/scripts/DICOM2NIFTY.py b/scripts/DICOM2NIFTY.py new file mode 100644 index 0000000..bcfb5b7 --- /dev/null +++ b/scripts/DICOM2NIFTY.py @@ -0,0 +1,66 @@ +import torch +import sys, os +torch.cuda.empty_cache() +from DataGenerator.DataGenerator import * +import toml +from pathlib import Path +import nibabel as nib +from monai.transforms import Spacing, LoadImage, EnsureChannelFirst +from dcmrtstruct2nii import dcmrtstruct2nii, list_rt_structs +config = toml.load(sys.argv[1]) +## First Connect to XNAT +session = xnat.connect(config['SERVER']['Address'], user=config['SERVER']['User'],password=config['SERVER']['Password']) + + +SubjectList = QuerySubjectList(config, session) +SynchronizeData(config, SubjectList) +## GeneratePath +for key in config['MODALITY'].keys(): + SubjectList[key+'_Path'] = "" +QuerySubjectInfo(config, SubjectList, session) +print(SubjectList) + +save_path = '/home/dgs1/data/Nifty_Data/UCLH/' + +for i in range(253, len(SubjectList)): + sub = SubjectList.iloc[i] + patient_label = sub['subject_label'] + out_folder = Path(save_path, patient_label) + RSPath = sub['Structs_Path'] + CTPath = sub['CT_Path'] + DosePath = sub['Dose_Path'] + # command = f'plastimatch convert --input {CTPath} --output-img {out_folder}/CT.nii.gz --spacing "1 1 3"' + # os.system(command) + # DoseName = 'Dose' + # command = f'plastimatch convert --input-dose-img {DosePath} --referenced-ct {CTPath} --resize-dose --output-dose-img {out_folder}/{DoseName}.nii.gz --fixed {out_folder}/CT.nii.gz' + # os.system(command) + + # command = f'plastimatch convert --input {RSPath} --fixed {out_folder}/CT.nii.gz --output-prefix {out_folder}/struct_DICOM --referenced-ct {CTPath} --prefix-format nii.gz' + # os.system(command) + # + # + Struct_folder = Path(out_folder, 'struct_DICOM') + print('before ', i, patient_label) + if not os.path.exists(Struct_folder): + os.system(f'mkdir {Struct_folder}') + # dcmrtstruct2nii(RSPath, CTPath, Path(out_folder, 'struct_DICOM')) + CTArray, meta = LoadImage()(CTPath) + RS = RTStructBuilder.create_from(dicom_series_path=CTPath, rt_struct_path=RSPath) + # %% + roi_names = RS.get_roi_names() + + for j in range(len(roi_names)): + mask_img = RS.get_roi_mask_by_name(roi_names[j]) + mask_img = np.rot90(mask_img) + mask_img = np.flip(mask_img, 0) + mask = MetaTensor(mask_img.copy(), meta=meta) + mask = EnsureChannelFirst()(mask) + mask = Spacing(pixdim=(1, 1, 3))(mask) + mask_array = mask.array.squeeze() + ni_mask = nib.Nifti1Image(mask_array.astype('int'), affine=mask.affine, dtype='uint8') + nib.save(ni_mask, Path(out_folder, 'struct_DICOM', roi_names[j]+'.nii.gz')) + + print(i) + + + diff --git a/scripts/DoseNii.py b/scripts/DoseNii.py new file mode 100644 index 0000000..b417b01 --- /dev/null +++ b/scripts/DoseNii.py @@ -0,0 +1,71 @@ +import sys +import toml +import glob +import nibabel as nib +import numpy as np +from Utils.DicomTools import * +from pathlib import Path +from DataGenerator.DataGenerator import QuerySubjectList, SynchronizeData, QuerySubjectInfo +from Utils.DicomTools import * +import os +from rt_utils import RTStructBuilder +import xnat +from Utils.FixRTSS import * +session = xnat.connect('http://128.16.11.124:8080/xnat', user='admin', password='mortavar1977') +import csv +from monai.transforms import Spacing, LoadImage, EnsureChannelFirst, ResampleToMatch +from monai.data import MetaTensor +import itk +config = toml.load(sys.argv[1]) + +session = xnat.connect(config['SERVER']['Address'], user=config['SERVER']['User'],password=config['SERVER']['Password']) +SubjectList = QuerySubjectList(config, session) +print(SubjectList) +SynchronizeData(config, SubjectList) + +for key in config['MODALITY'].keys(): + SubjectList[key+'_Path'] = "" +QuerySubjectInfo(config, SubjectList, session) + +path = config['DATA']['NiiFolder'] +sPatient = SubjectList + +for i in range(301,len(sPatient), 1): + print(i) + subjectid = sPatient.loc[i, 'subjectid'] + subject_label = sPatient.loc[i,'subject_label'] + + CTPath = SubjectList.loc[i, 'CT_Path'] + CTArray, meta = LoadImage()(CTPath) + DosePath = SubjectList.loc[i,'Dose_Path'] + DoseArray, dmeta = LoadImage()(DosePath) + DoseArray = DoseArray * np.double(dmeta['3004|000e']) + + ct = EnsureChannelFirst()(CTArray) + ct = Spacing(pixdim=(1, 1, 3))(ct) + DoseArray = EnsureChannelFirst()(DoseArray) + dose = ResampleToMatch()(DoseArray, ct) + + names_generator = itk.GDCMSeriesFileNames.New() + names_generator.SetUseSeriesDetails(True) + names_generator.AddSeriesRestriction("0008|0021") # Series Date + names_generator.SetDirectory(list(CTPath)[0]) + series_uid = names_generator.GetSeriesUIDs() + + if len(series_uid) > 1: + print(series_uid) + else: + ct_array = ct.array.squeeze() + ni_ct = nib.Nifti1Image(np.double(ct_array), affine=ct.affine) + dose_array = dose.array.squeeze() + ni_dose = nib.Nifti1Image(np.double(dose_array), affine=dose.affine) + spath = Path(path, subject_label) + print(subject_label) + + if not os.path.isdir(spath): + os.mkdir(spath) + nib.save(ni_ct, Path(spath, 'ct.nii.gz')) + nib.save(ni_dose, Path(spath, 'dose.nii.gz')) + + + diff --git a/scripts/FMaskDcm.py b/scripts/FMaskDcm.py new file mode 100644 index 0000000..058281d --- /dev/null +++ b/scripts/FMaskDcm.py @@ -0,0 +1,75 @@ +import sys +import os +import csv +from pathlib import Path +from monai.transforms import LoadImage +import nibabel as nib +import numpy as np + +base_path = '/home/dgs1/data/InnerEye/nii_root_folder/RTOG/' + +subjects = os.listdir(base_path) +# roi = ['heart', 'oesophagus', 'spinal canal', 'prox bronch tree', 'proximal trachea', 'cwall & ribs', 'lt lung','rt lung'] +# roi_name = ['heart', 'oesophagus', 'spinal_canal','prox_bronch_tree', 'proximal_trachea','cwall_ribs','lt_lung','rt_lung'] +# roi = ['lung_cntr','lung_ipsi'] +# roi_name = ['lung_cntr','lung_ipsi'] +roi = ['esophagus', 'lung', 'heart', 'ptv'] +header = ['subject', 'filePath', 'channel'] +count = 0 +rm_sub = [] + + +# for i in range(len(subjects)): +# sub = subjects[i] +# subpath = Path(base_path, sub) +# basename = os.listdir(subpath) +# if os.path.exists(Path(subpath, 'lung_left.nii.gz')) and os.path.exists(Path(subpath, 'lung_right.nii.gz')): +# LungL, meta = LoadImage()(Path(subpath, 'lung_left.nii.gz')) +# LungR, meta = LoadImage()(Path(subpath, 'lung_right.nii.gz')) +# Lung = LungL + LungR +# ni_mask = nib.Nifti1Image(Lung.astype('int'), affine=meta['affine'], dtype='uint8') +# nib.save(ni_mask, Path(subpath, 'lung.nii.gz')) +# else: +# rm_sub.append(sub) +# print(rm_sub) + +def BitSet(n, p, b): + p = p.astype(int) + n = n.astype(int) + b = b.astype(int) + mask = 1 << p + bm = b << p + return (n & ~mask) | bm + + +for i in range(len(subjects)): + sub = subjects[i] + subpath = Path(base_path, sub) + basename = os.listdir(subpath) + ct_name = ['ct.nii.gz', 'dose.nii.gz'] + oars = set(basename).difference(set(ct_name)) + oar_list = [] + + for oar in oars: + oar_name = oar.split(".") + oar_list.append(oar_name[0].lower()) + + if set(roi).issubset(set(oar_list)): + count = count + 1 + for idx, r in enumerate(roi): + try: + data, meta = LoadImage()(Path(subpath, r+'.nii.gz')) + if idx == 0: + masks_img = np.zeros_like(data) + except: + raise ValueError(sub + " has no ROI of name " + roi + " found in RTStruct") + masks_img = BitSet(masks_img, idx * np.ones_like(masks_img), data) + + ni_img = nib.Nifti1Image(np.double(masks_img), affine=meta['affine']) + nib.save(ni_img, Path(subpath, 'masks.nii.gz')) + else: + rm_sub.append(sub) + + +print('Counts: ', count) +print(rm_sub) \ No newline at end of file diff --git a/scripts/PTV.py b/scripts/PTV.py new file mode 100644 index 0000000..c2a51e3 --- /dev/null +++ b/scripts/PTV.py @@ -0,0 +1,103 @@ +import sys +#sys.path.insert(0, '/home/dgs1/Software/OutcomePrediction/') +import toml +import glob +import nibabel as nib +import numpy as np +from Utils.DicomTools import * +from pathlib import Path +from DataGenerator.DataGenerator import QuerySubjectList, SynchronizeData, QuerySubjectInfo +from Utils.DicomTools import * +import os +from rt_utils import RTStructBuilder +import xnat +from Utils.FixRTSS import * +session = xnat.connect('http://128.16.11.124:8080/xnat', user='admin', password='mortavar1977') +import csv +from monai.transforms import Spacing, LoadImage, EnsureChannelFirst +from monai.data import MetaTensor +import itk +config = toml.load(sys.argv[1]) +from scipy import ndimage +session = xnat.connect(config['SERVER']['Address'], user=config['SERVER']['User'],password=config['SERVER']['Password']) +SubjectList = QuerySubjectList(config, session) +print(SubjectList) +SynchronizeData(config, SubjectList) + +for key in config['MODALITY'].keys(): + SubjectList[key+'_Path'] = "" +QuerySubjectInfo(config, SubjectList, session) +# +# roi_series = ['Heart', 'Oesophagus', 'Spinal Canal', 'Prox bronch tree', 'Proximal trachea', 'Cwall & ribs', 'L Lung', +# 'R Lung', 'Brachial Plexus'] +# roi_name = ['HEART', 'ESOPHAGUS', 'SPINAL_CORD', 'PROX_BRONCH_TREE', 'PROXIMAL_TRACHEA', 'CWALL_RIBS', 'LUNG_LEFT', +# 'LUNG_RIGHT', 'BRACHIAL_PLEXUS'] +# roi_series = ['Heart', 'Esophagus', 'Lung_L', 'Lung_R','SpinalCord'] +# roi_name = ['HEART', 'ESOPHAGUS', 'LUNG_LEFT','LUNG_RIGHT', 'SPINAL_CORD'] + +# roi_series = ['HEART', 'PTV', 'LUNG_IPSI', 'LUNG_CNTR', 'SPINAL_CORD', 'ESOPHAGUS'] +# roi_name = ['HEART', 'PTV', 'LUNG_IPSI', 'LUNG_CNTR', 'SPINAL_CORD', 'ESOPHAGUS'] +roi_name = 'ptv' + +path = config['DATA']['NiiFolder'] +sPatient = SubjectList + +for i in range(0, len(SubjectList), 1): + subjectid = sPatient.loc[i, 'subjectid'] + subject_label = sPatient.loc[i,'subject_label'] + CTPath = SubjectList[SubjectList.subjectid == subjectid]['CT_Path'] + CTArray, meta = LoadImage()(CTPath) + ct = EnsureChannelFirst()(CTArray) + ct = Spacing(pixdim=(1, 1, 3))(ct) + + names_generator = itk.GDCMSeriesFileNames.New() + names_generator.SetUseSeriesDetails(True) + names_generator.AddSeriesRestriction("0008|0021") # Series Date + names_generator.SetDirectory(list(CTPath)[0]) + series_uid = names_generator.GetSeriesUIDs() + print('No.{}:'.format(i) + str(subject_label)) + + if len(series_uid) > 1: + print(series_uid) + else: + ct_array = ct.array.squeeze() + # First define the ROI based on target + RSPath = SubjectList[SubjectList.subjectid == subjectid]['Structs_Path'] + # FixRTSS(RSPath[0], list(CTPath)[0]) + RS = RTStructBuilder.create_from(dicom_series_path=list(CTPath)[0], rt_struct_path=list(RSPath)[0]) + #%% + roi_names = RS.get_roi_names() + strList = [x.lower() for x in roi_names] + ni_img = nib.Nifti1Image(np.double(ct_array), affine=ct.affine) + spath = Path(path, subject_label) + + if not os.path.isdir(spath): + os.mkdir(spath) + nib.save(ni_img, Path(spath, 'ct.nii.gz')) + + # r1 = [r for r in strList if roi_name in r] + + # mask = np.zeros_like(CTArray) + + # for j in range(len(r1)): + # roi = r1[j] + # index = strList.index(roi.lower()) + # mask_img = RS.get_roi_mask_by_name(roi_names[index]) + # mask_img = np.rot90(mask_img) + # mask_img = np.flip(mask_img, 0) + # mask = mask + mask_img + + index = strList.index(roi_name.lower()) + mask_img = RS.get_roi_mask_by_name(roi_names[index]) + mask_img = np.rot90(mask_img) + mask_img = np.flip(mask_img, 0) + + mask = MetaTensor(mask_img.copy(), meta=meta) + mask = EnsureChannelFirst()(mask) + mask = Spacing(pixdim=(1, 1, 3))(mask) + mask_array = mask.array.squeeze() + ni_mask = nib.Nifti1Image(mask_array.astype('int'), affine=mask.affine, dtype='uint8') + nib.save(ni_mask, Path(spath, 'AI_target.nii.gz')) + + + diff --git a/scripts/RTOG.py b/scripts/RTOG.py new file mode 100644 index 0000000..9cbeea5 --- /dev/null +++ b/scripts/RTOG.py @@ -0,0 +1,54 @@ +import sys +#sys.path.insert(0, '/home/dgs1/Software/OutcomePrediction/') +import toml +import glob +import nibabel as nib +import numpy as np +from Utils.DicomTools import * +from pathlib import Path +from DataGenerator.DataGenerator import QuerySubjectList, SynchronizeData, QuerySubjectInfo +from Utils.DicomTools import * +import os +from rt_utils import RTStructBuilder +import xnat +from Utils.FixRTSS import * +session = xnat.connect('http://128.16.11.124:8080/xnat', user='admin', password='mortavar1977') +import csv +from monai.transforms import Spacing, LoadImage, EnsureChannelFirst +from monai.data import MetaTensor +import itk +import pandas as pd +from pydicom import dcmread +config = toml.load(sys.argv[1]) + +session = xnat.connect(config['SERVER']['Address'], user=config['SERVER']['User'],password=config['SERVER']['Password']) +SubjectList = QuerySubjectList(config, session) +print(SubjectList) +SynchronizeData(config, SubjectList) +data = pd.read_excel('/home/dgs1/Downloads/LUNG_EXCEL.xlsx') + +for key in config['MODALITY'].keys(): + SubjectList[key+'_Path'] = "" +QuerySubjectInfo(config, SubjectList, session) +sPatient = SubjectList + +roi_set = set(['LEFT','RIGHT']) + +for i in range(0, len(SubjectList), 1): + subjectid = sPatient.loc[i, 'subjectid'] + subject_label = sPatient.loc[i,'subject_label'] + print(subject_label) + RSPath = glob.glob(list(SubjectList[SubjectList.subjectid == subjectid]['Structs_Path'])[0] + '/*dcm') + info = dcmread(RSPath[0]) + ind = list(data[data['Patient ID'] == subject_label]['LUNG_CNTR'])[0] + value = list(data[data['Patient ID'] == subject_label]['LUNG_IPSI']) + if not (ind == 'NO TUMOUR?'): + for r in range(len(info.StructureSetROISequence)): + if info.StructureSetROISequence[r].ROIName == 'LUNG_IPSI': + info.StructureSetROISequence[r].ROIName = 'LUNG_' + value[0] + if info.StructureSetROISequence[r].ROIName == 'LUNG_CNTR': + info.StructureSetROISequence[r].ROIName = 'LUNG_' + list(roi_set.difference(set(value)))[0] + info.save_as(RSPath[0]) + + + diff --git a/scripts/RTOG_nii.py b/scripts/RTOG_nii.py new file mode 100644 index 0000000..d6d2ba1 --- /dev/null +++ b/scripts/RTOG_nii.py @@ -0,0 +1,38 @@ +import nibabel as nib +import sys, glob +import matplotlib.pyplot as plt +import numpy as np +import pandas as pd +from scipy import ndimage +from pathlib import Path +import os + +path = '/home/dgs1/data/InnerEye/nii_root_folder/RTOG/' +data = pd.read_csv('/home/dgs1/data/InnerEye/nii_root_folder/RTOG/dataset.csv') +patient_id = data['subject'].unique() +cdata = pd.read_excel('/home/dgs1/Downloads/LUNG_EXCEL.xlsx') +roi_set = set(['LEFT','RIGHT']) +for i in range(142, len(patient_id), 1): + pat = data[data.subject == i] + mask_nii = pat[pat.channel == 'lung_ipsi']['filePath'] + id = list(mask_nii)[0].split('/')[0] + ind = list(cdata[cdata['Patient ID'] == id]['LUNG_IPSI'])[0] + value = list(cdata[cdata['Patient ID'] == id]['LUNG_CNTR']) + ipsi_name = 'lung_' + list(roi_set.difference(set(value)))[0].lower() + '.nii.gz' + + old = Path(path, list(mask_nii)[0]) + new = Path(path, id, ipsi_name) + if not os.path.exists(new): + os.rename(old, new) + + mask_nii = pat[pat.channel == 'lung_cntr']['filePath'] + cntr_name = 'lung_' + value[0].lower() + '.nii.gz' + + old = Path(path, list(mask_nii)[0]) + new = Path(path, id, cntr_name) + if not os.path.exists(new): + os.rename(old, new) + + + + diff --git a/scripts/RenameDicomPatientID.py b/scripts/RenameDicomPatientID.py new file mode 100644 index 0000000..083edd2 --- /dev/null +++ b/scripts/RenameDicomPatientID.py @@ -0,0 +1,48 @@ +import os +from pydicom import dcmread +from pathlib import Path +import shutil + +path = '/home/dgs1/data/IDEAL/IDEAL/' +subjects = os.listdir(path) +for i, sub in enumerate(subjects): + print(i) + subpath = Path(path, sub) + if not str(subpath).endswith('.DS_Store'): + plan_dir = Path(subpath, 'planning_data') + ## + sub_plan_dir = os.listdir(plan_dir) + + # if len(sub_plan_dir) < 8: + # sub_plan_dir_planning = Path(plan_dir, 'planning') + # ddf = os.listdir(sub_plan_dir_planning) + # df = dcmread(Path(sub_plan_dir_planning, ddf[0]), force=True) + # sid_st = df.StudyInstanceUID + # + # for sf in set(sub_plan_dir).difference(set(['planning','.DS_Store'])): + # splan_dir = Path(plan_dir, sf) + # ddf = os.listdir(splan_dir) + # filename = Path(splan_dir,ddf[0]) + # df = dcmread(filename, force=True) + # df.StudyInstanceUID = sid_st + # df.save_as(filename) + # + # for dirpath, dirs, files in os.walk(subpath): + # for filename in files: + # fname = os.path.join(dirpath, filename) + # if not fname.endswith('.DS_Store'): + # ds = dcmread(fname, force=True) + # ds.PatientID = sub + # ds.PatientName = sub + # # print(ds.PatientID) + # ds.save_as(fname) + + + # if not str(subpath).endswith('.DS_Store'): + # rmdir = os.listdir(subpath) + # plan_dir = Path(subpath, 'planning_data') + # + # for rd in set(rmdir).difference(set(['planning_data'])): + # if not rd.endswith('.DS_Store'): + # shutil.rmtree(Path(subpath,rd)) + # print(rd) diff --git a/check.py b/scripts/check.py similarity index 100% rename from check.py rename to scripts/check.py diff --git a/renameCT.py b/scripts/renameCT.py similarity index 100% rename from renameCT.py rename to scripts/renameCT.py diff --git a/scripts/sub_dose_analysis_total.py b/scripts/sub_dose_analysis_total.py new file mode 100644 index 0000000..cb844c8 --- /dev/null +++ b/scripts/sub_dose_analysis_total.py @@ -0,0 +1,88 @@ +import sys +sys.path.insert(0, '/home/dgs1/Software/OutcomePrediction/') +import csv +import glob +import os +from pathlib import Path +import nibabel as nib +import numpy as np +import pandas +from scipy import ndimage +import toml +from DataGenerator.DataGenerator import QuerySubjectList, SynchronizeData, QuerySubjectInfo +from Utils.DicomTools import * +import xnat + +session = xnat.connect('http://128.16.11.124:8080/xnat', user='admin', password='mortavar1977') +config = toml.load(sys.argv[1]) +session = xnat.connect(config['SERVER']['Address'], user=config['SERVER']['User'], + password=config['SERVER']['Password']) +SubjectList = QuerySubjectList(config, session) +print(SubjectList) +SynchronizeData(config, SubjectList) + +if 'Records' in config.keys(): + SubjectList, clinical_cols = LoadClinicalData(config, SubjectList) + +else: + clinical_cols = None + +for key in config['MODALITY'].keys(): + SubjectList[key + '_Path'] = "" +QuerySubjectInfo(config, SubjectList, session) + +path = '/home/dgs1/data/InnerEye/Seg' +header = ['idx', 'subject', 'survival_months', 'ptv', 'heart_atrium_left', 'heart_atrium_right', 'heart_myocardium', + 'heart_ventricle_left', 'heart_ventricle_right', 'pulmonary_artery', 'position', 'position_label'] +idx = 0 +mask_list = ['ptv.nii.gz', 'heart_atrium_left.nii.gz', 'heart_atrium_right.nii.gz', 'heart_myocardium.nii.gz', + 'heart_ventricle_left.nii.gz', 'heart_ventricle_right.nii.gz', 'pulmonary_artery.nii.gz'] + +clinical_cols = ['xnat_subjectdata_field_map_survival_months', 'xnat_subjectdata_field_map_grade3_toxicity'] + +with open('position_dmean.csv', 'w', encoding='UTF8', newline='') as f: + writer = csv.writer(f) + writer.writerow(header) + for patient_folder in glob.glob(path + "/0617-*"): + id = patient_folder.split('/')[-1] + organs = os.listdir(patient_folder) + data = [idx, id] + + col_data = SubjectList.loc[ + SubjectList.subjectid == subjectid, 'xnat_subjectdata_field_map_' + config['DATA']['target']] + data.append(col_data) + + if 'ptv.nii.gz' in organs: + dose_info = nib.load(Path(patient_folder, 'dose.nii.gz')) + dose_img = dose_info.get_fdata() + + for mask in mask_list: + mask_info = nib.load(Path(patient_folder, mask)) + mask_img = mask_info.get_fdata() + dmean = dose_img[mask_img > 0].mean() + data.append(dmean) + + ptv_info = nib.load(Path(patient_folder, 'ptv.nii.gz')) + ptv_img = ptv_info.get_fdata() + gtv_img = ndimage.binary_erosion(ptv_img, structure=np.ones((10, 10, 5))).astype(ptv_img.dtype) + g_slice = gtv_img.sum(axis=(0, 1)) + g_index = [i for i, v in enumerate(g_slice) if v > 0] + + PTV_COM = list(map(int, ndimage.center_of_mass(ptv_img))) + # print('PTV_COM: ', PTV_COM) + T7_info = nib.load(Path(patient_folder, 'vertebrae_T7.nii.gz')) + T7_img = T7_info.get_fdata() + t_slice = T7_img.sum(axis=(0, 1)) + t_index = [i for i, v in enumerate(t_slice) if v > 0] + T7_COM = list(map(int, ndimage.center_of_mass(T7_img))) + pos = PTV_COM[2] - T7_COM[2] + data.append(pos) + intersect = set(g_index).intersection(set(t_index)) + # print('T7_COM:', T7_COM) + if len(intersect) == 0 & pos > 0: + data.append(0) + else: + data.append(1) + writer.writerow(data) + idx = idx + 1 +print(idx) diff --git a/scripts/test.py b/scripts/test.py new file mode 100644 index 0000000..67bc513 --- /dev/null +++ b/scripts/test.py @@ -0,0 +1,52 @@ +import sys +#sys.path.insert(0, '/home/dgs1/Software/OutcomePrediction/') +import toml +import glob +import nibabel as nib +import numpy as np +from Utils.DicomTools import * +from pathlib import Path +from DataGenerator.DataGenerator import QuerySubjectList, SynchronizeData, QuerySubjectInfo +from Utils.DicomTools import * +import os +from rt_utils import RTStructBuilder +import xnat +from Utils.FixRTSS import * +session = xnat.connect('http://128.16.11.124:8080/xnat', user='admin', password='mortavar1977') +import csv +from monai.transforms import Spacing, LoadImage, EnsureChannelFirst +from monai.data import MetaTensor +import itk +import pandas as pd +from pydicom import dcmread +config = toml.load('/home/dgs1/Software/OutcomePrediction/SettingsRTOG_Inner2.ini') + +session = xnat.connect(config['SERVER']['Address'], user=config['SERVER']['User'],password=config['SERVER']['Password']) +SubjectList = QuerySubjectList(config, session) +print(SubjectList) +# SynchronizeData(config, SubjectList) + +for key in config['MODALITY'].keys(): + SubjectList[key+'_Path'] = "" +QuerySubjectInfo(config, SubjectList, session) +sPatient = SubjectList + +roi_set = set(['LEFT','RIGHT']) +plist = [] + +for i in range(0, len(SubjectList), 1): + subjectid = sPatient.loc[i, 'subjectid'] + subject_label = sPatient.loc[i,'subject_label'] + # print(subject_label) + RSPath = list(SubjectList[SubjectList.subjectid == subjectid]['Structs_Path'])[0] + pl = RSPath.split('/') + plc = '/'.join(pl[0:8]) + ll = glob.glob(plc+'/*DOSE') + if len(ll) > 1: + plist.append(subject_label) + print(subject_label) + print(ll) + + +print('test') + diff --git a/scripts/xnat_data_uniformization.py b/scripts/xnat_data_uniformization.py new file mode 100644 index 0000000..ee17a60 --- /dev/null +++ b/scripts/xnat_data_uniformization.py @@ -0,0 +1,95 @@ +from pyxnat import Interface +import pandas as pd +# set up connection with xnat by 'logging in' +interface= Interface(server='http://128.16.11.124:8080/xnat', user='yzhan', password='yzhan') +from pathlib import Path +# display the different projects +list(interface.select.projects()) + +# define the project of interest +pro= interface.select.projects().get() +prj=pro[6] + +#define subjects +subjs= interface.select.projects(prj).subjects().get() +# print(dir(subjs)) +#subjs.remove('XNAT01_S00032') #delete this subject from the 'testnlst' project because it does not have any scans and it sends an error whenever the code reaches it because of that + +## this loop replaces the name of the type of each scan in each experiment for every subject by the name of the modality (which is more consistent) +# run the loop on all the subjects within the chosen project +# df = pd.read_excel('C:\\Users\\clara\\Documents\\patientdata.xlsx') +# id_list = df['id'] + +for i in range (len(subjs)): + subj_i = interface.select.projects(prj).subjects(subjs[i]) + exps = subj_i.experiments().get() + id = subj_i.get(['xnat:subjectData/SUBJECT_ID']) + # item = df.loc[id_list == id[0].label()] + # path = item['path'] + # path = Path(list(path)[0]) + # print(id[0].label()) + #run the loop on all the experiments of each subject + for k in range(len(exps)): + exp_k=exps[k] + scns=interface.select.projects(prj).subjects(subjs[i]).experiments(exp_k).scans() + #run the loop on all the scans present in each experiment + for scn in scns: + mod = scn.attrs.get('modality') + typ=scn.attrs.get('type') + if mod == 'RTSTRUCT': + scn.attrs.set('type', 'Structs') + elif mod == 'RTDOSE': + # di = scn.dicom_dump() + # for x in di: + # if x['tag1'] == '(0008,0060)': + # print(x['value']) + # scn.__setattr__('type', str('')) + scn.attrs.set('type', 'Dose') + else: + scn.attrs.set('type',str(mod)) + # print('test') + + +# import xnat +# import numpy as np +# from pathlib import Path +# import nibabel as nib +# import os +# import pandas as pd +# +# session = xnat.connect('http://128.16.11.124:8080/xnat', user='yzhan', password='yzhan') +# project = session.projects['LUNG_IDEAL'] +# subjectS = project.subjects +# print(subjectS) +# new_path = '/home/dgs1/data/InnerEye/Seg/' +# df = pd.read_excel('/home/dgs1/data/Overall.xlsx') +# id_list = df['TrialNo'] +# for subject in subjectS.values(): +# # subject.fields['analysis_inclusion'] = 1 +# label = subject.label +# No = label[-3:] +# Os = df.loc[id_list == np.uint8(No)]['os_time'] +# try: +# subject.fields['survival_months'] = list(Os)[0] +# except: +# print('error') +# # # path = item['path'] + + # newpath = Path(new_path, label, 'ptv.nii.gz') + # if os.path.exists(newpath): + # ptv_info = nib.load(newpath) + # ptv = ptv_info.get_fdata() + # ptv_volume = numpy.sum(ptv)*3/1000 + # subject.fields['volume_ptv'] = ptv_volume + + # if 'patient_sex' in subject.fields: + # if subject.fields['patient_sex'] == 'Male': + # subject.fields['gender'] = 1 + # else: + # subject.fields['gender'] = 2 + # if 'year_of_birth' in subject.fields: + # scan = subject.experiments[0].scans[0] + # di = scan.dicom_dump() + # for x in di: + # if x['tag1'] == '(0008,0012)': + # subject.fields['age'] = numpy.int16(x['value'][0:4]) - numpy.int16(subject.fields['year_of_birth']) diff --git a/submitjob.pbs b/submitjob.pbs new file mode 100644 index 0000000..312cb73 --- /dev/null +++ b/submitjob.pbs @@ -0,0 +1,39 @@ +#!/bin/bash +### Job Name +#PBS -N OP +### Project code +#PBS -A AI_OutcomePrediction +### Maximum time this job can run before being killed (here, 1 day) +#PBS -l walltime=15:00:00:00 +### Resource Request (must contain cpucore, memory, and gpu (even if requested amount is zero) +#PBS -l cpucore=30:memory=200gb:gpu=4 +### Output Options (default is stdout_and_stderr) +#PBS -l outputMode=stdout_and_stderr +##PBS -l outputMode=no_output +##PBS -l outputMode=stdout_only +##PBS -l outputMode=stderr_only + +#### ENVIRONMENT VARIABLES BELOW AUTOMATICALLY ADDED FROM QSUB #### +# >>> conda initialize >>> +# !! Contents within this block are managed by 'conda init' !! +__conda_setup="$('/home/dgs1/anaconda3/bin/conda' 'shell.bash' 'hook' 2> /dev/null)" +if [ $? -eq 0 ]; then + eval "$__conda_setup" +else + if [ -f "/home/dgs1/anaconda3/etc/profile.d/conda.sh" ]; then + . "/home/dgs1/anaconda3/etc/profile.d/conda.sh" + else + export PATH="/home/dgs1/anaconda3/bin:$PATH" + fi +fi +unset __conda_setup +# <<< conda initialize <<< +conda activate UCL_OP +export PYTHONPATH=$PYTHONPATH:/home/dgs1/Software/OutcomePrediction +export LS_COLORS="$LS_COLORS:ow=1;34:tw=1;34:" +#### ENVIRONMENT VARIABLES ABOVE AUTOMATICALLY ADDED FROM QSUB #### +python Training/Regression.py Configs/SettingsRTOG_Multi10.ini + +### Run your job here +#printenv +#python Training/Preprocess.py ConfigDefault.ini diff --git a/submitjob2.pbs b/submitjob2.pbs new file mode 100644 index 0000000..c39afdd --- /dev/null +++ b/submitjob2.pbs @@ -0,0 +1,39 @@ +#!/bin/bash +### Job Name +#PBS -N OP +### Project code +#PBS -A AI_OutcomePrediction +### Maximum time this job can run before being killed (here, 1 day) +#PBS -l walltime=15:00:00:00 +### Resource Request (must contain cpucore, memory, and gpu (even if requested amount is zero) +#PBS -l cpucore=30:memory=200gb:gpu=4 +### Output Options (default is stdout_and_stderr) +#PBS -l outputMode=stdout_and_stderr +##PBS -l outputMode=no_output +##PBS -l outputMode=stdout_only +##PBS -l outputMode=stderr_only + +#### ENVIRONMENT VARIABLES BELOW AUTOMATICALLY ADDED FROM QSUB #### +# >>> conda initialize >>> +# !! Contents within this block are managed by 'conda init' !! +__conda_setup="$('/home/dgs1/anaconda3/bin/conda' 'shell.bash' 'hook' 2> /dev/null)" +if [ $? -eq 0 ]; then + eval "$__conda_setup" +else + if [ -f "/home/dgs1/anaconda3/etc/profile.d/conda.sh" ]; then + . "/home/dgs1/anaconda3/etc/profile.d/conda.sh" + else + export PATH="/home/dgs1/anaconda3/bin:$PATH" + fi +fi +unset __conda_setup +# <<< conda initialize <<< +conda activate UCL_OP +export PYTHONPATH=$PYTHONPATH:/home/dgs1/Software/OutcomePrediction +export LS_COLORS="$LS_COLORS:ow=1;34:tw=1;34:" +#### ENVIRONMENT VARIABLES ABOVE AUTOMATICALLY ADDED FROM QSUB #### +python Training/Regression.py Configs/SettingsRTOG_Multi11.ini + +### Run your job here +#printenv +#python Training/Preprocess.py ConfigDefault.ini From 8467b925088577cb03448f962f9dae69d38262a6 Mon Sep 17 00:00:00 2001 From: Clara Date: Tue, 14 Mar 2023 04:12:24 +0000 Subject: [PATCH 02/12] before censor --- .../SettingsRTOG_Multi.ini.bak | 0 .../SettingsRTOG_Multi10.ini~ | 0 DataGenerator/DataGenerator.py | 30 ++-- FormMasksLocal.py | 58 +++++++ Models/MixModel.py | 20 ++- Training/Regression.py | 8 +- Training/ValidationR.py | 143 ++++++++++++++++ Utils/GenerateSmoothLabel.py | 159 ++++++++++-------- Utils/PredictionReports.py | 34 ++-- submitjob.pbs | 2 +- submitjob2.pbs | 2 +- 11 files changed, 347 insertions(+), 109 deletions(-) rename Configs/{ => Classification}/SettingsRTOG_Multi.ini.bak (100%) rename Configs/{ => Classification}/SettingsRTOG_Multi10.ini~ (100%) create mode 100644 FormMasksLocal.py create mode 100644 Training/ValidationR.py diff --git a/Configs/SettingsRTOG_Multi.ini.bak b/Configs/Classification/SettingsRTOG_Multi.ini.bak similarity index 100% rename from Configs/SettingsRTOG_Multi.ini.bak rename to Configs/Classification/SettingsRTOG_Multi.ini.bak diff --git a/Configs/SettingsRTOG_Multi10.ini~ b/Configs/Classification/SettingsRTOG_Multi10.ini~ similarity index 100% rename from Configs/SettingsRTOG_Multi10.ini~ rename to Configs/Classification/SettingsRTOG_Multi10.ini~ diff --git a/DataGenerator/DataGenerator.py b/DataGenerator/DataGenerator.py index 7f85528..2478240 100644 --- a/DataGenerator/DataGenerator.py +++ b/DataGenerator/DataGenerator.py @@ -47,7 +47,7 @@ def __getitem__(self, i): if 'CT' in self.keys: CTPath = self.SubjectList.loc[i, 'CT_Path'] if self.config['DATA']['Nifty']: - CTPath = Path(CTPath, 'ct.nii.gz') + CTPath = Path(CTPath, 'CT.nii.gz') data['CT'], meta['CT'] = LoadImage()(CTPath) else: data['CT'], meta['CT'] = LoadImage()(CTPath) @@ -63,7 +63,7 @@ def __getitem__(self, i): if 'Dose' in self.keys: DosePath = self.SubjectList.loc[i, 'Dose_Path'] if self.config['DATA']['Nifty']: - DosePath = Path(DosePath, 'dose.nii.gz') + DosePath = Path(DosePath, 'Dose.nii.gz') data['Dose'], meta['Dose'] = LoadImage()(DosePath) data['Dose'] = data['Dose'] / 67 if not self.config['DATA']['Nifty']: @@ -80,7 +80,7 @@ def __getitem__(self, i): if 'Structs' in self.keys: RSPath = self.SubjectList.loc[i, 'Structs_Path'] if self.config['DATA']['Nifty']: - data['Structs'], meta['Structs'] = LoadImage()(Path(RSPath, 'masks.nii.gz')) + data['Structs'], meta['Structs'] = LoadImage()(Path(RSPath, 'maskswc.nii.gz')) # for roi in self.config['DATA']['Structs']: # data['Struct_' + roi], meta['Struct_' + roi] = LoadImage()(Path(RSPath,roi+'.nii.gz')) @@ -144,11 +144,15 @@ def __getitem__(self, i): if self.inference: return data else: - label = torch.tensor( - np.float(self.SubjectList.loc[i, "xnat_subjectdata_field_map_" + self.config['DATA']['target']])) - if self.config['DATA']['threshold'] is not None: label = torch.where( - label > self.config['DATA']['threshold'], 1, 0) - label = torch.as_tensor(label, dtype=torch.float32) + label_time = torch.tensor(np.float(self.SubjectList.loc[i, "xnat_subjectdata_field_map_" + self.config['DATA']['target']])) + if 'threshold' in self.config['DATA'].keys(): + label_time = torch.where(label_time > self.config['DATA']['threshold'], 1, 0) + label = torch.as_tensor(label_time, dtype=torch.float32) + label = [label] + else: + censored_label = not(np.int8(self.SubjectList.loc[i,'censor_label']).astype('bool')) + label_time = torch.as_tensor(label_time, dtype=torch.float32) + label = (censored_label, label_time) return data, label @@ -262,10 +266,10 @@ def QuerySubjectInfo(config, SubjectList, session): for i in range(len(SubjectList)): subject_label = SubjectList.loc[i, 'subject_label'] for key in config['MODALITY'].keys(): - #if key == 'Structs': - # SubjectList.loc[i, key + '_Path'] = Path(config['DATA']['DataFolder'], subject_label, 'struct_TS') - #else: - SubjectList.loc[i, key + '_Path'] = Path(config['DATA']['DataFolder'], subject_label) + if key == 'Structs': + SubjectList.loc[i, key + '_Path'] = Path(config['DATA']['DataFolder'], subject_label, 'struct_TS') + else: + SubjectList.loc[i, key + '_Path'] = Path(config['DATA']['DataFolder'], subject_label) else: with ThreadPoolExecutor(max_workers=10) as executor: future_to_url = {executor.submit(get_subject_info, config, session, subjectid) for subjectid in @@ -331,5 +335,7 @@ def LoadClinicalData(config, PatientList): df_trans['xnat_subjectdata_field_map_' + target] = PatientList.loc[:, 'xnat_subjectdata_field_map_' + target] df_trans['subject_label'] = PatientList.loc[:, 'subject_label'] df_trans['subjectid'] = PatientList.loc[:, 'subjectid'] + if 'censor_label' in config['Records'].keys(): + df_trans['censor_label'] = PatientList.loc[:, 'xnat_subjectdata_field_map_'+config['Records']['censor_label'][0]] return df_trans, clinical_col diff --git a/FormMasksLocal.py b/FormMasksLocal.py new file mode 100644 index 0000000..656e01a --- /dev/null +++ b/FormMasksLocal.py @@ -0,0 +1,58 @@ +import torch +import torchvision +from torch import nn +from pytorch_lightning.callbacks import ModelCheckpoint, EarlyStopping +from pytorch_lightning.strategies import DDPStrategy +from pytorch_lightning import LightningDataModule, LightningModule, Trainer, seed_everything +import sys, os +#import torchio as tio +import monai +torch.cuda.empty_cache() +## Module - Dataloaders +from DataGenerator.DataGenerator import * +from Models.Classifier import Classifier +from Models.Linear import Linear +from Models.MixModel import MixModel +from monai.transforms import EnsureChannelFirstd, ScaleIntensityd, ResampleToMatchd +## Main +from sklearn.preprocessing import StandardScaler, OneHotEncoder +import toml +from Utils.GenerateSmoothLabel import get_smoothed_label_distribution, get_module +from Utils.PredictionReports import PredictionReports +from pathlib import Path +#import torchio as tio +from torchmetrics import ConfusionMatrix +import torchmetrics +import nibabel as nib + +config = toml.load(sys.argv[1]) +## First Connect to XNAT +path = '/home/dgs1/data/Nifty_Data/RTOG0617/test' +subjects = os.listdir(path) +for i in range(0, len(subjects)): + patient_label = subjects[i] + print(i) + print(patient_label) + RSPath = Path(path, patient_label, 'struct_TS') + for idx, roi in enumerate(config['DATA']['Structs']): + try: + data, meta = LoadImage()(Path(RSPath, roi + '.nii.gz')) + #print(data.shape) + if idx == 0: + masks_img = np.zeros_like(data) + except: + raise ValueError(patient_label + " has no ROI of name " + roi + " found in RTStruct") + masks_img = BitSet(masks_img, idx * np.ones_like(masks_img), data) + + ni_img = nib.Nifti1Image(np.double(masks_img), affine=meta['affine']) + nib.save(ni_img, Path(RSPath, 'maskswc.nii.gz')) + + + +def BitSet(n, p, b): + p = p.astype(int) + n = n.astype(int) + b = b.astype(int) + mask = 1 << p + bm = b << p + return (n & ~mask) | bm diff --git a/Models/MixModel.py b/Models/MixModel.py index c8d55c5..32c3eb8 100644 --- a/Models/MixModel.py +++ b/Models/MixModel.py @@ -14,8 +14,8 @@ def __init__(self, module_dict, config, loss_fcn=torch.nn.BCEWithLogitsLoss()): self.module_dict = module_dict out_feat = np.sum([model.out_feat for model in module_dict.values()]) self.config = config - self.loss_fcn = getattr(torch.nn, self.config["MODEL"]["Loss_Function"])(pos_weight=torch.tensor(1.31)) - self.activation = getattr(torch.nn, self.config["MODEL"]["Activation"])() + self.loss_fcn = getattr(torch.nn, self.config["MODEL"]["Loss_Function"])()#(pos_weight=torch.tensor(1.31)) + self.activation = getattr(torch.nn, self.config["MODEL"]["Activation"])(1, 1) self.classifier = nn.Sequential( nn.Linear(out_feat, 120), nn.Dropout(0.3), @@ -31,14 +31,14 @@ def forward(self, data_dict): return prediction def training_step(self, batch, batch_idx): - out = {} + #out = {} data_dict, label = batch prediction = self.forward(data_dict) loss = self.loss_fcn(prediction.squeeze(dim=1), label) self.log("train_loss", loss, on_step=False, on_epoch=True, sync_dist=True) MAE = torch.abs(prediction.flatten(0) - label) - out['MAE'] = MAE.detach() out = copy.deepcopy(data_dict) + out['MAE'] = MAE.detach() out['prediction'] = prediction.detach() out['label'] = label out['loss'] = loss @@ -50,15 +50,16 @@ def training_epoch_end(self, step_outputs): self.logger.report_epoch(prediction, labels, step_outputs,self.current_epoch, 'train_epoch_') def validation_step(self, batch, batch_idx): - print('val step') - out = {} + #print('val step') + #out = {} data_dict, label = batch prediction = self.forward(data_dict) loss = self.loss_fcn(prediction.squeeze(dim=1), label) self.log("val_loss", loss, on_step=False, on_epoch=True, sync_dist=True) MAE = torch.abs(prediction.flatten(0) - label) - out['MAE'] = MAE + #out['MAE'] = MAE out = copy.deepcopy(data_dict) + out['MAE'] = MAE.detach() out['prediction'] = prediction out['label'] = label out['loss'] = loss @@ -73,10 +74,11 @@ def test_step(self, batch, batch_idx): data_dict, label = batch prediction = self.forward(data_dict) loss = self.loss_fcn(prediction.squeeze(dim=1), label) - out = {} + #out = {} MAE = torch.abs(prediction.flatten(0) - label) - out['MAE'] = MAE + #out['MAE'] = MAE out = copy.deepcopy(data_dict) + out['MAE'] = MAE.detach() out['prediction'] = prediction.squeeze(dim=1) out['label'] = label out['loss'] = loss diff --git a/Training/Regression.py b/Training/Regression.py index 16c078f..e3b3f25 100644 --- a/Training/Regression.py +++ b/Training/Regression.py @@ -82,12 +82,12 @@ QuerySubjectInfo(config, SubjectList, session) print(SubjectList) -threshold = config['DATA']['threshold'] +# threshold = config['DATA']['threshold'] # ckpt_path = Path('./lightning_logs', total_backbone, 'ckpt') rd = [53414, 88536, 89901, 62594, 13787, 21781, 18215, 4182, 10695, 61645, 93967, 35446, 41063, 98435, 94558, 67665, 98831, 76684, 33670, 66239, 24417, 29551, 68018, 52785, 41160, 60264, 75053, 58354, 55180, 58358, 51182, 8260] -for iter in range(0, 32, 1): +for iter in range(0, 20, 1): seed_everything(rd[iter]) # seed_everything(42, workers=True) dataloader = DataModule(SubjectList, @@ -103,8 +103,8 @@ #full_ckpt_path = Path(ckpt_path, 'Iter_'+ str(iter) + '.ckpt') #model.load_state_dict(torch.load(full_ckpt_path)['state_dict']) - filename = 'random_seed_75' - logger = PredictionReports(config=config, save_dir='lightning_logs', name=filename) + filename = config['DATA']['LogFolder'] + logger = PredictionReports(config=config, save_dir='lightning_logs/Regression', name=filename) logger.log_text() logger._version = iter callbacks = [ diff --git a/Training/ValidationR.py b/Training/ValidationR.py new file mode 100644 index 0000000..24a3088 --- /dev/null +++ b/Training/ValidationR.py @@ -0,0 +1,143 @@ +import torch +import torchvision +from torch import nn +from pytorch_lightning.callbacks import ModelCheckpoint, EarlyStopping +from pytorch_lightning.strategies import DDPStrategy +from pytorch_lightning import LightningDataModule, LightningModule, Trainer, seed_everything +import sys, os +# import torchio as tio +import monai +from Utils.GenerateSmoothLabel import generate_cumulative_dynamic_auc +torch.cuda.empty_cache() +## Module - Dataloaders +from DataGenerator.DataGenerator import * +from Models.Classifier import Classifier +from Models.Linear import Linear +from Models.MixModel import MixModel +from monai.transforms import EnsureChannelFirstd, ScaleIntensityd, ResampleToMatchd +## Main +from sklearn.preprocessing import StandardScaler, OneHotEncoder +import toml +from Utils.GenerateSmoothLabel import get_smoothed_label_distribution, get_module +from Utils.PredictionReports import PredictionReports +from pathlib import Path +from Utils.PredictionReports import c_index, r2_index +# import torchio as tio +from torchmetrics import ConfusionMatrix +import torchmetrics +config = toml.load(sys.argv[1]) +## 2D transform +img_keys = list(config['MODALITY'].keys()) +# img_keys.remove('Structs') +# if 'Structs' in config['DATA'].keys(): +# for roi in config['DATA']['Structs']: +# img_keys.append('Struct_' + roi) + +train_transform = torchvision.transforms.Compose([ + EnsureChannelFirstd(keys=img_keys), + # ResampleToMatchd(list(set(img_keys).difference(set(['CT']))), key_dst='CT'), + monai.transforms.ScaleIntensityd(keys=img_keys), + # monai.transforms.ResizeWithPadOrCropd(keys=img_keys, spatial_size=config['DATA']['dim']), + monai.transforms.Resized(keys=img_keys, spatial_size=config['DATA']['dim']), + monai.transforms.RandAffined(keys=img_keys), + monai.transforms.RandHistogramShiftd(keys=img_keys), + monai.transforms.RandAdjustContrastd(keys=img_keys), + monai.transforms.RandGaussianNoised(keys=img_keys), + +]) + +val_transform = torchvision.transforms.Compose([ + EnsureChannelFirstd(keys=img_keys), + # ResampleToMatchd(list(set(img_keys).difference(set(['CT']))), key_dst='CT'), + monai.transforms.ScaleIntensityd(img_keys), + # monai.transforms.ResizeWithPadOrCropd(img_keys, spatial_size=config['DATA']['dim']), + monai.transforms.Resized(keys=img_keys, spatial_size=config['DATA']['dim']), +]) + +## First Connect to XNAT +session = xnat.connect(config['SERVER']['Address'], user=config['SERVER']['User'], + password=config['SERVER']['Password']) + +SubjectList = QuerySubjectList(config, session) +SynchronizeData(config, SubjectList) +SubjectList.dropna(subset=['xnat_subjectdata_field_map_survival_months'], inplace=True) + +module_dict = nn.ModuleDict() +if config['DATA']['Multichannel']: ## Single-Model Multichannel learning + if config['MODALITY'].keys(): + module_dict['Image'] = Classifier(config, 'Image') +else: + for key in config['MODALITY'].keys(): # Multi-Model Single Channel learning + module_dict[key] = Classifier(config, key) + +if 'Records' in config.keys(): + SubjectList, clinical_cols = LoadClinicalData(config, SubjectList) + module_dict['Records'] = Linear(in_feat=len(clinical_cols), out_feat=42) +else: + clinical_cols = None + +print('clinical_cols:', len(clinical_cols)) +## GeneratePath +for key in config['MODALITY'].keys(): + SubjectList[key + '_Path'] = "" +QuerySubjectInfo(config, SubjectList, session) + +cindexs =[] +prediction_labels_full_list = [] +# bidx = [21, 25, 0, 4, 6] +auroc = torchmetrics.AUROC(num_classes=1) +for iter in range(0, 20, 1): + # iter = bidx[it] + # seed_everything(4200) + dataloader = DataModule(SubjectList, + config=config, + keys=config['MODALITY'].keys(), + train_transform=train_transform, + val_transform=val_transform, + clinical_cols=clinical_cols, + inference=False, + train_size=0.85) + + logger = PredictionReports(config=config, save_dir='lightning_logs/test', name= config['DATA']['LogFolder']) + model = MixModel(module_dict, config) + filename = 'lightning_logs/Regression/' + config['DATA']['LogFolder'] + '/version_' + str(iter) + full_ckpt_path = Path(filename, 'Iter_' + str(iter) + '.ckpt') + # full_ckpt_path = Path(ckpt_path, 'ckpt', 'Iter_' + str(iter) + '.ckpt') + # full_ckpt_path = 'Iter_' + str(iter) + '.ckpt' + model.load_state_dict(torch.load(full_ckpt_path)['state_dict']) + # model.load_state_dict(torch.load(full_ckpt_path, map_location='cpu')['state_dict']) + model.eval() + print('start testing...') + worstCase = 0 + with torch.no_grad(): + outs = [] + for i, data in enumerate(dataloader.test_dataloader()): + truth = data[1] + x = data[0] + output = model.test_step(data, i) + outs.append(output) + + validation_labels_full = torch.cat([out['label'] for i, out in enumerate(outs)], dim=0) + prediction_labels_full = torch.cat([out['prediction'] for i, out in enumerate(outs)], dim=0) + cindex = c_index(prediction_labels_full, validation_labels_full.int()) + print('cindex_' + str(iter), cindex) + cindexs.append(cindex[0]) + roc_i = auroc(torch.where(prediction_labels_full > 24, 1, 0), torch.where(validation_labels_full > 24, 1, 0)) + print('roc_' + str(iter), roc_i) + prediction_labels_full_list.append(prediction_labels_full.tolist()) + logger.report_test(config, outs, model, prediction_labels_full, validation_labels_full, 'test_') + +prediction_labels = torch.tensor(prediction_labels_full_list).mean(dim=0) +validation_labels = validation_labels_full + +fig_path = 'lightning_logs/test/' + config['DATA']['LogFolder'] +generate_cumulative_dynamic_auc(prediction_labels, validation_labels, fig_path) + +cindex_tol = c_index(prediction_labels, validation_labels) +r2_tol = r2_index(prediction_labels, validation_labels) +print('cindex series', cindexs) +print('avg_roc', str(cindex_tol)) +print('avg_r2', str(r2_tol)) + +auroc = auroc(torch.where(prediction_labels > 24, 1, 0), torch.where(validation_labels > 24, 1, 0)) +print('auroc', auroc) diff --git a/Utils/GenerateSmoothLabel.py b/Utils/GenerateSmoothLabel.py index 90af2f6..0c6b8b4 100644 --- a/Utils/GenerateSmoothLabel.py +++ b/Utils/GenerateSmoothLabel.py @@ -1,66 +1,93 @@ -import numpy as np -import matplotlib.pyplot as plt -from scipy.ndimage import convolve1d -from scipy.ndimage import gaussian_filter1d -from scipy.signal.windows import triang -from sksurv.metrics import cumulative_dynamic_auc -from torch import nn -from Models.Classifier import Classifier -from Models.Linear import Linear - -def get_lds_kernel_window(kernel, ks, sigma): - assert kernel in ['gaussian', 'triang', 'laplace'] - half_ks = (ks - 1) // 2 - if kernel == 'gaussian': - base_kernel = [0.] * half_ks + [1.] + [0.] * half_ks - kernel_window = gaussian_filter1d(base_kernel, sigma=sigma) / max(gaussian_filter1d(base_kernel, sigma=sigma)) - elif kernel == 'triang': - kernel_window = triang(ks) - else: - laplace = lambda x: np.exp(-abs(x) / sigma) / (2. * sigma) - kernel_window = list(map(laplace, np.arange(-half_ks, half_ks + 1))) / max( - map(laplace, np.arange(-half_ks, half_ks + 1))) - - return kernel_window - - -def get_smoothed_label_distribution(SubjectList, config): - label_all = get_train_label(SubjectList, config) - range_max = np.max(label_all).astype(int) + 1 - range_min = np.min(label_all).astype(int) - - label_range = np.arange(range_min, range_max, 1) - - bin_index_per_label = np.histogram(label_all, bins=label_range) - lds_kernel_window = get_lds_kernel_window(kernel='gaussian', ks=7, sigma=3) - eff_label_dist = convolve1d(np.array(bin_index_per_label[0]), weights=lds_kernel_window, mode='constant') - - eff_num_per_label = [eff_label_dist[bin_idx] for bin_idx in np.arange(eff_label_dist.shape[0])] - weights = [np.float32(1 / x) for x in eff_num_per_label] - - label_mean = np.mean(label_all) - mse = ((label_all - label_mean) ** 2).mean() - return weights, bin_index_per_label[1] - - -def get_train_label(SubjectList, config): - train_label = [] - for patient in SubjectList: - label = patient.fields[config['DATA']['target']] - train_label.append(label) - return train_label - - -def get_module(config): - s_module = config['DATA']['module'] - module_dict = nn.ModuleDict() - if config['MODEL']['Clinical_Backbone']: - Clinical_backbone = Linear() - for i, module in enumerate(s_module): - if module == 'CT' or module == 'Dose' or module == 'PET': - Backbone = Classifier(config) - module_dict[module] = Backbone - else: - module_dict[module] = Clinical_backbone - - return module_dict +import numpy as np +import matplotlib.pyplot as plt +from scipy.ndimage import convolve1d +from scipy.ndimage import gaussian_filter1d +from scipy.signal.windows import triang +from sksurv.metrics import cumulative_dynamic_auc +from torch import nn +from Models.Classifier import Classifier +from Models.Linear import Linear +from pathlib import Path + +def get_lds_kernel_window(kernel, ks, sigma): + assert kernel in ['gaussian', 'triang', 'laplace'] + half_ks = (ks - 1) // 2 + if kernel == 'gaussian': + base_kernel = [0.] * half_ks + [1.] + [0.] * half_ks + kernel_window = gaussian_filter1d(base_kernel, sigma=sigma) / max(gaussian_filter1d(base_kernel, sigma=sigma)) + elif kernel == 'triang': + kernel_window = triang(ks) + else: + laplace = lambda x: np.exp(-abs(x) / sigma) / (2. * sigma) + kernel_window = list(map(laplace, np.arange(-half_ks, half_ks + 1))) / max( + map(laplace, np.arange(-half_ks, half_ks + 1))) + + return kernel_window + + +def get_smoothed_label_distribution(SubjectList, config): + label_all = get_train_label(SubjectList, config) + range_max = np.max(label_all).astype(int) + 1 + range_min = np.min(label_all).astype(int) + + label_range = np.arange(range_min, range_max, 1) + + bin_index_per_label = np.histogram(label_all, bins=label_range) + lds_kernel_window = get_lds_kernel_window(kernel='gaussian', ks=7, sigma=3) + eff_label_dist = convolve1d(np.array(bin_index_per_label[0]), weights=lds_kernel_window, mode='constant') + + eff_num_per_label = [eff_label_dist[bin_idx] for bin_idx in np.arange(eff_label_dist.shape[0])] + weights = [np.float32(1 / x) for x in eff_num_per_label] + + label_mean = np.mean(label_all) + mse = ((label_all - label_mean) ** 2).mean() + return weights, bin_index_per_label[1] + + +def get_train_label(SubjectList, config): + train_label = [] + for patient in SubjectList: + label = patient.fields[config['DATA']['target']] + train_label.append(label) + return train_label + + +def get_module(config): + s_module = config['DATA']['module'] + module_dict = nn.ModuleDict() + if config['MODEL']['Clinical_Backbone']: + Clinical_backbone = Linear() + for i, module in enumerate(s_module): + if module == 'CT' or module == 'Dose' or module == 'PET': + Backbone = Classifier(config) + module_dict[module] = Backbone + else: + module_dict[module] = Clinical_backbone + + return module_dict + + +def generate_cumulative_dynamic_auc(prediction, label, path) -> None: + # this function has issues + risk_score = 1 / prediction + va_times = np.arange(int(label.cpu().min()) + 1, label.cpu().max(), 1) + + dtypes = np.dtype([('event', np.bool_), ('time', np.float)]) + construct_test = np.ndarray(shape=(len(label),), dtype=dtypes) + for i in range(len(label)): + construct_test[i] = (True, label[i].cpu().numpy()) + + cph_auc, cph_mean_auc = cumulative_dynamic_auc( + construct_test, construct_test, risk_score.cpu().squeeze(), va_times + ) + + fig = plt.figure() + plt.plot(va_times, cph_auc, marker="o") + plt.axhline(cph_mean_auc, linestyle="--") + plt.ylim([0, 1]) + plt.xlabel("survival months") + plt.ylabel("time-dependent AUC") + plt.grid(True) + #plt.show() + plt.savefig(Path(path, 't_auroc.jpg')) + diff --git a/Utils/PredictionReports.py b/Utils/PredictionReports.py index 02bfeb2..e636bc5 100644 --- a/Utils/PredictionReports.py +++ b/Utils/PredictionReports.py @@ -6,7 +6,7 @@ import numpy as np import matplotlib.pyplot as plt -# plt.switch_backend('agg') +plt.switch_backend('agg') import torchvision from pytorch_lightning.loggers import LightningLoggerBase from sksurv.metrics import cumulative_dynamic_auc @@ -74,7 +74,9 @@ def log_metrics(self, metrics: Dict[str, float], step: Optional[int] = None): self.experiment.add_scalar(k, v, step) def log_image(self, img, text, current_epoch=None): - img_batch = img.view(img.shape[0] * img.shape[1], *[1, img.shape[2], img.shape[3]]) + img = img.transpose(2, 0) + img_batch = img.view(img.shape[0], *[1, img.shape[1], img.shape[2]]) + #img_batch = img.view(img.shape[0] * img.shape[1], *[1, img.shape[2], img.shape[3]]) grid = torchvision.utils.make_grid(img_batch) self.experiment.add_image(text, grid, current_epoch) return grid @@ -103,7 +105,7 @@ def classification_matrix(self, prediction, label, prefix): tp = bcm[1][1] fp = bcm[0][1] fn = bcm[1][0] - if 'ROC' in self.config['CHECKPOINT']['matrix']: + if 'AUC' in self.config['CHECKPOINT']['matrix']: auroc = torchmetrics.AUROC() accuracy = auroc(prediction, label.int()) c_out[prefix + 'roc'] = accuracy @@ -126,16 +128,16 @@ def classification_matrix(self, prediction, label, prefix): def generate_cumulative_dynamic_auc(self, prediction, label, current_epoch, prefix) -> None: # this function has issues - risk_score = 1 / prediction - va_times = np.arange(int(label.cpu().min()) + 1, label.cpu().max(), 1) - + risk_score = 1/prediction + #va_times = np.arange(int(label.cpu().min()) + 1, label.cpu().max(), 1) + va_times = np.percentile(label.cpu(), np.linspace(5, 81, 20)) dtypes = np.dtype([('event', np.bool_), ('time', np.float)]) construct_test = np.ndarray(shape=(len(label),), dtype=dtypes) for i in range(len(label)): construct_test[i] = (True, label[i].cpu().numpy()) cph_auc, cph_mean_auc = cumulative_dynamic_auc( - construct_test, construct_test, risk_score.cpu(), va_times + construct_test, construct_test, risk_score.cpu().squeeze(), va_times ) fig = plt.figure() @@ -168,15 +170,15 @@ def worst_case_show(self, validation_step_outputs, prefix): loss = data['MAE'] idx = torch.argmax(loss) if loss[idx] > worst_AE: - if 'CT' in self.config['DATA']['target']: - worst_img = data['CT'][idx] - if 'Dose' in self.config['DATA']['target']: - worst_dose = data['dose'][idx] + if 'CT' in self.config['MODALITY'].keys(): + worst_img = data['Image'][idx][0,:,:,:] + if 'Dose' in self.config['MODALITY'].keys(): + worst_dose = data['Image'][idx][1,:,:,:] worst_AE = loss[idx] out[prefix + 'worst_AE'] = worst_AE - if 'CT' in self.config['DATA']['target']: + if 'CT' in self.config['MODALITY'].keys(): out[prefix + 'worst_img'] = worst_img - if 'Dose' in self.config['DATA']['target']: + if 'Dose' in self.config['MODALITY'].keys(): out[prefix + 'worst_dose'] = worst_dose return out @@ -205,10 +207,10 @@ def report_epoch(self, prediction, label, validation_step_outputs, if 'WorstCase' in self.config['CHECKPOINT']['matrix']: worst_record = self.worst_case_show(validation_step_outputs, prefix) self.log_metrics({prefix + 'worst_AE': worst_record[prefix + 'worst_AE']}, current_epoch) - if 'CT' in self.config['DATA']['target']: + if 'CT' in self.config['MODALITY'].keys(): text = 'validate_worst_case_img' self.log_image(worst_record[prefix + 'worst_img'], text, current_epoch) - if 'Dose' in self.config['DATA']['target']: + if 'Dose' in self.config['MODALITY'].keys(): text = 'validate_worst_case_dose' self.log_image(worst_record[prefix + 'worst_dose'], text, current_epoch) @@ -244,7 +246,7 @@ def report_test(self, config, outs, model, prediction_labels, validation_labels, self.experiment.add_text('Specificity:', str(classification_out[prefix + 'accuracy'])) if 'Precision' in config['CHECKPOINT']['matrix']: self.experiment.add_text('Specificity:', str(classification_out[prefix + 'precision'])) - if 'ROC' in config['CHECKPOINT']['matrix']: + if 'AUC' in config['CHECKPOINT']['matrix']: self.experiment.add_text('ROC:', str(classification_out[prefix + 'roc'])) return classification_out diff --git a/submitjob.pbs b/submitjob.pbs index 312cb73..63f4ddd 100644 --- a/submitjob.pbs +++ b/submitjob.pbs @@ -32,7 +32,7 @@ conda activate UCL_OP export PYTHONPATH=$PYTHONPATH:/home/dgs1/Software/OutcomePrediction export LS_COLORS="$LS_COLORS:ow=1;34:tw=1;34:" #### ENVIRONMENT VARIABLES ABOVE AUTOMATICALLY ADDED FROM QSUB #### -python Training/Regression.py Configs/SettingsRTOG_Multi10.ini +python Training/Regression.py Configs/Regression/SettingsRTOG_Multi1.ini ### Run your job here #printenv diff --git a/submitjob2.pbs b/submitjob2.pbs index c39afdd..b665698 100644 --- a/submitjob2.pbs +++ b/submitjob2.pbs @@ -32,7 +32,7 @@ conda activate UCL_OP export PYTHONPATH=$PYTHONPATH:/home/dgs1/Software/OutcomePrediction export LS_COLORS="$LS_COLORS:ow=1;34:tw=1;34:" #### ENVIRONMENT VARIABLES ABOVE AUTOMATICALLY ADDED FROM QSUB #### -python Training/Regression.py Configs/SettingsRTOG_Multi11.ini +python Training/Regression.py Configs/Regression/SettingsRTOG_Multi.ini ### Run your job here #printenv From 7a337e207c59f470e5f2254104379943e3d3f82b Mon Sep 17 00:00:00 2001 From: Clara Date: Thu, 30 Mar 2023 01:06:18 +0100 Subject: [PATCH 03/12] all --- .../SettingsRTOG_Multi.ini.bak | 0 .../SettingsRTOG_Multi.ini~} | 30 +- DataGenerator/DataGenerator.py | 62 ++-- DataGenerator/DataGenerator_PTV.py | 346 ++++++++++++++++++ Models/Classifier.py | 15 - Models/MixModel.py | 76 ++-- Training/Classification.py | 144 ++++++++ Training/Regression.py | 57 +-- Training/Validation.py | 154 -------- Training/test.py | 196 ++++++++-- Training/{ValidationR.py => testR.py} | 84 +++-- Utils/PredictionReports.py | 114 +++--- submitjob.pbs | 4 +- submitjob2.pbs | 2 +- 14 files changed, 883 insertions(+), 401 deletions(-) rename Configs/Classification/{ => old_settings}/SettingsRTOG_Multi.ini.bak (100%) rename Configs/{Classification/SettingsRTOG_Multi10.ini~ => Regression/SettingsRTOG_Multi.ini~} (67%) create mode 100644 DataGenerator/DataGenerator_PTV.py create mode 100644 Training/Classification.py delete mode 100644 Training/Validation.py rename Training/{ValidationR.py => testR.py} (60%) diff --git a/Configs/Classification/SettingsRTOG_Multi.ini.bak b/Configs/Classification/old_settings/SettingsRTOG_Multi.ini.bak similarity index 100% rename from Configs/Classification/SettingsRTOG_Multi.ini.bak rename to Configs/Classification/old_settings/SettingsRTOG_Multi.ini.bak diff --git a/Configs/Classification/SettingsRTOG_Multi10.ini~ b/Configs/Regression/SettingsRTOG_Multi.ini~ similarity index 67% rename from Configs/Classification/SettingsRTOG_Multi10.ini~ rename to Configs/Regression/SettingsRTOG_Multi.ini~ index 8834c22..91f4779 100644 --- a/Configs/Classification/SettingsRTOG_Multi10.ini~ +++ b/Configs/Regression/SettingsRTOG_Multi.ini~ @@ -1,22 +1,21 @@ [MODEL] -Activation = 'Identity' +Activation = 'ReLU' Backbone = 'DenseNet' Records_Backbone = 'Linear' batch_size = 10 -Prediction_type = 'Classification' +Prediction_type = 'Regression' CT_spatial_dims = 3 -Loss_Function = 'BCEWithLogitsLoss' +Loss_Function = 'MSELoss' [MODEL_PARAMETERS] spatial_dims = 3 -in_channels = 3 +in_channels = 2 out_channels = 512 block_config = [1,2,4,2] dropout_prob = 0.3 [MODALITY] CT = '1' -Dose = '1' Structs = '1' [DATA] @@ -24,27 +23,20 @@ Nifty = true n_classes = 1 Multichannel = true dim = [128,128,32] -threshold = 24 -Structs = ['esophagus','lung', 'heart', 'ptv'] -DataFolder = '/home/dgs1/data/Segmentation/nii_root_folder/RTOG/' +Structs = ['esophagus','pulmonary_artery', 'lung', 'lung_vessels', 'heart_atrium_left', 'heart_atrium_right', + 'heart_ventricle_left', 'heart_ventricle_right', 'heart_myocardium', 'coronary_arteries' ,'AI_Target'] +DataFolder = '/home/dgs1/data/Nifty_Data/RTOG0617/' +LogFolder = 'Regression/random_seed_75_CT' vis = [0] train_size = 0.7 val_size = 0.3 target = 'survival_months' - -[Records] -category_feat = ['arm', 'gender', 'race', 'ethnicity', 'zubrod', - 'histology', 'nonsquam_squam', 'ajcc_stage_grp', 'rt_technique', - 'smoke_hx', 'rx_terminated_ae', 'received_conc_chemo','rt_compliance_ptv90', - 'received_cons_chemo','rt_dose','pet_staging','received_rt'] -numerical_feat = ['age', 'volume_ptv','v5_lung','v20_lung','dmean_lung', - 'v5_heart','v30_heart','v20_esophagus','v60_esophagus'] - +censor_label = 'lost_to_followup' [CHECKPOINT] monitor = "val_loss" #"val_acc_epoch" -mode = "max" -matrix = ['ROC', 'Specificity','Sensitivity','Accuracy','Precision'] +mode = "min" +matrix = ['r2','cindex'] [SERVER] diff --git a/DataGenerator/DataGenerator.py b/DataGenerator/DataGenerator.py index 2478240..e19e162 100644 --- a/DataGenerator/DataGenerator.py +++ b/DataGenerator/DataGenerator.py @@ -43,11 +43,12 @@ def __getitem__(self, i): meta = {} subject_id = self.SubjectList.loc[i, 'subjectid'] slabel = self.SubjectList.loc[i, 'subject_label'] + data['slabel'] = slabel ## Load CT if 'CT' in self.keys: CTPath = self.SubjectList.loc[i, 'CT_Path'] if self.config['DATA']['Nifty']: - CTPath = Path(CTPath, 'CT.nii.gz') + CTPath = Path(CTPath, 'ct.nii.gz') data['CT'], meta['CT'] = LoadImage()(CTPath) else: data['CT'], meta['CT'] = LoadImage()(CTPath) @@ -63,9 +64,9 @@ def __getitem__(self, i): if 'Dose' in self.keys: DosePath = self.SubjectList.loc[i, 'Dose_Path'] if self.config['DATA']['Nifty']: - DosePath = Path(DosePath, 'Dose.nii.gz') + DosePath = Path(DosePath, 'dose.nii.gz') data['Dose'], meta['Dose'] = LoadImage()(DosePath) - data['Dose'] = data['Dose'] / 67 + data['Dose'] = data['Dose'] / 67 ## Probably need to make it a variable if not self.config['DATA']['Nifty']: data['Dose'] = data['Dose'] * np.double(meta['Dose']['3004|000e']) / 67 @@ -73,14 +74,14 @@ def __getitem__(self, i): if 'PET' in self.keys: PETPath = self.SubjectList.loc[i, 'PET_Path'] if self.config['DATA']['Nifty']: - PETPath = Path(PETPath, 'dose.nii.gz') + PETPath = Path(PETPath, 'pet.nii.gz') data['PET'], meta['PET'] = LoadImage()(PETPath) ## Load Mask if 'Structs' in self.keys: RSPath = self.SubjectList.loc[i, 'Structs_Path'] if self.config['DATA']['Nifty']: - data['Structs'], meta['Structs'] = LoadImage()(Path(RSPath, 'maskswc.nii.gz')) + data['Structs'], meta['Structs'] = LoadImage()(Path(RSPath, 'masks.nii.gz')) # for roi in self.config['DATA']['Structs']: # data['Struct_' + roi], meta['Struct_' + roi] = LoadImage()(Path(RSPath,roi+'.nii.gz')) @@ -115,8 +116,9 @@ def __getitem__(self, i): masks_img = np.flip(masks_img, 0) masks_img = MetaTensor(masks_img.copy(), meta=meta['CT']) data['Structs'] = masks_img - else: - data['Structs'] = np.ones_like(data['CT']) ## No ROI target defined + #else: + # if 'CT' in self.keys: + # data['Structs'] = np.ones_like(data['CT']) ## No ROI target defined ## Apply transforms on all if self.transform: data = self.transform(data) @@ -129,12 +131,14 @@ def __getitem__(self, i): # for key in data.keys(): # data[key] = get_masked_img_voxel(data[key], data['Mask']) # Decide between multi-branch single-channel/multi-channel single-branch + if self.config['DATA']['Multichannel']: - old_keys = list(data.keys()) - data['Image'] = np.concatenate([data[key] for key in data.keys()], axis=0) + old_keys = list(self.keys) + data['Image'] = np.concatenate([data[key] for key in old_keys], axis=0) for key in old_keys: data.pop(key) else: - data.pop('Structs') ## No need for mask in single-channel multi-branch + if 'Structs' in data.keys(): + data.pop('Structs') ## No need for mask in single-channel multi-branch # data = ResizeWithPadOrCropd(keys=data.keys(), spatial_size=self.config['DATA']['dim'])(data) @@ -143,16 +147,14 @@ def __getitem__(self, i): dtype=torch.float32) if self.inference: return data - else: - label_time = torch.tensor(np.float(self.SubjectList.loc[i, "xnat_subjectdata_field_map_" + self.config['DATA']['target']])) - if 'threshold' in self.config['DATA'].keys(): - label_time = torch.where(label_time > self.config['DATA']['threshold'], 1, 0) - label = torch.as_tensor(label_time, dtype=torch.float32) - label = [label] - else: - censored_label = not(np.int8(self.SubjectList.loc[i,'censor_label']).astype('bool')) - label_time = torch.as_tensor(label_time, dtype=torch.float32) - label = (censored_label, label_time) + else: ##Training + label = torch.tensor(np.float(self.SubjectList.loc[i, "xnat_subjectdata_field_map_" + self.config['DATA']['target']])) + censored_label = not(np.int8(self.SubjectList.loc[i, 'xnat_subjectdata_field_map_' + self.config['DATA']['censor_label']]).astype('bool')) + if 'threshold' in self.config['DATA'].keys(): ## Classification + label = torch.where(label > self.config['DATA']['threshold'], 1, 0) + label = torch.as_tensor(label, dtype=torch.float32) + + label = (censored_label, label) return data, label @@ -178,8 +180,8 @@ def __init__(self, SubjectList, config=None, train_transform=None, val_transform test_list = test_list.reset_index(drop=True) self.train_data = DataGenerator(train_list, config=config, transform=train_transform, **kwargs) - self.val_data = DataGenerator(val_list, config=config, transform=val_transform, **kwargs) - self.test_data = DataGenerator(test_list, config=config, transform=val_transform, **kwargs) + self.val_data = DataGenerator(val_list, config=config, transform=val_transform, **kwargs) + self.test_data = DataGenerator(test_list, config=config, transform=val_transform, **kwargs) def train_dataloader(self): return DataLoader(self.train_data, batch_size=self.batch_size, num_workers=self.num_workers, pin_memory=True, drop_last=True, @@ -201,6 +203,10 @@ def QuerySubjectList(config, session): XML.Add_search_field( {"element_name": "xnat:subjectData", "field_ID": "XNAT_SUBJECTDATA_FIELD_MAP=" + str(config['DATA']['target']), "sequence": "1", "type": "int"}) + if 'censor_label' in config['DATA'].keys(): + XML.Add_search_field( + {"element_name": "xnat:subjectData", "field_ID": "XNAT_SUBJECTDATA_FIELD_MAP=" + str(config['DATA']['censor_label']), + "sequence": "1", "type": "int"}) ## Label XML.Add_search_field( {"element_name": "xnat:subjectData", "field_ID": "SUBJECT_LABEL", "sequence": "1", "type": "string"}) @@ -266,10 +272,10 @@ def QuerySubjectInfo(config, SubjectList, session): for i in range(len(SubjectList)): subject_label = SubjectList.loc[i, 'subject_label'] for key in config['MODALITY'].keys(): - if key == 'Structs': - SubjectList.loc[i, key + '_Path'] = Path(config['DATA']['DataFolder'], subject_label, 'struct_TS') - else: - SubjectList.loc[i, key + '_Path'] = Path(config['DATA']['DataFolder'], subject_label) + #if key == 'Structs': + # SubjectList.loc[i, key + '_Path'] = Path(config['DATA']['DataFolder'], subject_label, 'struct_TS') + #else: + SubjectList.loc[i, key + '_Path'] = Path(config['DATA']['DataFolder'], subject_label) else: with ThreadPoolExecutor(max_workers=10) as executor: future_to_url = {executor.submit(get_subject_info, config, session, subjectid) for subjectid in @@ -335,7 +341,7 @@ def LoadClinicalData(config, PatientList): df_trans['xnat_subjectdata_field_map_' + target] = PatientList.loc[:, 'xnat_subjectdata_field_map_' + target] df_trans['subject_label'] = PatientList.loc[:, 'subject_label'] df_trans['subjectid'] = PatientList.loc[:, 'subjectid'] - if 'censor_label' in config['Records'].keys(): - df_trans['censor_label'] = PatientList.loc[:, 'xnat_subjectdata_field_map_'+config['Records']['censor_label'][0]] + if 'censor_label' in config['DATA'].keys(): + df_trans['xnat_subjectdata_field_map_' + config['DATA']['censor_label']] = PatientList.loc[:, 'xnat_subjectdata_field_map_' + config['DATA']['censor_label']] return df_trans, clinical_col diff --git a/DataGenerator/DataGenerator_PTV.py b/DataGenerator/DataGenerator_PTV.py new file mode 100644 index 0000000..7a670db --- /dev/null +++ b/DataGenerator/DataGenerator_PTV.py @@ -0,0 +1,346 @@ +from pytorch_lightning import LightningDataModule, LightningModule, Trainer, seed_everything +from torch.utils.data import DataLoader +import numpy as np +import torch +from monai.data import MetaTensor +from sklearn.model_selection import train_test_split, StratifiedShuffleSplit +import xnat +import matplotlib.pyplot as plt +from monai.transforms import ( + LoadImage, EnsureChannelFirstd, ResampleToMatchd, ResizeWithPadOrCropd +) +from Utils.DicomTools import * +from Utils.XNATXML import XMLCreator +from io import StringIO +import requests +import pandas as pd +import xmltodict +from sklearn.preprocessing import OneHotEncoder, MinMaxScaler, LabelEncoder, OrdinalEncoder +from rt_utils import RTStructBuilder +from sklearn.compose import ColumnTransformer +from pathlib import Path +from concurrent.futures import ThreadPoolExecutor +import concurrent + + +class DataGenerator(torch.utils.data.Dataset): + def __init__(self, SubjectList, config=None, keys=['CT'], transform=None, inference=False, + clinical_cols=None, session=None, **kwargs): + super().__init__() + self.config = config + self.SubjectList = SubjectList + self.keys = keys + self.transform = transform + self.inference = inference + self.clinical_cols = clinical_cols + + def __len__(self): + return int(self.SubjectList.shape[0]) + + def __getitem__(self, i): + + data = {} + meta = {} + subject_id = self.SubjectList.loc[i, 'subjectid'] + slabel = self.SubjectList.loc[i, 'subject_label'] + data['slabel'] = slabel + ## Load CT + if 'CT' in self.keys: + CTPath = self.SubjectList.loc[i, 'CT_Path'] + if self.config['DATA']['Nifty']: + CTPath = Path(CTPath, 'ct.nii.gz') + data['CT'], meta['CT'] = LoadImage()(CTPath) + else: + data['CT'], meta['CT'] = LoadImage()(CTPath) + CTSession = ReadDicom(CTPath) + CTArray = sitk.GetArrayFromImage(CTSession) + if not (CTArray.shape == data['CT'].shape): + CTArray = CTArray.transpose((2, 1, 0)) + CTArray = np.flip(CTArray, axis=2) + mCT = MetaTensor(CTArray.copy(), meta=meta['CT']) + data['CT'] = mCT + + ## Load Dose + if 'Dose' in self.keys: + DosePath = self.SubjectList.loc[i, 'Dose_Path'] + if self.config['DATA']['Nifty']: + DosePath = Path(DosePath, 'dose.nii.gz') + data['Dose'], meta['Dose'] = LoadImage()(DosePath) + data['Dose'] = data['Dose'] / 67 ## Probably need to make it a variable + if not self.config['DATA']['Nifty']: + data['Dose'] = data['Dose'] * np.double(meta['Dose']['3004|000e']) / 67 + + ## Load PET + if 'PET' in self.keys: + PETPath = self.SubjectList.loc[i, 'PET_Path'] + if self.config['DATA']['Nifty']: + PETPath = Path(PETPath, 'pet.nii.gz') + data['PET'], meta['PET'] = LoadImage()(PETPath) + + ## Load Mask + if 'Structs' in self.keys: + RSPath = self.SubjectList.loc[i, 'Structs_Path'] + if self.config['DATA']['Nifty']: + data['Structs'], meta['Structs'] = LoadImage()(Path(RSPath, self.config['DATA']['Structs'])) + + # for roi in self.config['DATA']['Structs']: + # data['Struct_' + roi], meta['Struct_' + roi] = LoadImage()(Path(RSPath,roi+'.nii.gz')) + # dt = distance_transform_edt(data['Struct_' + roi]) + # data['Struct_' + roi] = MetaTensor(dt, meta = meta['CT']) + + # masks_img = np.zeros_like(data['CT']) + # masks_img = get_nii_masks(slabel, masks_img, RSPath, self.config['DATA']['Structs']) + # masks_img = MetaTensor(masks_img.copy(), meta=meta['CT']) + # data['Structs'] = masks_img + else: + ## mask in multichannels + RS = RTStructBuilder.create_from(dicom_series_path=CTPath, rt_struct_path=RSPath) + # roi_names = RS.get_roi_names() + # for roi in self.config['DATA']['Structs']: + # if roi in roi_names: + # mask_img = RS.get_roi_mask_by_name(roi) + # mask_img = distance_transform_edt(mask_img) + # else: + # message = "No ROI of name " + self.targetROI + " found in RTStruct" + # raise ValueError(message) + # mask_img = np.rot90(mask_img) + # mask_img = np.flip(mask_img, 2) + # mask_img = np.flip(mask_img, 0) + # mask = MetaTensor(mask_img.copy(), meta = meta['CT']) + # data['Struct_' + roi] = mask + + ### masks images + masks_img = np.zeros_like(data['CT']) + masks_img = get_RS_masks(slabel, CTPath, masks_img, RSPath, self.config['DATA']['Structs']) + masks_img = np.rot90(masks_img) + masks_img = np.flip(masks_img, 0) + masks_img = MetaTensor(masks_img.copy(), meta=meta['CT']) + data['Structs'] = masks_img + #else: + # if 'CT' in self.keys: + # data['Structs'] = np.ones_like(data['CT']) ## No ROI target defined + + ## Apply transforms on all + if self.transform: data = self.transform(data) + + # mask_imgs = np.zeros_like(CTArray) + # for key in data.keys(): + # if 'Mask' in key: + # mask_imgs = mask_imgs + data[key] + + # for key in data.keys(): + # data[key] = get_masked_img_voxel(data[key], data['Mask']) + # Decide between multi-branch single-channel/multi-channel single-branch + + if self.config['DATA']['Multichannel']: + old_keys = list(self.keys) + data['Image'] = np.concatenate([data[key] for key in old_keys], axis=0) + for key in old_keys: data.pop(key) + else: + if 'Structs' in data.keys(): + data.pop('Structs') ## No need for mask in single-channel multi-branch + + # data = ResizeWithPadOrCropd(keys=data.keys(), spatial_size=self.config['DATA']['dim'])(data) + + ## Add clinical record at the end + if 'Records' in self.config.keys(): data['Records'] = torch.tensor(self.SubjectList.loc[i, self.clinical_cols], + dtype=torch.float32) + if self.inference: + return data + else: ##Training + label = torch.tensor(np.float(self.SubjectList.loc[i, "xnat_subjectdata_field_map_" + self.config['DATA']['target']])) + censored_label = not(np.int8(self.SubjectList.loc[i, 'xnat_subjectdata_field_map_' + self.config['DATA']['censor_label']]).astype('bool')) + if 'threshold' in self.config['DATA'].keys(): ## Classification + label = torch.where(label > self.config['DATA']['threshold'], 1, 0) + label = torch.as_tensor(label, dtype=torch.float32) + label = (censored_label, label) + return data, label + + +### DataLoader +class DataModule(LightningDataModule): + def __init__(self, SubjectList, config=None, train_transform=None, val_transform=None, train_size=0.85, num_workers=10, **kwargs): + super().__init__() + self.batch_size = config['MODEL']['batch_size'] + self.num_workers = num_workers + data_trans = class_stratify(SubjectList, config) + ## Split Test with fixed seed + train_val_list, test_list = train_test_split(SubjectList, train_size=train_size, random_state=75, + stratify=data_trans) + + data_trans = class_stratify(train_val_list, config) + ## Split train-val with random seed + train_list, val_list = train_test_split(train_val_list, train_size=train_size, + random_state=np.random.randint(10000), + stratify=data_trans) + + train_list = train_list.reset_index(drop=True) + val_list = val_list.reset_index(drop=True) + test_list = test_list.reset_index(drop=True) + + self.train_data = DataGenerator(train_list, config=config, transform=train_transform, **kwargs) + self.val_data = DataGenerator(val_list, config=config, transform=val_transform, **kwargs) + self.test_data = DataGenerator(test_list, config=config, transform=val_transform, **kwargs) + + def train_dataloader(self): return DataLoader(self.train_data, batch_size=self.batch_size, + num_workers=self.num_workers, pin_memory=True, drop_last=True, + shuffle=True) + + def val_dataloader(self): return DataLoader(self.val_data, batch_size=self.batch_size, + num_workers=self.num_workers, pin_memory=True, drop_last=True, + shuffle=False) + + def test_dataloader(self): return DataLoader(self.test_data, batch_size=self.batch_size, + num_workers=self.num_workers, pin_memory=True, drop_last=True) + + +def QuerySubjectList(config, session): + root_element = "xnat:subjectData" + XML = XMLCreator(root_element) # , search_field, search_where) + print("Querying from Server") + ## Target + XML.Add_search_field( + {"element_name": "xnat:subjectData", "field_ID": "XNAT_SUBJECTDATA_FIELD_MAP=" + str(config['DATA']['target']), + "sequence": "1", "type": "int"}) + if 'censor_label' in config['DATA'].keys(): + XML.Add_search_field( + {"element_name": "xnat:subjectData", "field_ID": "XNAT_SUBJECTDATA_FIELD_MAP=" + str(config['DATA']['censor_label']), + "sequence": "1", "type": "int"}) + ## Label + XML.Add_search_field( + {"element_name": "xnat:subjectData", "field_ID": "SUBJECT_LABEL", "sequence": "1", "type": "string"}) + + if 'Records' in config.keys(): + feats_list = [item for sublist in config['Records'].values() for item in sublist] + for value in feats_list: + dict_temp = {"element_name": "xnat:subjectData", "field_ID": "XNAT_SUBJECTDATA_FIELD_MAP=" + str(value), + "sequence": "1", "type": "int"} + XML.Add_search_field(dict_temp) + + ## Where Condition + templist = [] + for value in config['SERVER']['Projects']: + templist.append({"schema_field": "xnat:subjectData.PROJECT", "comparison_type": "=", "value": str(value)}) + if (len(templist)): XML.Add_search_where(templist, "OR") + + templist = [] + for key, value in config['CRITERIA'].items(): + templist.append({"schema_field": "xnat:subjectData.XNAT_SUBJECTDATA_FIELD_MAP=" + key, "comparison_type": "=", + "value": str(value)}) + if (len(templist)): XML.Add_search_where(templist, "AND") ## if any items in here + + templist = [] + for key, value in config['MODALITY'].items(): + templist.append( + {"schema_field": "xnat:ctSessionData.SCAN_COUNT_TYPE=" + key, "comparison_type": ">=", "value": str(value)}) + if (len(templist)): XML.Add_search_where(templist, "AND") ## if any items in here + + templist = [] + if (config['FILTER']): + for value in config['FILTER']['patient_id']: + dict_temp = {"schema_field": "xnat:subjectData.SUBJECT_LABEL", "comparison_type": "!=", "value": str(value)} + + templist.append(dict_temp) + if (len(templist)): XML.Add_search_where(templist, "AND") + + xmlstr = XML.ConstructTree() + response = session.post(config['SERVER']['Address'] + '/data/search/', data=xmlstr, format='csv') + SubjectList = pd.read_csv(StringIO(response.text), dtype=str) + # print('Query: ', SubjectList) + return SubjectList + + +def SynchronizeData(config, SubjectList): + session = xnat.connect(config['SERVER']['Address'], user=config['SERVER']['User'], + password=config['SERVER']['Password']) + for subjectlabel, subjectid in zip(SubjectList['subject_label'], SubjectList['subjectid']): + if (not Path(config['DATA']['DataFolder'], subjectlabel).is_dir()): + xnatsubject = session.create_object('/data/subjects/' + subjectid) + print("Synchronizing ", subjectid, subjectlabel) + xnatsubject.download_dir(config['DATA']['DataFolder']) ## Download data + + +def get_subject_info(config, session, subjectid): + r = session.get(config['SERVER']['Address'] + '/data/subjects/' + subjectid, format='xml') + data = xmltodict.parse(r.text, force_list=True) + return data + + +def QuerySubjectInfo(config, SubjectList, session): + if config['DATA']['Nifty']: + for i in range(len(SubjectList)): + subject_label = SubjectList.loc[i, 'subject_label'] + for key in config['MODALITY'].keys(): + #if key == 'Structs': + # SubjectList.loc[i, key + '_Path'] = Path(config['DATA']['DataFolder'], subject_label, 'struct_TS') + #else: + SubjectList.loc[i, key + '_Path'] = Path(config['DATA']['DataFolder'], subject_label) + else: + with ThreadPoolExecutor(max_workers=10) as executor: + future_to_url = {executor.submit(get_subject_info, config, session, subjectid) for subjectid in + SubjectList['subjectid']} + executor.shutdown(wait=True) + for future in concurrent.futures.as_completed(future_to_url): + subjectdata = future.result() + subjectid = subjectdata["xnat:Subject"][0]["@ID"] + for key in config['MODALITY'].keys(): + path = GeneratePath(subjectdata, Modality=key, config=config) + if key == 'CT': + SubjectList.loc[SubjectList.subjectid == subjectid, key + '_Path'] = path + else: + spath = glob.glob(path + '/*dcm') + SubjectList.loc[SubjectList.subjectid == subjectid, key + '_Path'] = spath[0] + + +def GeneratePath(subjectdata, Modality, config): + subject = subjectdata['xnat:Subject'][0] + subject_label = subject['@label'] + experiments = subject['xnat:experiments'][0]['xnat:experiment'] + + ## Won't work with many experiments yet + for experiment in experiments: + experiment_label = experiment['@label'] + scans = experiment['xnat:scans'][0]['xnat:scan'] + for scan in scans: + if (scan['@type'] in Modality): + scan_label = scan['@ID'] + '-' + scan['@type'] + resources_label = scan['xnat:file'][0]['@label'] + if resources_label == 'SNAPSHOTS': + resources_label = scan['xnat:file'][1]['@label'] + path = os.path.join(config['DATA']['DataFolder'], subject_label, experiment_label, 'scans', + scan_label, 'resources', resources_label, 'files') + return path + + +def LoadClinicalData(config, PatientList): + category_cols = [] + numerical_cols = [] + for col in config['Records']['category_feat']: + category_cols.append('xnat_subjectdata_field_map_' + col) + + for row in config['Records']['numerical_feat']: + numerical_cols.append('xnat_subjectdata_field_map_' + row) + + target = config['DATA']['target'] + + ct = ColumnTransformer( + [("CatTrans", OneHotEncoder(), category_cols), + ("NumTrans", MinMaxScaler(), numerical_cols), ]) + + X = PatientList.loc[:, category_cols + numerical_cols] + yc = X[category_cols].astype('float32') + X[category_cols] = yc.fillna(yc.mean().astype('int')) + yn = X[numerical_cols].astype('float32') + X[numerical_cols] = yn.fillna(yn.mean()) # X.loc[:, numerical_cols] = yn.fillna(yn.mean()) + X_trans = ct.fit_transform(X) + if not isinstance(X_trans, (np.ndarray, np.generic)): X_trans = X_trans.toarray() + + df_trans = pd.DataFrame(X_trans, index=X.index, columns=ct.get_feature_names_out()) + clinical_col = list(df_trans.columns) + df_trans['xnat_subjectdata_field_map_' + target] = PatientList.loc[:, 'xnat_subjectdata_field_map_' + target] + df_trans['subject_label'] = PatientList.loc[:, 'subject_label'] + df_trans['subjectid'] = PatientList.loc[:, 'subjectid'] + if 'censor_label' in config['DATA'].keys(): + df_trans['xnat_subjectdata_field_map_' + config['DATA']['censor_label']] = PatientList.loc[:, 'xnat_subjectdata_field_map_' + config['DATA']['censor_label']] + return df_trans, clinical_col + diff --git a/Models/Classifier.py b/Models/Classifier.py index 0f254a7..5d2ddbc 100644 --- a/Models/Classifier.py +++ b/Models/Classifier.py @@ -23,25 +23,10 @@ def __init__(self, config, module_str): model_str = 'nets.' + model + '(**parameters)' self.backbone = eval(model_str) - #layers = list(self.backbone.children())[:-1] - # self.model = nn.Sequential(*layers) - - # self.flatten = nn.Sequential( - # # nn.Dropout(0.3), - # # nn.AdaptiveAvgPool3d(output_size=(4, 4, 4)), - # nn.Dropout(0.3), - # nn.AdaptiveAvgPool3d(output_size=(1, 1, 1)), - # nn.Flatten(), - # ) - # self.model.apply(self.weights_init) self.out_feat = config['MODEL_PARAMETERS']['out_channels'] - self.accuracy = torchmetrics.AUROC(task="binary") - self.loss_fcn = torch.nn.BCEWithLogitsLoss() def forward(self, x): return self.backbone(x) - # features = self.model(x) - # return self.flatten(features) def weights_init(self, m): if isinstance(m, nn.Conv3d) or isinstance(m, nn.Linear): diff --git a/Models/MixModel.py b/Models/MixModel.py index 32c3eb8..6affdcf 100644 --- a/Models/MixModel.py +++ b/Models/MixModel.py @@ -14,8 +14,8 @@ def __init__(self, module_dict, config, loss_fcn=torch.nn.BCEWithLogitsLoss()): self.module_dict = module_dict out_feat = np.sum([model.out_feat for model in module_dict.values()]) self.config = config - self.loss_fcn = getattr(torch.nn, self.config["MODEL"]["Loss_Function"])()#(pos_weight=torch.tensor(1.31)) - self.activation = getattr(torch.nn, self.config["MODEL"]["Activation"])(1, 1) + self.loss_fcn = getattr(torch.nn, self.config["MODEL"]["Loss_Function"])() + self.activation = getattr(torch.nn, self.config["MODEL"]["Activation"])() self.classifier = nn.Sequential( nn.Linear(out_feat, 120), nn.Dropout(0.3), @@ -31,55 +31,75 @@ def forward(self, data_dict): return prediction def training_step(self, batch, batch_idx): - #out = {} - data_dict, label = batch - prediction = self.forward(data_dict) - loss = self.loss_fcn(prediction.squeeze(dim=1), label) + data_dict, label = batch ## Data_dict is [B, NM, Sx, Sy, Sz, C], Label is [B,(1,1)] + prediction = self.forward(data_dict).squeeze(dim=1) + loss = self.loss_fcn(prediction, label[-1]) self.log("train_loss", loss, on_step=False, on_epoch=True, sync_dist=True) - MAE = torch.abs(prediction.flatten(0) - label) + MAE = torch.abs(prediction - label[-1]) out = copy.deepcopy(data_dict) - out['MAE'] = MAE.detach() + out['MAE'] = MAE.detach() out['prediction'] = prediction.detach() - out['label'] = label - out['loss'] = loss + out['label'] = label + out['loss'] = loss return out def training_epoch_end(self, step_outputs): - labels = torch.cat([out['label'] for i, out in enumerate(step_outputs)], dim=0) + labels = [] + for j in range(0, len(step_outputs[0]['label'])): + labels.append(torch.cat([out['label'][j] for i, out in enumerate(step_outputs)], dim=0)) prediction = torch.cat([out['prediction'] for i, out in enumerate(step_outputs)], dim=0) self.logger.report_epoch(prediction, labels, step_outputs,self.current_epoch, 'train_epoch_') - + with open(self.logger.log_dir + "/train_record.ini", "a") as toml_file: + toml_file.write('\n') + toml_file.write('label_epoch_' + str(self.current_epoch) + ':\n') + toml_file.write(str(labels[1])) + toml_file.write('\n') + toml_file.write('censor_epoch_' + str(self.current_epoch) + ':\n') + toml_file.write(str(labels[0])) + toml_file.write('\n') + toml_file.write('prediction_epoch_' + str(self.current_epoch) + ':\n') + toml_file.write(str(prediction)) + toml_file.write('\n') + def validation_step(self, batch, batch_idx): - #print('val step') - #out = {} data_dict, label = batch - prediction = self.forward(data_dict) - loss = self.loss_fcn(prediction.squeeze(dim=1), label) + prediction = self.forward(data_dict).squeeze(dim=1) + loss = self.loss_fcn(prediction, label[-1]) self.log("val_loss", loss, on_step=False, on_epoch=True, sync_dist=True) - MAE = torch.abs(prediction.flatten(0) - label) - #out['MAE'] = MAE + MAE = torch.abs(prediction - label[-1]) out = copy.deepcopy(data_dict) - out['MAE'] = MAE.detach() + out['MAE'] = MAE out['prediction'] = prediction out['label'] = label out['loss'] = loss return out def validation_epoch_end(self, step_outputs): - labels = torch.cat([out['label'] for i, out in enumerate(step_outputs)], dim=0) + labels = [] + for j in range(0, len(step_outputs[0]['label'])): + labels.append(torch.cat([out['label'][j] for i, out in enumerate(step_outputs)], dim=0)) prediction = torch.cat([out['prediction'] for i, out in enumerate(step_outputs)], dim=0) self.logger.report_epoch(prediction.squeeze(), labels, step_outputs, self.current_epoch,'val_epoch_') + with open(self.logger.log_dir + "/val_record.ini", "a") as toml_file: + toml_file.write('\n') + toml_file.write('label_epoch_' + str(self.current_epoch) + ':\n') + toml_file.write(str(labels[1])) + toml_file.write('\n') + toml_file.write('censor_epoch_' + str(self.current_epoch) + ':\n') + toml_file.write(str(labels[0])) + toml_file.write('\n') + toml_file.write('prediction_epoch_' + str(self.current_epoch) + ':\n') + toml_file.write(str(prediction)) + toml_file.write('\n') def test_step(self, batch, batch_idx): data_dict, label = batch - prediction = self.forward(data_dict) - loss = self.loss_fcn(prediction.squeeze(dim=1), label) - #out = {} - MAE = torch.abs(prediction.flatten(0) - label) - #out['MAE'] = MAE + prediction = self.forward(data_dict).squeeze(dim=1) + loss = self.loss_fcn(prediction, label[-1]) + MAE = torch.abs(prediction - label[-1]) out = copy.deepcopy(data_dict) - out['MAE'] = MAE.detach() - out['prediction'] = prediction.squeeze(dim=1) + out['MAE'] = MAE + out['prediction'] = prediction out['label'] = label out['loss'] = loss return out @@ -93,6 +113,6 @@ def weights_reset(self, m): m.reset_parameters() def configure_optimizers(self): - optimizer = torch.optim.Adam(self.parameters(), lr=1e-3) + optimizer = torch.optim.Adam(self.parameters(), lr=5e-4, weight_decay=1e-5) scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=5, gamma=0.5) return [optimizer], [scheduler] diff --git a/Training/Classification.py b/Training/Classification.py new file mode 100644 index 0000000..3c1d4b1 --- /dev/null +++ b/Training/Classification.py @@ -0,0 +1,144 @@ +import torch +import torchvision +from torch import nn +from pytorch_lightning.callbacks import ModelCheckpoint, EarlyStopping +from pytorch_lightning.strategies import DDPStrategy +from pytorch_lightning import LightningDataModule, LightningModule, Trainer, seed_everything +import sys, os +import monai +torch.cuda.empty_cache() +## Module - Dataloaders +from DataGenerator.DataGenerator_PTV import * +from Models.Classifier import Classifier +from Models.Linear import Linear +from Models.MixModel import MixModel +from monai.transforms import EnsureChannelFirstd, ScaleIntensityd, ResampleToMatchd +## Main +from sklearn.preprocessing import StandardScaler, OneHotEncoder +import toml +from Utils.GenerateSmoothLabel import get_smoothed_label_distribution, get_module +from Utils.PredictionReports import PredictionReports +from pathlib import Path +from torchmetrics import ConfusionMatrix +import torchmetrics + +config = toml.load(sys.argv[1]) + +## 2D transform +img_keys = list(config['MODALITY'].keys()) +## Multichannel masks +#img_keys.remove('Structs') +#if 'Structs' in config['DATA'].keys(): +# for roi in config['DATA']['Structs']: +# img_keys.append('Struct_' + roi) + +if config['MODALITY'].values(): + train_transform = torchvision.transforms.Compose([ + EnsureChannelFirstd(keys=img_keys), + #ResampleToMatchd(list(set(img_keys).difference(set(['CT']))), key_dst='CT'), + monai.transforms.ScaleIntensityd(keys=list(set(img_keys).difference(set(['Dose'])))), + #monai.transforms.ResizeWithPadOrCropd(keys=img_keys, spatial_size=config['DATA']['dim']), + monai.transforms.Resized(keys=img_keys, spatial_size=config['DATA']['dim']), + monai.transforms.RandAffined(keys=img_keys), + monai.transforms.RandHistogramShiftd(keys=img_keys), + monai.transforms.RandAdjustContrastd(keys=img_keys), + monai.transforms.RandGaussianNoised(keys=img_keys), + + ]) + + val_transform = torchvision.transforms.Compose([ + EnsureChannelFirstd(keys=img_keys), + ResampleToMatchd(list(set(img_keys).difference(set(['CT']))), key_dst='CT'), + monai.transforms.ScaleIntensityd(list(set(img_keys).difference(set(['Dose'])))), + #monai.transforms.ResizeWithPadOrCropd(img_keys, spatial_size=config['DATA']['dim']), + monai.transforms.Resized(keys=img_keys, spatial_size=config['DATA']['dim']), + ]) +else: + train_transform = None + val_transform = None + +## First Connect to XNAT +session = xnat.connect(config['SERVER']['Address'], user=config['SERVER']['User'],password=config['SERVER']['Password']) + + +SubjectList = QuerySubjectList(config, session) +SynchronizeData(config, SubjectList) +SubjectList.dropna(subset=['xnat_subjectdata_field_map_survival_months'], inplace=True) +# SubjectList.dropna(subset=['xnat_subjectdata_field_map_overall_stage'], inplace=True) + +module_dict = nn.ModuleDict() +if config['DATA']['Multichannel']: ## Single-Model Multichannel learning + if config['MODALITY'].keys(): + module_dict['Image'] = Classifier(config, 'Image') +else: + for key in config['MODALITY'].keys():# Multi-Model Single Channel learning + module_dict[key] = Classifier(config, key) + +if 'Records' in config.keys(): + SubjectList, clinical_cols = LoadClinicalData(config, SubjectList) + module_dict['Records'] = Linear(in_feat=len(clinical_cols), out_feat=42) +else: + clinical_cols = None + +## GeneratePath +for key in config['MODALITY'].keys(): + SubjectList[key+'_Path'] = "" +QuerySubjectInfo(config, SubjectList, session) +print(SubjectList) + +# threshold = config['DATA']['threshold'] +# ckpt_path = Path('./lightning_logs', total_backbone, 'ckpt') +rd = [53414, 88536, 89901, 62594, 13787, 21781, 18215, 4182, 10695, 61645, 93967, 35446, 41063, 98435, 94558, 67665, + 98831, 76684, 33670, 66239, 24417, 29551, 68018, 52785, 41160, 60264, 75053, 58354, 55180, 58358, 51182, 8260] + +for iter in range(0, 25, 1): + seed_everything(rd[iter],workers=True) + + dataloader = DataModule(SubjectList, + config=config, + keys=config['MODALITY'].keys(), + train_transform=train_transform, + val_transform=val_transform, + clinical_cols=clinical_cols, + inference=False) + + model = MixModel(module_dict, config) + model.apply(model.weights_reset) + #full_ckpt_path = Path(ckpt_path, 'Iter_'+ str(iter) + '.ckpt') + #model.load_state_dict(torch.load(full_ckpt_path)['state_dict']) + + filename = config['DATA']['LogFolder'] + + logger = PredictionReports(config=config, save_dir='lightning_logs', name=filename) + logger.log_text() + logger._version = iter + callbacks = [ + ModelCheckpoint(dirpath=Path(logger.log_dir, 'ckpt'), + monitor='val_loss', + filename='Iter_' + str(iter), + save_top_k=3, + mode='min'), + # EarlyStopping(monitor='val_loss', + # check_finite=True), + ] + + trainer = Trainer( + #gpus=1, + accelerator="gpu", + devices=[0,1,2,3], + strategy=DDPStrategy(find_unused_parameters=True), + max_epochs=40, + logger=logger, + callbacks=callbacks, + ) + #model = torch.compile(model) + trainer.fit(model, dataloader) + torch.save({'state_dict': model.state_dict(),}, Path(logger.log_dir, 'Iter_' + str(iter) + '.ckpt')) + +with open(logger.root_dir + "/Config.ini", "w+") as toml_file: + toml.dump(config, toml_file) + toml_file.write("Train transform:\n") + toml_file.write(str(train_transform)) + toml_file.write("Val/Test transform:\n") + toml_file.write(str(val_transform)) + diff --git a/Training/Regression.py b/Training/Regression.py index e3b3f25..76c4296 100644 --- a/Training/Regression.py +++ b/Training/Regression.py @@ -23,6 +23,7 @@ import torchmetrics config = toml.load(sys.argv[1]) + ## 2D transform img_keys = list(config['MODALITY'].keys()) ## Multichannel masks @@ -31,27 +32,30 @@ # for roi in config['DATA']['Structs']: # img_keys.append('Struct_' + roi) -train_transform = torchvision.transforms.Compose([ - EnsureChannelFirstd(keys=img_keys), - #ResampleToMatchd(list(set(img_keys).difference(set(['CT']))), key_dst='CT'), - monai.transforms.ScaleIntensityd(keys=list(set(img_keys).difference(set(['Dose'])))), - # monai.transforms.ResizeWithPadOrCropd(keys=img_keys, spatial_size=config['DATA']['dim']), - monai.transforms.Resized(keys=img_keys, spatial_size=config['DATA']['dim']), - monai.transforms.RandAffined(keys=img_keys), - monai.transforms.RandHistogramShiftd(keys=img_keys), - monai.transforms.RandAdjustContrastd(keys=img_keys), - monai.transforms.RandGaussianNoised(keys=img_keys), - -]) - -val_transform = torchvision.transforms.Compose([ - EnsureChannelFirstd(keys=img_keys), - #ResampleToMatchd(list(set(img_keys).difference(set(['CT']))), key_dst='CT'), - monai.transforms.ScaleIntensityd(list(set(img_keys).difference(set(['Dose'])))), - # monai.transforms.ResizeWithPadOrCropd(img_keys, spatial_size=config['DATA']['dim']), - monai.transforms.Resized(keys=img_keys, spatial_size=config['DATA']['dim']), -]) - +if config['MODALITY'].values(): + train_transform = torchvision.transforms.Compose([ + EnsureChannelFirstd(keys=img_keys), + #ResampleToMatchd(list(set(img_keys).difference(set(['CT']))), key_dst='CT'), + monai.transforms.ScaleIntensityd(keys=list(set(img_keys).difference(set(['Dose'])))), + #monai.transforms.ResizeWithPadOrCropd(keys=img_keys, spatial_size=config['DATA']['dim']), + monai.transforms.Resized(keys=img_keys, spatial_size=config['DATA']['dim']), + monai.transforms.RandAffined(keys=img_keys), + monai.transforms.RandHistogramShiftd(keys=img_keys), + monai.transforms.RandAdjustContrastd(keys=img_keys), + monai.transforms.RandGaussianNoised(keys=img_keys), + + ]) + + val_transform = torchvision.transforms.Compose([ + EnsureChannelFirstd(keys=img_keys), + #ResampleToMatchd(list(set(img_keys).difference(set(['CT']))), key_dst='CT'), + monai.transforms.ScaleIntensityd(list(set(img_keys).difference(set(['Dose'])))), + #monai.transforms.ResizeWithPadOrCropd(img_keys, spatial_size=config['DATA']['dim']), + monai.transforms.Resized(keys=img_keys, spatial_size=config['DATA']['dim']), + ]) +else: + train_transform = None + val_transform = None ## First Connect to XNAT session = xnat.connect(config['SERVER']['Address'], user=config['SERVER']['User'],password=config['SERVER']['Password']) @@ -87,9 +91,9 @@ rd = [53414, 88536, 89901, 62594, 13787, 21781, 18215, 4182, 10695, 61645, 93967, 35446, 41063, 98435, 94558, 67665, 98831, 76684, 33670, 66239, 24417, 29551, 68018, 52785, 41160, 60264, 75053, 58354, 55180, 58358, 51182, 8260] -for iter in range(0, 20, 1): - seed_everything(rd[iter]) - # seed_everything(42, workers=True) +for iter in range(0, 2, 1): + seed_everything(rd[iter],workers=True) + dataloader = DataModule(SubjectList, config=config, keys=config['MODALITY'].keys(), @@ -104,7 +108,8 @@ #model.load_state_dict(torch.load(full_ckpt_path)['state_dict']) filename = config['DATA']['LogFolder'] - logger = PredictionReports(config=config, save_dir='lightning_logs/Regression', name=filename) + + logger = PredictionReports(config=config, save_dir='lightning_logs', name=filename) logger.log_text() logger._version = iter callbacks = [ @@ -120,7 +125,7 @@ trainer = Trainer( #gpus=1, accelerator="gpu", - devices=[0,1,2,3], + devices=[0,1], strategy=DDPStrategy(find_unused_parameters=True), max_epochs=30, logger=logger, diff --git a/Training/Validation.py b/Training/Validation.py deleted file mode 100644 index fe4f3b4..0000000 --- a/Training/Validation.py +++ /dev/null @@ -1,154 +0,0 @@ -import torch -import torchvision -from torch import nn -from pytorch_lightning.callbacks import ModelCheckpoint, EarlyStopping -from pytorch_lightning.strategies import DDPStrategy -from pytorch_lightning import LightningDataModule, LightningModule, Trainer, seed_everything -import sys, os -# import torchio as tio -import monai - -torch.cuda.empty_cache() -## Module - Dataloaders -from DataGenerator.DataGenerator import * -from Models.Classifier import Classifier -from Models.Linear import Linear -from Models.MixModel import MixModel -from monai.transforms import EnsureChannelFirstd, ScaleIntensityd, ResampleToMatchd -## Main -from sklearn.preprocessing import StandardScaler, OneHotEncoder -import toml -from Utils.GenerateSmoothLabel import get_smoothed_label_distribution, get_module -from Utils.PredictionReports import PredictionReports -from pathlib import Path -# import torchio as tio -from torchmetrics import ConfusionMatrix -import torchmetrics - -config = toml.load(sys.argv[1]) -## 2D transform -img_keys = list(config['MODALITY'].keys()) -# img_keys.remove('Structs') -# if 'Structs' in config['DATA'].keys(): -# for roi in config['DATA']['Structs']: -# img_keys.append('Struct_' + roi) - -train_transform = torchvision.transforms.Compose([ - EnsureChannelFirstd(keys=img_keys), - # ResampleToMatchd(list(set(img_keys).difference(set(['CT']))), key_dst='CT'), - monai.transforms.ScaleIntensityd(keys=img_keys), - # monai.transforms.ResizeWithPadOrCropd(keys=img_keys, spatial_size=config['DATA']['dim']), - monai.transforms.Resized(keys=img_keys, spatial_size=config['DATA']['dim']), - monai.transforms.RandAffined(keys=img_keys), - monai.transforms.RandHistogramShiftd(keys=img_keys), - monai.transforms.RandAdjustContrastd(keys=img_keys), - monai.transforms.RandGaussianNoised(keys=img_keys), - -]) - -val_transform = torchvision.transforms.Compose([ - EnsureChannelFirstd(keys=img_keys), - # ResampleToMatchd(list(set(img_keys).difference(set(['CT']))), key_dst='CT'), - monai.transforms.ScaleIntensityd(img_keys), - # monai.transforms.ResizeWithPadOrCropd(img_keys, spatial_size=config['DATA']['dim']), - monai.transforms.Resized(keys=img_keys, spatial_size=config['DATA']['dim']), -]) - -## First Connect to XNAT -session = xnat.connect(config['SERVER']['Address'], user=config['SERVER']['User'], - password=config['SERVER']['Password']) - -SubjectList = QuerySubjectList(config, session) -SynchronizeData(config, SubjectList) -SubjectList.dropna(subset=['xnat_subjectdata_field_map_survival_months'], inplace=True) - -module_dict = nn.ModuleDict() -if config['DATA']['Multichannel']: ## Single-Model Multichannel learning - if config['MODALITY'].keys(): - module_dict['Image'] = Classifier(config, 'Image') -else: - for key in config['MODALITY'].keys(): # Multi-Model Single Channel learning - module_dict[key] = Classifier(config, key) - -if 'Records' in config.keys(): - SubjectList, clinical_cols = LoadClinicalData(config, SubjectList) - module_dict['Records'] = Linear(in_feat=len(clinical_cols), out_feat=42) -else: - clinical_cols = None - -print('clinical_cols:', len(clinical_cols)) -## GeneratePath -for key in config['MODALITY'].keys(): - SubjectList[key + '_Path'] = "" -QuerySubjectInfo(config, SubjectList, session) - -threshold = config['DATA']['threshold'] -roc_list = [] -sp_list = [] -sensi_list = [] -acc_list = [] -pre_list = [] - -tprs = [] -roc = torchmetrics.ROC() -auroc = torchmetrics.AUROC() -fig = plt.figure() -base_fpr = np.linspace(0, 1, 39) -cm = ConfusionMatrix(num_classes=2) -prediction_labels_full_list = [] -bidx = [21, 25, 0, 4, 6] -for it in range(0, 5, 1): - iter = bidx[it] - # seed_everything(4200) - dataloader = DataModule(SubjectList, - config=config, - keys=config['MODALITY'].keys(), - train_transform=train_transform, - val_transform=val_transform, - clinical_cols=clinical_cols, - inference=False, - train_size=0.85) - - model = MixModel(module_dict, config) - filename = 'lightning_logs/random_seed_75_Seg/version_' + str(iter) - full_ckpt_path = Path(filename, 'Iter_' + str(iter) + '.ckpt') - # full_ckpt_path = Path(ckpt_path, 'ckpt', 'Iter_' + str(iter) + '.ckpt') - # full_ckpt_path = 'Iter_' + str(iter) + '.ckpt' - model.load_state_dict(torch.load(full_ckpt_path)['state_dict']) - # model.load_state_dict(torch.load(full_ckpt_path, map_location='cpu')['state_dict']) - model.eval() - print('start testing...') - worstCase = 0 - with torch.no_grad(): - outs = [] - for i, data in enumerate(dataloader.test_dataloader()): - truth = data[1] - x = data[0] - output = model.test_step(data, i) - outs.append(output) - - validation_labels_full = torch.cat([out['label'] for i, out in enumerate(outs)], dim=0) - prediction_labels_full = torch.cat([out['prediction'] for i, out in enumerate(outs)], dim=0) - roc_i = auroc(prediction_labels_full, validation_labels_full.int()) - print('roc_' + str(iter), roc_i) - prediction_labels_full_list.append(prediction_labels_full.tolist()) - -prediction_labels = torch.tensor(prediction_labels_full_list).mean(dim=0) -validation_labels = validation_labels_full -roc = auroc(prediction_labels, validation_labels.int()) -bcm = cm(prediction_labels.round(), validation_labels.int()) -tn = bcm[0][0] -tp = bcm[1][1] -fp = bcm[0][1] -fn = bcm[1][0] -acc = bcm.diag().sum() / bcm.sum() -sensitivity = tp / (tp + fn) -precision = tp / (tp + fp) -spec = tn / (tn + fp) - -print('avg_roc', str(roc)) -print('avg_specificity', str(spec)) -print('avg_sensitivity', str(sensitivity)) -print('avg_accuracy', str(acc)) -print('avg_precision', str(precision)) -print('finish test') diff --git a/Training/test.py b/Training/test.py index 83a51d7..86c44c6 100644 --- a/Training/test.py +++ b/Training/test.py @@ -1,42 +1,178 @@ -import torch, torchvision +import torch +import torchvision from torch import nn -from torchvision import transforms, models, datasets -import shap -import json -import numpy as np -mean = [0.485, 0.456, 0.406] -std = [0.229, 0.224, 0.225] +from pytorch_lightning.callbacks import ModelCheckpoint, EarlyStopping +from pytorch_lightning.strategies import DDPStrategy +from pytorch_lightning import LightningDataModule, LightningModule, Trainer, seed_everything +import sys, os +# import torchio as tio +import monai -def normalize(image): - if image.max() > 1: - image /= 255 - image = (image - mean) / std - # in addition, roll the axis so that they suit pytorch - return torch.tensor(image.swapaxes(-1, 1).swapaxes(2, 3)).float() +torch.cuda.empty_cache() +## Module - Dataloaders +from DataGenerator.DataGenerator_PTV import * +from Models.Classifier import Classifier +from Models.Linear import Linear +from Models.MixModel import MixModel +from monai.transforms import EnsureChannelFirstd, ScaleIntensityd, ResampleToMatchd +## Main +from sklearn.preprocessing import StandardScaler, OneHotEncoder +import toml +from Utils.GenerateSmoothLabel import get_smoothed_label_distribution, get_module +from Utils.PredictionReports import PredictionReports +from pathlib import Path +# import torchio as tio +from torchmetrics import ConfusionMatrix +import torchmetrics +config = toml.load(sys.argv[1]) +## 2D transform +img_keys = list(config['MODALITY'].keys()) +# img_keys.remove('Structs') +# if 'Structs' in config['DATA'].keys(): +# for roi in config['DATA']['Structs']: +# img_keys.append('Struct_' + roi) -# load the model -model = models.vgg16(pretrained=True).eval() +train_transform = torchvision.transforms.Compose([ + EnsureChannelFirstd(keys=img_keys), + # ResampleToMatchd(list(set(img_keys).difference(set(['CT']))), key_dst='CT'), + monai.transforms.ScaleIntensityd(keys=img_keys), + #monai.transforms.ResizeWithPadOrCropd(keys=img_keys, spatial_size=config['DATA']['dim']), + monai.transforms.Resized(keys=img_keys, spatial_size=config['DATA']['dim']), + monai.transforms.RandAffined(keys=img_keys), + monai.transforms.RandHistogramShiftd(keys=img_keys), + monai.transforms.RandAdjustContrastd(keys=img_keys), + monai.transforms.RandGaussianNoised(keys=img_keys), -X,y = shap.datasets.imagenet50() +]) -X /= 255 +val_transform = torchvision.transforms.Compose([ + EnsureChannelFirstd(keys=img_keys), + # ResampleToMatchd(list(set(img_keys).difference(set(['CT']))), key_dst='CT'), + monai.transforms.ScaleIntensityd(img_keys), + #monai.transforms.ResizeWithPadOrCropd(img_keys, spatial_size=config['DATA']['dim']), + monai.transforms.Resized(keys=img_keys, spatial_size=config['DATA']['dim']), +]) -to_explain = X[[39, 41]] +## First Connect to XNAT +session = xnat.connect(config['SERVER']['Address'], user=config['SERVER']['User'], + password=config['SERVER']['Password']) -# load the ImageNet class names -url = "https://s3.amazonaws.com/deep-learning-models/image-models/imagenet_class_index.json" -fname = shap.datasets.cache(url) -with open(fname) as f: - class_names = json.load(f) +SubjectList = QuerySubjectList(config, session) +SynchronizeData(config, SubjectList) +SubjectList.dropna(subset=['xnat_subjectdata_field_map_survival_months'], inplace=True) -e = shap.GradientExplainer((model, model.features[7]), normalize(X)) -shap_values,indexes = e.shap_values(normalize(to_explain), ranked_outputs=2, nsamples=200) +module_dict = nn.ModuleDict() +if config['DATA']['Multichannel']: ## Single-Model Multichannel learning + if config['MODALITY'].keys(): + module_dict['Image'] = Classifier(config, 'Image') +else: + for key in config['MODALITY'].keys(): # Multi-Model Single Channel learning + module_dict[key] = Classifier(config, key) -# get the names for the classes -index_names = np.vectorize(lambda x: class_names[str(x)][1])(indexes) +if 'Records' in config.keys(): + SubjectList, clinical_cols = LoadClinicalData(config, SubjectList) + module_dict['Records'] = Linear(in_feat=len(clinical_cols), out_feat=42) +else: + clinical_cols = None -# plot the explanations -shap_values = [np.swapaxes(np.swapaxes(s, 2, 3), 1, -1) for s in shap_values] +print('clinical_cols:', len(clinical_cols)) +## GeneratePath +for key in config['MODALITY'].keys(): + SubjectList[key + '_Path'] = "" +QuerySubjectInfo(config, SubjectList, session) -shap.image_plot(shap_values, to_explain, index_names) +threshold = config['DATA']['threshold'] +roc_list = [] +sp_list = [] +sensi_list = [] +acc_list = [] +pre_list = [] + +tprs = [] +roc = torchmetrics.ROC() +auroc = torchmetrics.AUROC() +fig = plt.figure() +base_fpr = np.linspace(0, 1, 39) +cm = ConfusionMatrix(num_classes=2) +prediction_labels_full_list = [] +bidx = [21, 25, 0, 4, 6] +for iter in range(0, 5, 1): + # iter = bidx[it] + # seed_everything(4200) + dataloader = DataModule(SubjectList, + config=config, + keys=config['MODALITY'].keys(), + train_transform=train_transform, + val_transform=val_transform, + clinical_cols=clinical_cols, + inference=False, + train_size=0.85) + + + logger = PredictionReports(config=config, save_dir='lightning_logs/test', name= config['DATA']['LogFolder']) + model = MixModel(module_dict, config) + filename = 'lightning_logs/' + config['DATA']['LogFolder'] + '/version_' + str(iter) + full_ckpt_path = Path(filename, 'Iter_' + str(iter) + '.ckpt') + # full_ckpt_path = Path(ckpt_path, 'ckpt', 'Iter_' + str(iter) + '.ckpt') + # full_ckpt_path = 'Iter_' + str(iter) + '.ckpt' + model.load_state_dict(torch.load(full_ckpt_path)['state_dict']) + + #model = MixModel(module_dict, config) + #filename = 'lightning_logs/' + config['DATA']['LogFolder'] + '/version_' + str(iter) + #full_ckpt_path = Path(filename, 'Iter_' + str(iter) + '.ckpt') + # full_ckpt_path = Path(ckpt_path, 'ckpt', 'Iter_' + str(iter) + '.ckpt') + # full_ckpt_path = 'Iter_' + str(iter) + '.ckpt' + #model.load_state_dict(torch.load(full_ckpt_path)['state_dict']) + # model.load_state_dict(torch.load(full_ckpt_path, map_location='cpu')['state_dict']) + model.eval() + print('start testing...') + worstCase = 0 + with torch.no_grad(): + outs = [] + for i, data in enumerate(dataloader.test_dataloader()): + truth = data[1] + x = data[0] + output = model.test_step(data, i) + outs.append(output) + + validation_labels_full = torch.cat([out['label'][1] for i, out in enumerate(outs)], dim=0) + validation_censor_full = torch.cat([out['label'][0] for i, out in enumerate(outs)], dim=0) + prediction_labels_full = torch.cat([out['prediction'] for i, out in enumerate(outs)], dim=0) + roc_i = auroc(prediction_labels_full, validation_labels_full.int()) + print('roc_' + str(iter), roc_i) + prediction_labels_full_list.append(prediction_labels_full.tolist()) + logger.report_test(config, outs, model, prediction_labels_full, [validation_censor_full, validation_labels_full], 'test_') + with open(logger.log_dir + "/test_record.ini", "a") as toml_file: + toml_file.write('\n') + toml_file.write('label_iter_' + str(iter) + ':\n') + toml_file.write(str(validation_labels_full)) + toml_file.write('\n') + toml_file.write('censor_iter_' + str(iter) + ':\n') + toml_file.write(str(validation_censor_full)) + toml_file.write('\n') + toml_file.write('prediction_iter_' + str(iter) + ':\n') + toml_file.write(str(prediction_labels_full)) + toml_file.write('\n') + +prediction_labels = torch.tensor(prediction_labels_full_list).mean(dim=0) +validation_labels = validation_labels_full +#print(prediction_labels_full_list) +#print(validation_labels) +roc = auroc(prediction_labels, validation_labels.int()) +bcm = cm(prediction_labels.round(), validation_labels.int()) +tn = bcm[0][0] +tp = bcm[1][1] +fp = bcm[0][1] +fn = bcm[1][0] +acc = bcm.diag().sum() / bcm.sum() +sensitivity = tp / (tp + fn) +precision = tp / (tp + fp) +spec = tn / (tn + fp) + +print('avg_roc', str(roc)) +print('avg_specificity', str(spec)) +print('avg_sensitivity', str(sensitivity)) +print('avg_accuracy', str(acc)) +print('avg_precision', str(precision)) +print('finish test') diff --git a/Training/ValidationR.py b/Training/testR.py similarity index 60% rename from Training/ValidationR.py rename to Training/testR.py index 24a3088..211f489 100644 --- a/Training/ValidationR.py +++ b/Training/testR.py @@ -9,6 +9,7 @@ import monai from Utils.GenerateSmoothLabel import generate_cumulative_dynamic_auc torch.cuda.empty_cache() +from lifelines.utils import concordance_index ## Module - Dataloaders from DataGenerator.DataGenerator import * from Models.Classifier import Classifier @@ -21,7 +22,7 @@ from Utils.GenerateSmoothLabel import get_smoothed_label_distribution, get_module from Utils.PredictionReports import PredictionReports from pathlib import Path -from Utils.PredictionReports import c_index, r2_index +from Utils.PredictionReports import r2_index # import torchio as tio from torchmetrics import ConfusionMatrix import torchmetrics @@ -33,26 +34,30 @@ # for roi in config['DATA']['Structs']: # img_keys.append('Struct_' + roi) -train_transform = torchvision.transforms.Compose([ - EnsureChannelFirstd(keys=img_keys), - # ResampleToMatchd(list(set(img_keys).difference(set(['CT']))), key_dst='CT'), - monai.transforms.ScaleIntensityd(keys=img_keys), - # monai.transforms.ResizeWithPadOrCropd(keys=img_keys, spatial_size=config['DATA']['dim']), - monai.transforms.Resized(keys=img_keys, spatial_size=config['DATA']['dim']), - monai.transforms.RandAffined(keys=img_keys), - monai.transforms.RandHistogramShiftd(keys=img_keys), - monai.transforms.RandAdjustContrastd(keys=img_keys), - monai.transforms.RandGaussianNoised(keys=img_keys), +if config['MODALITY'].values(): + train_transform = torchvision.transforms.Compose([ + EnsureChannelFirstd(keys=img_keys), + #ResampleToMatchd(list(set(img_keys).difference(set(['CT']))), key_dst='CT'), + monai.transforms.ScaleIntensityd(keys=list(set(img_keys).difference(set(['Dose'])))), + # monai.transforms.ResizeWithPadOrCropd(keys=img_keys, spatial_size=config['DATA']['dim']), + monai.transforms.Resized(keys=img_keys, spatial_size=config['DATA']['dim']), + monai.transforms.RandAffined(keys=img_keys), + monai.transforms.RandHistogramShiftd(keys=img_keys), + monai.transforms.RandAdjustContrastd(keys=img_keys), + monai.transforms.RandGaussianNoised(keys=img_keys), -]) + ]) -val_transform = torchvision.transforms.Compose([ - EnsureChannelFirstd(keys=img_keys), - # ResampleToMatchd(list(set(img_keys).difference(set(['CT']))), key_dst='CT'), - monai.transforms.ScaleIntensityd(img_keys), - # monai.transforms.ResizeWithPadOrCropd(img_keys, spatial_size=config['DATA']['dim']), - monai.transforms.Resized(keys=img_keys, spatial_size=config['DATA']['dim']), -]) + val_transform = torchvision.transforms.Compose([ + EnsureChannelFirstd(keys=img_keys), + #ResampleToMatchd(list(set(img_keys).difference(set(['CT']))), key_dst='CT'), + monai.transforms.ScaleIntensityd(list(set(img_keys).difference(set(['Dose'])))), + # monai.transforms.ResizeWithPadOrCropd(img_keys, spatial_size=config['DATA']['dim']), + monai.transforms.Resized(keys=img_keys, spatial_size=config['DATA']['dim']), + ]) +else: + train_transform = None + val_transform = None ## First Connect to XNAT session = xnat.connect(config['SERVER']['Address'], user=config['SERVER']['User'], @@ -76,7 +81,7 @@ else: clinical_cols = None -print('clinical_cols:', len(clinical_cols)) +#print('clinical_cols:', len(clinical_cols)) ## GeneratePath for key in config['MODALITY'].keys(): SubjectList[key + '_Path'] = "" @@ -86,7 +91,7 @@ prediction_labels_full_list = [] # bidx = [21, 25, 0, 4, 6] auroc = torchmetrics.AUROC(num_classes=1) -for iter in range(0, 20, 1): +for iter in range(0, 2, 1): # iter = bidx[it] # seed_everything(4200) dataloader = DataModule(SubjectList, @@ -98,9 +103,12 @@ inference=False, train_size=0.85) + + + #censored_label = not(np.int8(dataloader.test_list.loc[i, 'xnat_subjectdata_field_map_' + config['DATA']['censor_label']]).astype('bool')) logger = PredictionReports(config=config, save_dir='lightning_logs/test', name= config['DATA']['LogFolder']) model = MixModel(module_dict, config) - filename = 'lightning_logs/Regression/' + config['DATA']['LogFolder'] + '/version_' + str(iter) + filename = 'lightning_logs/' + config['DATA']['LogFolder'] + '/version_' + str(iter) full_ckpt_path = Path(filename, 'Iter_' + str(iter) + '.ckpt') # full_ckpt_path = Path(ckpt_path, 'ckpt', 'Iter_' + str(iter) + '.ckpt') # full_ckpt_path = 'Iter_' + str(iter) + '.ckpt' @@ -116,28 +124,34 @@ x = data[0] output = model.test_step(data, i) outs.append(output) - - validation_labels_full = torch.cat([out['label'] for i, out in enumerate(outs)], dim=0) + + validation_labels_full = torch.cat([out['label'][1] for i, out in enumerate(outs)], dim=0) + validation_censor_full = torch.cat([out['label'][0] for i, out in enumerate(outs)], dim=0) prediction_labels_full = torch.cat([out['prediction'] for i, out in enumerate(outs)], dim=0) - cindex = c_index(prediction_labels_full, validation_labels_full.int()) + cindex = concordance_index(validation_labels_full, prediction_labels_full, validation_censor_full) print('cindex_' + str(iter), cindex) - cindexs.append(cindex[0]) - roc_i = auroc(torch.where(prediction_labels_full > 24, 1, 0), torch.where(validation_labels_full > 24, 1, 0)) - print('roc_' + str(iter), roc_i) + cindexs.append(cindex) prediction_labels_full_list.append(prediction_labels_full.tolist()) - logger.report_test(config, outs, model, prediction_labels_full, validation_labels_full, 'test_') + logger.report_test(config, outs, model, prediction_labels_full, [validation_censor_full, validation_labels_full], 'test_') + with open(logger.log_dir + "/test_record.ini", "a") as toml_file: + toml_file.write('\n') + toml_file.write('label_iter_' + str(iter) + ':\n') + toml_file.write(str(validation_labels_full)) + toml_file.write('\n') + toml_file.write('censor_iter_' + str(iter) + ':\n') + toml_file.write(str(validation_censor_full)) + toml_file.write('\n') + toml_file.write('prediction_iter_' + str(iter) + ':\n') + toml_file.write(str(prediction_labels_full)) + toml_file.write('\n') prediction_labels = torch.tensor(prediction_labels_full_list).mean(dim=0) validation_labels = validation_labels_full - fig_path = 'lightning_logs/test/' + config['DATA']['LogFolder'] -generate_cumulative_dynamic_auc(prediction_labels, validation_labels, fig_path) -cindex_tol = c_index(prediction_labels, validation_labels) +cindex_tol = concordance_index(validation_labels, prediction_labels, validation_censor_full) r2_tol = r2_index(prediction_labels, validation_labels) + print('cindex series', cindexs) print('avg_roc', str(cindex_tol)) print('avg_r2', str(r2_tol)) - -auroc = auroc(torch.where(prediction_labels > 24, 1, 0), torch.where(validation_labels > 24, 1, 0)) -print('auroc', auroc) diff --git a/Utils/PredictionReports.py b/Utils/PredictionReports.py index e636bc5..74ce792 100644 --- a/Utils/PredictionReports.py +++ b/Utils/PredictionReports.py @@ -4,9 +4,9 @@ from pytorch_lightning.loggers.base import rank_zero_experiment from torch.utils.tensorboard import SummaryWriter import numpy as np - +from lifelines.utils import concordance_index import matplotlib.pyplot as plt -plt.switch_backend('agg') +import matplotlib import torchvision from pytorch_lightning.loggers import LightningLoggerBase from sksurv.metrics import cumulative_dynamic_auc @@ -76,8 +76,7 @@ def log_metrics(self, metrics: Dict[str, float], step: Optional[int] = None): def log_image(self, img, text, current_epoch=None): img = img.transpose(2, 0) img_batch = img.view(img.shape[0], *[1, img.shape[1], img.shape[2]]) - #img_batch = img.view(img.shape[0] * img.shape[1], *[1, img.shape[2], img.shape[3]]) - grid = torchvision.utils.make_grid(img_batch) + grid = torchvision.utils.make_grid(img_batch, normalize=True) self.experiment.add_image(text, grid, current_epoch) return grid @@ -87,14 +86,16 @@ def log_text(self) -> None: # str(self.config['MODALITY'].keys()) self.experiment.add_text('configurations:', configurations) - def regression_matrix(self, prediction, label, prefix): + def regression_matrix(self, prediction, label, prefix): ## matrix should be metrics r_out = {} if 'cindex' in self.config['CHECKPOINT']['matrix']: - cindex = c_index(prediction, label) - r_out[prefix + 'cindex'] = cindex[0] + cindex = concordance_index(label[1].cpu().detach().numpy(), prediction.cpu().detach().numpy(), label[0].cpu().detach().numpy()) + r_out[prefix + 'cindex'] = cindex if 'r2' in self.config['CHECKPOINT']['matrix']: - r2 = r2_index(prediction, label) + r2 = r2_index(prediction.detach(), label[1].detach()) r_out[prefix + 'r2'] = r2 + if 'train' in prefix: + print('r2', r2) return r_out def classification_matrix(self, prediction, label, prefix): @@ -105,7 +106,7 @@ def classification_matrix(self, prediction, label, prefix): tp = bcm[1][1] fp = bcm[0][1] fn = bcm[1][0] - if 'AUC' in self.config['CHECKPOINT']['matrix']: + if 'ROC' in self.config['CHECKPOINT']['matrix']: auroc = torchmetrics.AUROC() accuracy = auroc(prediction, label.int()) c_out[prefix + 'roc'] = accuracy @@ -126,29 +127,30 @@ def classification_matrix(self, prediction, label, prefix): c_out[prefix + 'precision'] = precision return c_out - def generate_cumulative_dynamic_auc(self, prediction, label, current_epoch, prefix) -> None: - # this function has issues - risk_score = 1/prediction - #va_times = np.arange(int(label.cpu().min()) + 1, label.cpu().max(), 1) - va_times = np.percentile(label.cpu(), np.linspace(5, 81, 20)) - dtypes = np.dtype([('event', np.bool_), ('time', np.float)]) - construct_test = np.ndarray(shape=(len(label),), dtype=dtypes) - for i in range(len(label)): - construct_test[i] = (True, label[i].cpu().numpy()) - - cph_auc, cph_mean_auc = cumulative_dynamic_auc( - construct_test, construct_test, risk_score.cpu().squeeze(), va_times - ) - - fig = plt.figure() - plt.plot(va_times, cph_auc, marker="o") - plt.axhline(cph_mean_auc, linestyle="--") - plt.ylim([0, 1]) - plt.xlabel("survival months") - plt.ylabel("time-dependent AUC") - plt.grid(True) - self.experiment.add_figure(prefix + "AUC", fig, current_epoch) - plt.close(fig) + # def generate_cumulative_dynamic_auc(self, prediction, label, current_epoch, prefix) -> None: + # # this function has issues + # risk_score = 1 / prediction + # # risk_score = prediction + # # va_times = np.arange(int(label.cpu().min()) + 1, label.cpu().max(), 1) + # va_times = np.percentile(label.cpu(), np.linspace(5, 81, 20)) + # dtypes = np.dtype([('event', np.bool_), ('time', np.float)]) + # construct_test = np.ndarray(shape=(len(label),), dtype=dtypes) + # for i in range(len(label)): + # construct_test[i] = (True, label[i].cpu().numpy()) + # + # cph_auc, cph_mean_auc = cumulative_dynamic_auc( + # construct_test, construct_test, risk_score.cpu().squeeze(), va_times + # ) + # + # fig = plt.figure() + # plt.plot(va_times, cph_auc, marker="o") + # plt.axhline(cph_mean_auc, linestyle="--") + # plt.ylim([0, 1]) + # plt.xlabel("survival months") + # plt.ylabel("time-dependent AUC") + # plt.grid(True) + # self.experiment.add_figure(prefix + "AUC", fig, current_epoch) + # plt.close(fig) def plot_AUROC(self, prediction, label, prefix, current_epoch=None) -> None: roc = torchmetrics.ROC() @@ -166,47 +168,43 @@ def plot_AUROC(self, prediction, label, prefix, current_epoch=None) -> None: def worst_case_show(self, validation_step_outputs, prefix): out = {} worst_AE = 0 + label = '' for i, data in enumerate(validation_step_outputs): loss = data['MAE'] idx = torch.argmax(loss) if loss[idx] > worst_AE: if 'CT' in self.config['MODALITY'].keys(): - worst_img = data['Image'][idx][0,:,:,:] + worst_img = data['Image'][idx][0, :, :, :] if 'Dose' in self.config['MODALITY'].keys(): - worst_dose = data['Image'][idx][1,:,:,:] + worst_dose = data['Image'][idx][1, :, :, :] ### this index needs to be careful when adding pet image worst_AE = loss[idx] + label = data['slabel'][idx] out[prefix + 'worst_AE'] = worst_AE if 'CT' in self.config['MODALITY'].keys(): out[prefix + 'worst_img'] = worst_img if 'Dose' in self.config['MODALITY'].keys(): out[prefix + 'worst_dose'] = worst_dose + out[prefix + 'slabel'] = label return out - # def report_step(self, prediction, label, step, prefix) -> None: - # if self.config['MODEL']['Prediction_type'] == 'Regression': - # regression_out = self.regression_matrix(prediction, label, prefix) - # self.log_metrics(regression_out, step) - # if self.config['MODEL']['Prediction_type'] == 'Classification': - # classification_out = self.classification_matrix(prediction.squeeze(), label, prefix) - # self.log_metrics(classification_out, step) - def report_epoch(self, prediction, label, validation_step_outputs, current_epoch, prefix) -> None: if self.config['MODEL']['Prediction_type'] == 'Regression': regression_out = self.regression_matrix(prediction, label, prefix) + if 'train' in prefix: + print('regression_matrix:', regression_out) self.log_metrics(regression_out, current_epoch) - if 'AUROC' in self.config['CHECKPOINT']['matrix']: - self.generate_cumulative_dynamic_auc(prediction, label, current_epoch, prefix) if self.config['MODEL']['Prediction_type'] == 'Classification': - classification_out = self.classification_matrix(prediction.squeeze(), label, prefix) + classification_out = self.classification_matrix(prediction.squeeze(), label[1], prefix) self.log_metrics(classification_out, current_epoch) if 'AUROC' in self.config['CHECKPOINT']['matrix']: - self.plot_AUROC(prediction.squeeze(), label, prefix, current_epoch) + self.plot_AUROC(prediction.squeeze(), label[0], prefix, current_epoch) if 'WorstCase' in self.config['CHECKPOINT']['matrix']: worst_record = self.worst_case_show(validation_step_outputs, prefix) self.log_metrics({prefix + 'worst_AE': worst_record[prefix + 'worst_AE']}, current_epoch) + self.log_metrics('worst_subject: ', str(worst_record[prefix + 'slabel'])) if 'CT' in self.config['MODALITY'].keys(): text = 'validate_worst_case_img' self.log_image(worst_record[prefix + 'worst_img'], text, current_epoch) @@ -218,6 +216,7 @@ def report_test(self, config, outs, model, prediction_labels, validation_labels, if 'WorstCase' in config['CHECKPOINT']['matrix']: worst_record = self.worst_case_show(outs, prefix) self.experiment.add_text('worst_test_AE: ', str(worst_record[prefix + 'worst_AE'])) + self.experiment.add_text('worst_subject: ', str(worst_record[prefix + 'slabel'])) if 'CT' in config['MODALITY'].keys(): text = 'test_worst_case_img' self.log_image(worst_record[prefix + 'worst_img'], text) @@ -226,17 +225,16 @@ def report_test(self, config, outs, model, prediction_labels, validation_labels, self.log_image(worst_record[prefix + 'worst_dose'], text) if config['MODEL']['Prediction_type'] == 'Regression': - self.experiment.add_text('test loss: ', str(model.loss_fcn(prediction_labels, validation_labels))) - self.generate_cumulative_dynamic_auc(prediction_labels, validation_labels, 0, prefix) + self.experiment.add_text('test loss: ', str(model.loss_fcn(prediction_labels, validation_labels[1]))) regression_out = self.regression_matrix(prediction_labels, validation_labels, prefix) self.experiment.add_text('test_cindex: ', str(regression_out[prefix + 'cindex'])) self.experiment.add_text('test_r2: ', str(regression_out[prefix + 'r2'])) return regression_out if config['MODEL']['Prediction_type'] == 'Classification': - classification_out = self.classification_matrix(prediction_labels.squeeze(), validation_labels, prefix) + classification_out = self.classification_matrix(prediction_labels.squeeze(), validation_labels[1], prefix) if 'AUROC' in config['CHECKPOINT']['matrix']: - self.plot_AUROC(prediction_labels, validation_labels, prefix) + self.plot_AUROC(prediction_labels, validation_labels[0], prefix) self.experiment.add_text('test_AUROC: ', str(classification_out[prefix + 'roc'])) if 'Specificity' in config['CHECKPOINT']['matrix']: self.experiment.add_text('Specificity:', str(classification_out[prefix + 'specificity'])) @@ -246,24 +244,14 @@ def report_test(self, config, outs, model, prediction_labels, validation_labels, self.experiment.add_text('Specificity:', str(classification_out[prefix + 'accuracy'])) if 'Precision' in config['CHECKPOINT']['matrix']: self.experiment.add_text('Specificity:', str(classification_out[prefix + 'precision'])) - if 'AUC' in config['CHECKPOINT']['matrix']: + if 'ROC' in config['CHECKPOINT']['matrix']: self.experiment.add_text('ROC:', str(classification_out[prefix + 'roc'])) return classification_out def r2_index(prediction, label): - loss = nn.MSELoss() - MSE = loss(prediction, label) - SSres = MSE * label.shape[0] + loss = nn.MSELoss(reduction='sum') + SSres = loss(prediction, label) SStotal = torch.sum(torch.square(label - torch.mean(label))) r2 = 1 - SSres / SStotal return r2 - - -def c_index(prediction, label): - event_indicator = torch.ones(label.shape, dtype=torch.bool) - risk = 1 / prediction.squeeze() - cindex = concordance_index_censored(event_indicator.cpu().detach().numpy(), - event_time=label.cpu().detach().numpy(), - estimate=risk.cpu().detach().numpy()) - return cindex diff --git a/submitjob.pbs b/submitjob.pbs index 63f4ddd..fb89aa9 100644 --- a/submitjob.pbs +++ b/submitjob.pbs @@ -32,8 +32,8 @@ conda activate UCL_OP export PYTHONPATH=$PYTHONPATH:/home/dgs1/Software/OutcomePrediction export LS_COLORS="$LS_COLORS:ow=1;34:tw=1;34:" #### ENVIRONMENT VARIABLES ABOVE AUTOMATICALLY ADDED FROM QSUB #### -python Training/Regression.py Configs/Regression/SettingsRTOG_Multi1.ini - +#python Training/Regression.py Configs/Regression/SettingsRTOG_Multi_Apple.ini +python Training/Regression.py Configs/Regression/SettingsRTOG_Multi2.ini ### Run your job here #printenv #python Training/Preprocess.py ConfigDefault.ini diff --git a/submitjob2.pbs b/submitjob2.pbs index b665698..24dc9f7 100644 --- a/submitjob2.pbs +++ b/submitjob2.pbs @@ -32,7 +32,7 @@ conda activate UCL_OP export PYTHONPATH=$PYTHONPATH:/home/dgs1/Software/OutcomePrediction export LS_COLORS="$LS_COLORS:ow=1;34:tw=1;34:" #### ENVIRONMENT VARIABLES ABOVE AUTOMATICALLY ADDED FROM QSUB #### -python Training/Regression.py Configs/Regression/SettingsRTOG_Multi.ini +python Training/Classification.py Configs/Classification/SettingsRTOG_All_Dicom.ini ### Run your job here #printenv From 0ed3794bf7968aa19fe4e18f049e101764783b6c Mon Sep 17 00:00:00 2001 From: Clara Date: Thu, 20 Apr 2023 09:48:58 +0100 Subject: [PATCH 04/12] latest --- .../Classification/tSettingsRTOG_All_PTV.ini~ | 71 ++++++++ .../tSettingsRTOG_Dose_Nii.ini~ | 61 +++++++ DataGenerator/DataGenerator.py | 15 +- ...DataGenerator_PTV.py => DataGenerator2.py} | 0 Models/AutoEncoder2D.py | 87 ---------- Models/AutoEncoder3D.py | 91 ---------- Models/Classifier.py | 6 +- Models/MixModel.py | 16 +- Models/ModelCAE.py | 146 ----------------- Models/ModelCoTr.py | 59 ------- Models/ModelTransUnet.py | 56 ------- Models/PretrainedEncoder3D.py | 64 -------- Models/TransformerEncoder.py | 127 -------------- Models/UnetEDcoder.py | 42 ----- Models/UnetEncoder.py | 27 --- Models/fds.py | 155 ------------------ SubmitScript/submitjob.pbs | 39 +++++ submitjob2.pbs => SubmitScript/submitjob2.pbs | 2 +- SubmitScript/submitjob3.pbs | 39 +++++ SubmitScript/submitjob4.pbs | 40 +++++ SubmitScript/submitjob4.pbs.save | 39 +++++ SubmitScript/submitjob5.pbs | 39 +++++ Training/Classification.py | 20 +-- Training/Regression.py | 144 ---------------- Training/test.py | 114 ++++++++++--- ConvertNii.py => Utils/ConvertNii.py | 0 Count.py => Utils/Count.py | 0 {scripts => Utils}/CropMask.py | 0 {scripts => Utils}/DICOM2NIFTY.py | 0 {scripts => Utils}/DoseNii.py | 0 {scripts => Utils}/FMaskDcm.py | 0 FormMasks.py => Utils/FormMasks.py | 0 FormMasksLocal.py => Utils/FormMasksLocal.py | 0 {scripts => Utils}/PTV.py | 0 {scripts => Utils}/RenameDicomPatientID.py | 0 SegCT.py => Utils/SegCT.py | 0 {scripts => Utils}/check.py | 0 {scripts => Utils/remove}/RTOG.py | 0 {scripts => Utils/remove}/RTOG_nii.py | 0 {scripts => Utils}/renameCT.py | 0 {scripts => Utils}/sub_dose_analysis_total.py | 0 {scripts => Utils}/test.py | 0 .../xnat_data_uniformization.py | 0 temp.png | Bin 0 -> 43220 bytes 44 files changed, 443 insertions(+), 1056 deletions(-) create mode 100644 Configs/Classification/tSettingsRTOG_All_PTV.ini~ create mode 100644 Configs/Classification/tSettingsRTOG_Dose_Nii.ini~ rename DataGenerator/{DataGenerator_PTV.py => DataGenerator2.py} (100%) delete mode 100644 Models/AutoEncoder2D.py delete mode 100644 Models/AutoEncoder3D.py delete mode 100644 Models/ModelCAE.py delete mode 100644 Models/ModelCoTr.py delete mode 100644 Models/ModelTransUnet.py delete mode 100644 Models/PretrainedEncoder3D.py delete mode 100644 Models/TransformerEncoder.py delete mode 100644 Models/UnetEDcoder.py delete mode 100644 Models/UnetEncoder.py delete mode 100644 Models/fds.py create mode 100644 SubmitScript/submitjob.pbs rename submitjob2.pbs => SubmitScript/submitjob2.pbs (98%) create mode 100644 SubmitScript/submitjob3.pbs create mode 100644 SubmitScript/submitjob4.pbs create mode 100644 SubmitScript/submitjob4.pbs.save create mode 100644 SubmitScript/submitjob5.pbs delete mode 100644 Training/Regression.py rename ConvertNii.py => Utils/ConvertNii.py (100%) rename Count.py => Utils/Count.py (100%) rename {scripts => Utils}/CropMask.py (100%) rename {scripts => Utils}/DICOM2NIFTY.py (100%) rename {scripts => Utils}/DoseNii.py (100%) rename {scripts => Utils}/FMaskDcm.py (100%) rename FormMasks.py => Utils/FormMasks.py (100%) rename FormMasksLocal.py => Utils/FormMasksLocal.py (100%) rename {scripts => Utils}/PTV.py (100%) rename {scripts => Utils}/RenameDicomPatientID.py (100%) rename SegCT.py => Utils/SegCT.py (100%) rename {scripts => Utils}/check.py (100%) rename {scripts => Utils/remove}/RTOG.py (100%) rename {scripts => Utils/remove}/RTOG_nii.py (100%) rename {scripts => Utils}/renameCT.py (100%) rename {scripts => Utils}/sub_dose_analysis_total.py (100%) rename {scripts => Utils}/test.py (100%) rename {scripts => Utils}/xnat_data_uniformization.py (100%) create mode 100644 temp.png diff --git a/Configs/Classification/tSettingsRTOG_All_PTV.ini~ b/Configs/Classification/tSettingsRTOG_All_PTV.ini~ new file mode 100644 index 0000000..44e8209 --- /dev/null +++ b/Configs/Classification/tSettingsRTOG_All_PTV.ini~ @@ -0,0 +1,71 @@ +[MODEL] +Activation = 'Sigmoid' +Backbone = 'DenseNet' +Records_Backbone = 'Linear' +batch_size = 10 +Prediction_type = 'Classification' +CT_spatial_dims = 3 +Loss_Function = 'BCEWithLogitsLoss' + +[MODEL_PARAMETERS] +spatial_dims = 3 +in_channels = 3 +out_channels = 512 +block_config = [1,2,4,2] +dropout_prob = 0.15 + +[MODALITY] +CT = '1' +Dose = '1' +Structs = '1' + +[DATA] +Nifty = true +n_classes = 1 +Multichannel = true +dim = [128,128,32] +threshold = 24 +Structs = 'AI_target.nii.gz' +DataFolder = '/home/dgs1/data/Segmentation/Seg/RTOG0617/' +LogFolder = 'Classification/random_seed_75_all_ptv' +vis = [0] +train_size = 0.7 +val_size = 0.3 +target = 'survival_months' +censor_label = 'lost_to_followup' + +[Records] +category_feat = ['arm', 'gender', 'race', 'ethnicity', 'zubrod', + 'histology', 'nonsquam_squam', 'ajcc_stage_grp', 'rt_technique', + 'smoke_hx', 'rx_terminated_ae', 'received_conc_chemo','rt_compliance_ptv90', + 'received_cons_chemo','rt_dose','pet_staging','received_rt'] +numerical_feat = ['age', 'volume_ptv','v5_lung','v20_lung','dmean_lung', + 'v5_heart','v30_heart','v20_esophagus','v60_esophagus'] + + +[CHECKPOINT] +monitor = "val_loss" #"val_acc_epoch" +mode = "max" +matrix = ['ROC', 'Specificity','Sensitivity','Accuracy','Precision','WorstCase'] + + +[SERVER] +Address = 'http://128.16.11.124:8080/xnat' +Projects = ["RTOG_0617"] +User = "yzhan" +Password = "yzhang" +[CRITERIA] +analysis_inclusion = 1 +[FILTER] +patient_id = ['0617-529851', '0617-446756', '0617-647889', '0617-608184', '0617-483479', '0617-381906', '0617-637689', '0617-568854', '0617-444138', + '0617-619629', '0617-698612', '0617-647078', '0617-704484', '0617-482383', '0617-704274', '0617-727320', '0617-674729', '0617-583972', + '0617-309598', '0617-511240', '0617-656006', '0617-658810', '0617-712281', '0617-559978', '0617-761615', '0617-673098', '0617-673681', + '0617-605230', '0617-669208', '0617-607275', '0617-457079', '0617-730769', '0617-449451', '0617-752117', '0617-635346', '0617-657744', + '0617-739625', '0617-736925', '0617-620721', '0617-617572', '0617-438343', '0617-553859', '0617-483282', '0617-690485', '0617-689511', + '0617-575441', '0617-789552', '0617-682647', '0617-445118', '0617-536867', '0617-457446', '0617-592873', '0617-540476', '0617-619079', + '0617-548132', '0617-535250', '0617-654574', '0617-621065', '0617-714849', '0617-613919', '0617-714162', '0617-540662', '0617-711646', + '0617-602544', '0617-693977', '0617-596122', '0617-716741', '0617-466966', '0617-579384', '0617-664444', '0617-469897', '0617-761493', + '0617-747279', '0617-763022', '0617-805653', '0617-640690'] + + + diff --git a/Configs/Classification/tSettingsRTOG_Dose_Nii.ini~ b/Configs/Classification/tSettingsRTOG_Dose_Nii.ini~ new file mode 100644 index 0000000..b56fef9 --- /dev/null +++ b/Configs/Classification/tSettingsRTOG_Dose_Nii.ini~ @@ -0,0 +1,61 @@ +[MODEL] +Activation = 'Sigmoid' +Backbone = 'DenseNet' +Records_Backbone = 'Linear' +batch_size = 10 +Prediction_type = 'Classification' +CT_spatial_dims = 3 +Loss_Function = 'BCEWithLogitsLoss' + +[MODEL_PARAMETERS] +spatial_dims = 3 +in_channels = 2 +out_channels = 512 +block_config = [1,2,4,2] +dropout_prob = 0.3 + +[MODALITY] +Dose = '1' +Structs = '1' + +[DATA] +Nifty = true +n_classes = 1 +Multichannel = true +dim = [128,128,32] +threshold = 24 +Structs = 'masks.nii.gz' +DataFolder = '/home/dgs1/data/Segmentation/Seg/RTOG0617/' +LogFolder = 'Classification/random_seed_75_dose_nii' +vis = [0] +train_size = 0.7 +val_size = 0.3 +target = 'survival_months' +censor_label = 'lost_to_followup' + +[CHECKPOINT] +monitor = "val_loss" #"val_acc_epoch" +mode = "max" +matrix = ['ROC', 'Specificity','Sensitivity','Accuracy','Precision','WorstCase'] + + +[SERVER] +Address = 'http://128.16.11.124:8080/xnat' +Projects = ["RTOG_0617"] +User = "yzhan" +Password = "yzhang" +[CRITERIA] +analysis_inclusion = 1 +[FILTER] +patient_id = ['0617-529851', '0617-446756', '0617-647889', '0617-608184', '0617-483479', '0617-381906', '0617-637689', '0617-568854', '0617-444138', + '0617-619629', '0617-698612', '0617-647078', '0617-704484', '0617-482383', '0617-704274', '0617-727320', '0617-674729', '0617-583972', + '0617-309598', '0617-511240', '0617-656006', '0617-658810', '0617-712281', '0617-559978', '0617-761615', '0617-673098', '0617-673681', + '0617-605230', '0617-669208', '0617-607275', '0617-457079', '0617-730769', '0617-449451', '0617-752117', '0617-635346', '0617-657744', + '0617-739625', '0617-736925', '0617-620721', '0617-617572', '0617-438343', '0617-553859', '0617-483282', '0617-690485', '0617-689511', + '0617-575441', '0617-789552', '0617-682647', '0617-445118', '0617-536867', '0617-457446', '0617-592873', '0617-540476', '0617-619079', + '0617-548132', '0617-535250', '0617-654574', '0617-621065', '0617-714849', '0617-613919', '0617-714162', '0617-540662', '0617-711646', + '0617-602544', '0617-693977', '0617-596122', '0617-716741', '0617-466966', '0617-579384', '0617-664444', '0617-469897', '0617-761493', + '0617-747279', '0617-763022', '0617-805653', '0617-640690'] + + + diff --git a/DataGenerator/DataGenerator.py b/DataGenerator/DataGenerator.py index e19e162..41625be 100644 --- a/DataGenerator/DataGenerator.py +++ b/DataGenerator/DataGenerator.py @@ -48,7 +48,7 @@ def __getitem__(self, i): if 'CT' in self.keys: CTPath = self.SubjectList.loc[i, 'CT_Path'] if self.config['DATA']['Nifty']: - CTPath = Path(CTPath, 'ct.nii.gz') + CTPath = Path(CTPath, 'CT.nii.gz') data['CT'], meta['CT'] = LoadImage()(CTPath) else: data['CT'], meta['CT'] = LoadImage()(CTPath) @@ -64,7 +64,7 @@ def __getitem__(self, i): if 'Dose' in self.keys: DosePath = self.SubjectList.loc[i, 'Dose_Path'] if self.config['DATA']['Nifty']: - DosePath = Path(DosePath, 'dose.nii.gz') + DosePath = Path(DosePath, 'Dose.nii.gz') data['Dose'], meta['Dose'] = LoadImage()(DosePath) data['Dose'] = data['Dose'] / 67 ## Probably need to make it a variable if not self.config['DATA']['Nifty']: @@ -81,7 +81,7 @@ def __getitem__(self, i): if 'Structs' in self.keys: RSPath = self.SubjectList.loc[i, 'Structs_Path'] if self.config['DATA']['Nifty']: - data['Structs'], meta['Structs'] = LoadImage()(Path(RSPath, 'masks.nii.gz')) + data['Structs'], meta['Structs'] = LoadImage()(Path(RSPath, self.config['DATA']['Structs'])) # for roi in self.config['DATA']['Structs']: # data['Struct_' + roi], meta['Struct_' + roi] = LoadImage()(Path(RSPath,roi+'.nii.gz')) @@ -153,7 +153,6 @@ def __getitem__(self, i): if 'threshold' in self.config['DATA'].keys(): ## Classification label = torch.where(label > self.config['DATA']['threshold'], 1, 0) label = torch.as_tensor(label, dtype=torch.float32) - label = (censored_label, label) return data, label @@ -272,10 +271,10 @@ def QuerySubjectInfo(config, SubjectList, session): for i in range(len(SubjectList)): subject_label = SubjectList.loc[i, 'subject_label'] for key in config['MODALITY'].keys(): - #if key == 'Structs': - # SubjectList.loc[i, key + '_Path'] = Path(config['DATA']['DataFolder'], subject_label, 'struct_TS') - #else: - SubjectList.loc[i, key + '_Path'] = Path(config['DATA']['DataFolder'], subject_label) + if key == 'Structs': + SubjectList.loc[i, key + '_Path'] = Path(config['DATA']['DataFolder'], subject_label, 'struct_TS') + else: + SubjectList.loc[i, key + '_Path'] = Path(config['DATA']['DataFolder'], subject_label) else: with ThreadPoolExecutor(max_workers=10) as executor: future_to_url = {executor.submit(get_subject_info, config, session, subjectid) for subjectid in diff --git a/DataGenerator/DataGenerator_PTV.py b/DataGenerator/DataGenerator2.py similarity index 100% rename from DataGenerator/DataGenerator_PTV.py rename to DataGenerator/DataGenerator2.py diff --git a/Models/AutoEncoder2D.py b/Models/AutoEncoder2D.py deleted file mode 100644 index 73c389a..0000000 --- a/Models/AutoEncoder2D.py +++ /dev/null @@ -1,87 +0,0 @@ -import matplotlib.pyplot as plt -from pytorch_lightning import LightningDataModule, LightningModule -import numpy as np -import torch -from collections import Counter -import torchvision -from torchvision import datasets, models, transforms -from torchvision import transforms -from pytorch_lightning.callbacks import ModelCheckpoint, EarlyStopping -from pytorch_lightning import LightningDataModule, LightningModule, Trainer,seed_everything -from torchsummary import summary -import sys -import torchio as tio -import sklearn -from pytorch_lightning import loggers as pl_loggers -import torchmetrics - -## Module - Dataloaders -from Dataloader.Dataloader import DataModule, DataGenerator, LoadSortDataLabel - -## Model -class Classifier2D(LightningModule): - def __init__(self): - super().__init__() - self.n_classes = 1 - self.backbone = models.resnet50(pretrained=True) - self.model= torch.nn.Sequential( - self.unet_model, - torch.nn.LazyLinear(128), - torch.nn.LazyLinear(self.n_classes) - ) - summary(self.model.to('cuda'), (2,160,160,40)) - self.accuracy = torchmetrics.AUC(reorder=True) - self.loss_fcn = torch.nn.BCEWithLogitsLoss() - - def forward(self, x): - return self.model(x) - - def training_step(self, batch,batch_idx): - image,label = batch - prediction = self.forward(image) - loss = self.loss_fcn(prediction.squeeze(), label) - self.log("loss", loss) - return {"loss":loss,"prediction":prediction.squeeze(),"label":label} - - def validation_step(self, batch,batch_idx): - image,label = batch - prediction = self.forward(image) - loss = self.loss_fcn(prediction.squeeze(), label) - return {"loss":loss,"prediction":prediction.squeeze(),"label":label} - - def configure_optimizers(self): - optimizer = torch.optim.Adam(self.parameters(), lr=1e-3) - scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=5, gamma=0.5) - return [optimizer], [scheduler] - -if __name__ == "__main__": - -## Main -train_transform = tio.Compose([ - tio.RandomAffine(), - # tio.RescaleIntensity(out_min_max=(0, 1)) -]) - -val_transform = tio.Compose([ - tio.RandomAffine(), - # tio.RescaleIntensity(out_min_max=(0, 1)) -]) -callbacks = [ - ModelCheckpoint( - dirpath='./', - monitor='val_loss', - filename="model_DeepSurv",#.{epoch:02d}-{val_loss:.2f}.h5", - save_top_k=1, - mode='min'), - EarlyStopping(monitor='val_loss') -] - -data_file = np.load(sys.argv[1]) -label_file = sys.argv[2] -label_name = sys.argv[3] - -data,label = LoadSortDataLabel(label_name, label_file, data_file) -trainer = Trainer(gpus=1, max_epochs=20)#,callbacks=callbacks) -model = DeepSurv() -dataloader = DataModule(data, label, train_transform = train_transform, val_transform = val_transform, batch_size=4, inference=False) -trainer.fit(model, dataloader) diff --git a/Models/AutoEncoder3D.py b/Models/AutoEncoder3D.py deleted file mode 100644 index 27f450e..0000000 --- a/Models/AutoEncoder3D.py +++ /dev/null @@ -1,91 +0,0 @@ -import matplotlib.pyplot as plt -from pytorch_lightning import LightningDataModule, LightningModule -import numpy as np -import torch -from collections import Counter -import torchvision -from torchvision import datasets, models, transforms -from torchvision import transforms -from pytorch_lightning.callbacks import ModelCheckpoint, EarlyStopping -from pytorch_lightning import LightningDataModule, LightningModule, Trainer, seed_everything -from torchsummary import summary -import sys -import torchio as tio -import sklearn -from pytorch_lightning import loggers as pl_loggers -import torchmetrics - -## Module - Dataloaders -from Dataloader.Dataloader import DataModule, DataGenerator, LoadSortDataLabel - - -## Model -class Classifier2D(LightningModule): - def __init__(self): - super().__init__() - self.n_classes = 1 - self.backbone = models.resnet50(pretrained=True) - self.model = torch.nn.Sequential( - self.unet_model, - torch.nn.LazyLinear(128), - torch.nn.LazyLinear(self.n_classes) - ) - summary(self.model.to('cuda'), (2, 160, 160, 40)) - self.accuracy = torchmetrics.AUC(reorder=True) - self.loss_fcn = torch.nn.BCEWithLogitsLoss() - - def forward(self, x): - return self.model(x) - - def training_step(self, batch, batch_idx): - image, label = batch - prediction = self.forward(image) - loss = self.loss_fcn(prediction.squeeze(), label) - self.log("loss", loss) - return {"loss": loss, "prediction": prediction.squeeze(), "label": label} - - def validation_step(self, batch, batch_idx): - image, label = batch - prediction = self.forward(image) - loss = self.loss_fcn(prediction.squeeze(), label) - return {"loss": loss, "prediction": prediction.squeeze(), "label": label} - - def configure_optimizers(self): - optimizer = torch.optim.Adam(self.parameters(), lr=1e-3) - scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=5, gamma=0.5) - return [optimizer], [scheduler] - - -if __name__ == "__main__": - -## Main -train_transform = tio.Compose([ - tio.RandomAffine(), - # tio.RescaleIntensity(out_min_max=(0, 1)) -]) - -val_transform = tio.Compose([ - tio.RandomAffine(), - # tio.RescaleIntensity(out_min_max=(0, 1)) -]) -callbacks = [ - ModelCheckpoint( - dirpath='./', - monitor='val_loss', - filename="model_DeepSurv", - # .{epoch:02d}-{val_loss:.2f}.h5", - save_top_k=1, - mode='min'), - EarlyStopping(monitor='val_loss') -] - -data_file = np.load(sys.argv[1]) -label_file = sys.argv[2] -label_name = sys.argv[3] - -data, label = LoadSortDataLabel(label_name, label_file, data_file) -trainer = Trainer(gpus=1, max_epochs=20) # ,callbacks=callbacks) -model = DeepSurv() -dataloader = DataModule(data, label, train_transform=train_transform, val_transform=val_transform, batch_size=4, - inference=False) -trainer.fit(model, dataloader) \ No newline at end of file diff --git a/Models/Classifier.py b/Models/Classifier.py index 5d2ddbc..16f49e3 100644 --- a/Models/Classifier.py +++ b/Models/Classifier.py @@ -5,8 +5,6 @@ from torch import nn import torchmetrics from monai.networks import blocks, nets -from Models.UnetEncoder import UnetEncoder -from Models.PretrainedEncoder3D import PretrainedEncoder3D ## Model class Classifier(LightningModule): def __init__(self, config, module_str): @@ -23,8 +21,8 @@ def __init__(self, config, module_str): model_str = 'nets.' + model + '(**parameters)' self.backbone = eval(model_str) - self.out_feat = config['MODEL_PARAMETERS']['out_channels'] - + # self.out_feat = config['MODEL_PARAMETERS']['out_channels'] + self.out_feat = config['MODEL_PARAMETERS']['num_classes'] def forward(self, x): return self.backbone(x) diff --git a/Models/MixModel.py b/Models/MixModel.py index 6affdcf..9b888be 100644 --- a/Models/MixModel.py +++ b/Models/MixModel.py @@ -14,13 +14,13 @@ def __init__(self, module_dict, config, loss_fcn=torch.nn.BCEWithLogitsLoss()): self.module_dict = module_dict out_feat = np.sum([model.out_feat for model in module_dict.values()]) self.config = config - self.loss_fcn = getattr(torch.nn, self.config["MODEL"]["Loss_Function"])() + self.loss_fcn = getattr(torch.nn, self.config["MODEL"]["Loss_Function"])(pos_weight=torch.tensor(1.18)) self.activation = getattr(torch.nn, self.config["MODEL"]["Activation"])() self.classifier = nn.Sequential( - nn.Linear(out_feat, 120), - nn.Dropout(0.3), + nn.Linear(out_feat, 120), + nn.Dropout(0.05), nn.Linear(120, 40), - nn.Dropout(0.3), + nn.Dropout(0.05), nn.Linear(40, config['DATA']['n_classes']), self.activation ) @@ -34,7 +34,7 @@ def training_step(self, batch, batch_idx): data_dict, label = batch ## Data_dict is [B, NM, Sx, Sy, Sz, C], Label is [B,(1,1)] prediction = self.forward(data_dict).squeeze(dim=1) loss = self.loss_fcn(prediction, label[-1]) - self.log("train_loss", loss, on_step=False, on_epoch=True, sync_dist=True) + self.log("train_loss", loss, on_step=True, on_epoch=True, sync_dist=True) MAE = torch.abs(prediction - label[-1]) out = copy.deepcopy(data_dict) out['MAE'] = MAE.detach() @@ -65,7 +65,9 @@ def validation_step(self, batch, batch_idx): data_dict, label = batch prediction = self.forward(data_dict).squeeze(dim=1) loss = self.loss_fcn(prediction, label[-1]) - self.log("val_loss", loss, on_step=False, on_epoch=True, sync_dist=True) + self.log("val_loss", loss, on_step=True, on_epoch=True, sync_dist=True) + #acc = nn.MSELoss()(prediction.round(), label[-1]) + #self.log("val_acc", acc, on_step=True, on_epoch=True, sync_dist=True) MAE = torch.abs(prediction - label[-1]) out = copy.deepcopy(data_dict) out['MAE'] = MAE @@ -113,6 +115,6 @@ def weights_reset(self, m): m.reset_parameters() def configure_optimizers(self): - optimizer = torch.optim.Adam(self.parameters(), lr=5e-4, weight_decay=1e-5) + optimizer = torch.optim.Adam(self.parameters(), lr=1e-4, weight_decay=1e-5) scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=5, gamma=0.5) return [optimizer], [scheduler] diff --git a/Models/ModelCAE.py b/Models/ModelCAE.py deleted file mode 100644 index fa527db..0000000 --- a/Models/ModelCAE.py +++ /dev/null @@ -1,146 +0,0 @@ -import matplotlib.pyplot as plt -from pytorch_lightning import LightningDataModule, LightningModule -import numpy as np -import torch -from torch import nn -from collections import Counter -import torchvision -from torchvision import datasets, models, transforms -from torchvision import transforms -from pytorch_lightning.callbacks import ModelCheckpoint, EarlyStopping -from pytorch_lightning import LightningDataModule, LightningModule, Trainer, seed_everything -import sys -import torchio as tio -import sklearn -from pytorch_lightning import loggers as pl_loggers -import torchmetrics -from Losses.loss import WeightedMSE - -## Models -from Models.Linear import Linear -from Models.Classifier2D import Classifier2D -from Models.Classifier3D import Classifier3D -from Models.TransformerEncoder import PositionEncoding, PatchEmbedding, TransformerBlock -from Models.fds import FDS - -# Please refer paper CAE-TRANSFORMER: TRANSFORMER-BASED MODEL TO PREDICT INVASIVENESS -# OF LUNG ADENOCARCINOMA SUBSOLID NODULES FROM NON-THIN SECTION 3D -# CT SCANS - - -class ModelCAE(LightningModule): - def __init__(self, config): - super().__init__() - ## define backbone - # backbone = torchvision.models.resnet18(pretrained=True) - backbone = torch.hub.load('pytorch/vision:v0.10.0', 'densenet121', pretrained=True) - layers = list(backbone.children())[:-1] - self.feature_extractor = nn.Sequential(*layers) - self.feature_extractor.eval() - - for param in self.feature_extractor.parameters(): - param.requires_grad = False - # for param in self.feature_extractor[0][0:10].parameters(): - # param.requires_grad = False - - self.linear1 = nn.LazyLinear(config['transformer_embed_dim']) - # self.FDS = FDS(feature_dim=1024, start_update=0, start_smooth=1, kernel='gaussian', ks=7, sigma=3) - - self.pe = PositionEncoding(img_size=config['img_sizes'], patch_size=config['patch_size'], - in_channel=config['in_channels'], - embed_dim=config['transformer_embed_dim'], dropout=config['dropout'], img_dim=2, iftoken=False) - - self.transformers = nn.ModuleList( - [TransformerBlock(num_heads=config['transformer_head'], embed_dim=config['transformer_embed_dim'], - mlp_dim=config['transformer_mlp_dim'], - dropout=config['dropout']) for _ in range(config['transformer_layer'])]) - self.pool_top = nn.MaxPool2d(4) - - # def WeightedMSE(self, prediction, labels): - # loss = 0 - # for i, label in enumerate(labels): - # idx = (self.label_range == int(label.cpu().numpy())).nonzero() - # if (idx is not None) and (idx[0][0] < 60): - # # print(idx[0][0]) - # loss = loss + (prediction[i] - label) ** 2 * self.weights[idx[0][0]] - # else: - # loss = loss + (prediction[i] - label) ** 2 * self.weights[-1] - # loss = loss / (i + 1) - # return loss - - def convert2d(self, x): - y = x.repeat(1, 3, 1, 1) - features = self.feature_extractor(y) - features = features.flatten(1) - features = features.unsqueeze(0) - features = features.unsqueeze(1) - # features = features.permute(2, 3, 0, 1) - features = self.linear1(features) - return features - - def forward(self, x): - features = torch.cat([self.convert2d(b.transpose(0, 1)) for i, b in enumerate(x)], dim=0) - # features = self.pe(features) - features = features.permute(0, 2, 3, 1).flatten(2) - for transformer in self.transformers: - features = transformer(features) - x = self.pool_top(features) - features = x.flatten(start_dim=1) - return features - - # def training_step(self, batch, batch_idx): - # datadict, label = batch - # forward_cal = self.forward(datadict) - # prediction = forward_cal['prediction'] - # print(prediction, label) - # if self.config['REGULARIZATION']['Label_smoothing']: - # loss = self.WeightedMSE(prediction.squeeze(dim=1), batch[-1]) - # else: - # loss = self.loss_fcn(prediction.squeeze(dim=1), batch[-1]) - # self.log("loss", loss, on_epoch=True) - # out = {'loss': loss, 'features': forward_cal['features'], 'label': label} - # return out - - # def training_epoch_end(self, training_step_outputs): - # if self.config['REGULARIZATION']['Feature_smoothing']: - # training_features = torch.cat([out['features'] for i, out in enumerate(training_step_outputs)], dim=0) - # training_labels = torch.cat([out['label'] for i, out in enumerate(training_step_outputs)], dim=0) - # if self.current_epoch >= 0: - # self.FDS.update_last_epoch_stats(self.current_epoch) - # self.FDS.update_running_stats(training_features, training_labels, self.current_epoch) - - # def validation_step(self, batch, batch_idx): - # datadict, label = batch - # forward_cal = self.forward(datadict, label) - # prediction = forward_cal['prediction'] - # val_loss = self.loss_fcn(prediction.squeeze(dim=1), batch[-1]) - # self.log("val_loss", val_loss, on_epoch=True) - # MAE = torch.abs(prediction.flatten(0) - label) - # out = {'MAE': MAE, 'img': datadict['Anatomy']} - # return out - - # def validation_epoch_end(self, validation_step_outputs): - # worst_MAE = 0 - # for i, data in enumerate(validation_step_outputs): - # loss = data['MAE'] - # idx = torch.argmax(loss) - # if loss[idx] > worst_MAE: - # worst_img = data['img'][idx] - # worst_MAE = loss[idx] - # self.log('worst_MAE', worst_MAE) - # grid = self.generate_report(worst_img) - # self.logger.experiment.add_image('validate_worst_case_img', grid, self.current_epoch) - - # def test_step(self, batch, batch_idx): - # datadict, label = batch - # forward_cal = self.forward(datadict, label) - # prediction = forward_cal['prediction'] - # test_loss = self.loss_fcn(prediction.squeeze(dim=1), batch[-1]) - # print('test_prediction:', prediction, label) - # self.log('test_loss:', test_loss) - # return test_loss - - # def configure_optimizers(self): - # optimizer = torch.optim.Adam(self.parameters(), lr=1e-5) - # scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=5, gamma=0.5) - # return [optimizer], [scheduler] diff --git a/Models/ModelCoTr.py b/Models/ModelCoTr.py deleted file mode 100644 index cd32cdb..0000000 --- a/Models/ModelCoTr.py +++ /dev/null @@ -1,59 +0,0 @@ -import matplotlib.pyplot as plt -from pytorch_lightning import LightningDataModule, LightningModule -import numpy as np -import torch -from torch import nn -from collections import Counter -import torchvision -from torchvision import datasets, models, transforms -from torchvision import transforms -from pytorch_lightning.callbacks import ModelCheckpoint, EarlyStopping -from pytorch_lightning import LightningDataModule, LightningModule, Trainer, seed_everything -import sys -import torchio as tio -import sklearn -from pytorch_lightning import loggers as pl_loggers -import torchmetrics -from Models.UnetEncoder import UnetEncoder -## Models -from Models.Linear import Linear -from Models.Classifier2D import Classifier2D -from Models.Classifier3D import Classifier3D -from Models.TransformerEncoder import PositionEncoding, PatchEmbedding, TransformerBlock - -# Please refer to model CoTr: Efficiently Bridging CNN and Transformer for 3D Medical Image Segmentation. - - -class ModelCoTr(LightningModule): - def __init__(self, config): - super().__init__() - parameters = config['MODEL_PARAMETERS'] - depth = parameters['depth'] - wf = parameters['width'] - self.model = UnetEncoder(**parameters) - self.pe = nn.ModuleList( - [PositionEncoding(img_size=config['img_sizes'][i], patch_size=config['patch_size'], in_channel=2 ** (wf + i), - embed_dim=config['transformer_embed_dim'], - img_dim=3, dropout=config['dropout'], iftoken=True) for i in range(depth)] - ) - self.transformers = nn.ModuleList( - [TransformerBlock(num_heads=config['transformer_head'], embed_dim=config['transformer_embed_dim'], - mlp_dim=config['transformer_mlp_dim'], - dropout=config['dropout']) for _ in range(config['transformer_layer'])]) - - def forward(self, x): - flg = 0 - for i, down in enumerate(self.model.encoder): - x = down(x) - if flg == 0: - feature = self.pe[i](x) - flg = 1 - else: - out_trans = self.pe[i](x) - feature = torch.cat((feature, out_trans), dim=1) - - for transformer in self.transformers: - feature = transformer(feature) - features = feature.flatten(start_dim=1) - - return features diff --git a/Models/ModelTransUnet.py b/Models/ModelTransUnet.py deleted file mode 100644 index 4e8ecb7..0000000 --- a/Models/ModelTransUnet.py +++ /dev/null @@ -1,56 +0,0 @@ -import matplotlib.pyplot as plt -from pytorch_lightning import LightningDataModule, LightningModule -import numpy as np -import torch -from torch import nn -from collections import Counter -import torchvision -from torchvision import datasets, models, transforms -from torchvision import transforms -from pytorch_lightning.callbacks import ModelCheckpoint, EarlyStopping -from pytorch_lightning import LightningDataModule, LightningModule, Trainer, seed_everything -import sys -import torchio as tio -import sklearn -from pytorch_lightning import loggers as pl_loggers -import torchmetrics -from Models.UnetEncoder import UnetEncoder -from torchinfo import summary -## Models -from Models.Linear import Linear -from Models.Classifier2D import Classifier2D -from Models.Classifier3D import Classifier3D -from Models.TransformerEncoder import PositionEncoding, PatchEmbedding, TransformerBlock - -# Please refer to model TransUNet: Transformers Make Strong Encoders for Medical Image Segmentation -# img_sizes=256, patch_size=4, embed_dim=256, in_channels=1, -# num_layers=3, num_heads=8, dropout=0.5, mlp_dim=128 - -class ModelTransUnet(LightningModule): - def __init__(self, config): - super().__init__() - parameters = config['MODEL_PARAMETERS'] - self.model = UnetEncoder(**parameters) - self.model.apply(self.weights_init) - summary(self.model.to('cuda'), (3, 1, 32, 128, 128), col_names=["input_size", "output_size"], depth=5) - - self.pe = PositionEncoding(img_size=config['img_sizes'], patch_size=config['patch_size'], in_channel=config['in_channels'], - embed_dim=config['transformer_embed_dim'], img_dim=3, dropout=config['dropout'], iftoken=True) - self.transformers = nn.ModuleList( - [TransformerBlock(num_heads=config['transformer_head'], embed_dim=config['transformer_embed_dim'], - mlp_dim=config['transformer_mlp_dim'], - dropout=config['dropout']) for _ in range(config['transformer_layer'])]) - - def forward(self, x): - for i, down in enumerate(self.model.encoder): - x = down(x) - feature = self.pe(x) - for transformer in self.transformers: - feature = transformer(feature) - features = feature.flatten(start_dim=1) - - return features - - def weights_init(self, m): - if isinstance(m, nn.Conv3d) or isinstance(m, nn.Linear): - nn.init.xavier_uniform_(m.weight.data) diff --git a/Models/PretrainedEncoder3D.py b/Models/PretrainedEncoder3D.py deleted file mode 100644 index d977481..0000000 --- a/Models/PretrainedEncoder3D.py +++ /dev/null @@ -1,64 +0,0 @@ -import matplotlib.pyplot as plt -import torch -from pytorch_lightning import LightningModule -from torch import nn -import torchmetrics -from monai.networks import blocks, nets -from Models.UnetEncoder import UnetEncoder - - -class PretrainedEncoder3D(LightningModule): - def __init__(self, config, module_str): - super().__init__() - self.n_classes = 1 - model_str = config['MODEL'][module_str + '_Backbone'] - parameters = config[module_str + '_MODEL_PARAMETERS'] - self.config = config - - self.loss_fcn = getattr(torch.nn, self.config["MODEL"]["Loss_Function"])() - self.activation = getattr(torch.nn, self.config["MODEL"]["Activation"])() - - model_str = 'nets.' + model_str + '(**parameters)' - full_model = eval(model_str) - vit_dict = torch.load(config['MODEL'][module_str + '_ckpt_path']) - vit_weights = vit_dict['state_dict'] - model_dict = full_model.state_dict() - vit_weights = {k: v for k, v in vit_weights.items() if k in model_dict} - model_dict.update(vit_weights) - full_model.load_state_dict(model_dict) - del model_dict, vit_weights, vit_dict - - self.backbone = full_model.vit - self.hidden_size = full_model.hidden_size - self.feat_size = full_model.feat_size - self.encoder2 = full_model.encoder2 - self.encoder1 = full_model.encoder1 - - for param in self.backbone.parameters(): - param.requires_grad = False - - for param in self.encoder1.parameters(): - param.requires_grad = False - - for param in self.encoder2.parameters(): - param.requires_grad = False - - self.accuracy = torchmetrics.AUC(reorder=True) - - def forward(self, img): - out1 = self.backbone(img) - enc1 = self.encoder1(img) - f1 = nn.AdaptiveMaxPool3d((1, 1, 48))(enc1) - f1f = f1.flatten(start_dim=1) - enc2 = self.encoder2(self.proj_feat(out1[1][3], self.hidden_size, self.feat_size)) - f2 = nn.AdaptiveAvgPool3d((1, 1, 12))(enc2) - f2f = f2.flatten(start_dim=1) - connect_features = torch.cat((f1f, f2f), dim=1) - return connect_features - - def proj_feat(self, x, hidden_size, feat_size): - new_view = (x.size(0), *feat_size, hidden_size) - x = x.view(new_view) - new_axes = (0, len(x.shape) - 1) + tuple(d + 1 for d in range(len(feat_size))) - x = x.permute(new_axes).contiguous() - return x diff --git a/Models/TransformerEncoder.py b/Models/TransformerEncoder.py deleted file mode 100644 index 5ab26ee..0000000 --- a/Models/TransformerEncoder.py +++ /dev/null @@ -1,127 +0,0 @@ -import torch -import torch.nn as nn - - -class PositionEncoding(nn.Module): - - def __init__(self, img_size, patch_size, in_channel, embed_dim, dropout=0.8, img_dim=3, iftoken=True): - super().__init__() - self.img_size = img_size - self.in_channel = in_channel - self.iftoken = iftoken - self.patch_embed = PatchEmbedding(img_size=img_size, patch_size=patch_size, embed_dim=embed_dim, in_channel=in_channel, img_dim=img_dim) - self.cls_token = nn.Parameter(torch.zeros(1, 1, embed_dim)) - if iftoken: - self.pos_embed = nn.Parameter(torch.zeros(1, self.patch_embed.num_patches + 1, embed_dim)) - else: - self.pos_embed = nn.Parameter(torch.zeros(1, self.patch_embed.num_patches, embed_dim)) - self.pos_drop = nn.Dropout(dropout) - - def forward(self, x): - B = x.shape[0] - x = self.patch_embed(x) - if self.iftoken: - cls_tokens = self.cls_token.expand(B, -1, -1) - x = torch.cat((cls_tokens, x), dim=1) - x = x + self.pos_embed - x = self.pos_drop(x) - return x - - def get_attention_maps(self, x): - attention_maps = [] - for l in self.layers: - _, attn_map = l.self_attn(x, return_attention=True) - attention_maps.append(attn_map) - x = l(x) - return attention_maps - - -class TransformerBlock(nn.Module): - - def __init__(self, num_heads, embed_dim, mlp_dim, dropout=0.0): - """ - Inputs: - input_dim - Dimensionality of the input - num_heads - Number of heads to use in the attention block - dim_feedforward - Dimensionality of the hidden layer in the MLP - dropout - Dropout probability to use in the dropout layers - """ - super().__init__() - - # Attention layer - self.query = nn.Linear(embed_dim, embed_dim) - self.key = nn.Linear(embed_dim, embed_dim) - self.value = self.query = nn.Linear(embed_dim, embed_dim) - - self.attn = nn.MultiheadAttention(embed_dim=embed_dim, num_heads=num_heads, dropout=dropout) - - # Two-layer MLP - self.mlp = nn.Sequential( - nn.Linear(embed_dim, mlp_dim), - nn.GELU(), - nn.Dropout(dropout), - nn.Linear(mlp_dim, embed_dim), - nn.Dropout(dropout), - ) - - # Layers to apply in between the main layers - self.norm0 = nn.LayerNorm(embed_dim) - self.norm1 = nn.LayerNorm(embed_dim) - self.norm2 = nn.LayerNorm(embed_dim) - self.dropout = nn.Dropout(dropout) - - def forward(self, x): - query = self.query(self.norm0(x)) - key = self.key(self.norm0(x)) - value = self.value(self.norm0(x)) - out, attention = self.attn(query, key, value) - x = x + self.dropout(out) - x = self.norm1(x) - - # MLP part - linear_out = self.mlp(x) - x = x + linear_out - x = self.norm2(x) - - return x - -## -#class TransformerRegression(nn.Module): -# def __init__(self, num_layers, input_dim, num_heads, embed_dim, mlp_dim, dropout=0.0, -# input_dropout=0.0): -# super().__init__() -# self.linear_net = nn.Sequential( -# nn.Dropout(input_dim), -# nn.Linear(input_dim, embed_dim) -# ) -# self.transformer = TransformerEncoder(num_layers, num_heads, embed_dim, mlp_dim, dropout=0.0) -# -# def forward(self, x): -# linear_out = self.linear_net(x) -# return self.transformer(linear_out) - - -class PatchEmbedding(nn.Module): - def __init__(self, img_size=64, patch_size=4, embed_dim = 64, in_channel=1, img_dim=3): - super().__init__() - if len(img_size) == 1: - num_patches = (img_size[0] // patch_size) ** 3 ## for 3D image - if len(img_size) == 2: - num_patches = (img_size[0] // patch_size) * (img_size[1] // patch_size) - if len(img_size) == 3: - num_patches = (img_size[0] // patch_size) * (img_size[1] // patch_size) * (img_size[2] // patch_size) - self.img_size = img_size - self.img_dim = img_dim - self.patch_size = patch_size - self.num_patches = int(num_patches) - self.in_channel = in_channel - self.embed_dim = embed_dim - if img_dim == 3: - self.proj = nn.Conv3d(in_channel, embed_dim, kernel_size=patch_size, stride=patch_size) - else: - self.proj = nn.Conv2d(in_channel, embed_dim, kernel_size=patch_size, stride=patch_size) - - def forward(self, x): - #B, C, H, W, D = x.shape - x = self.proj(x).flatten(2).transpose(1, 2) - return x diff --git a/Models/UnetEDcoder.py b/Models/UnetEDcoder.py deleted file mode 100644 index 0f273a7..0000000 --- a/Models/UnetEDcoder.py +++ /dev/null @@ -1,42 +0,0 @@ -import monai.networks.nets -import torch -from torch import nn -from monai.networks import blocks, nets -from pytorch_lightning.callbacks import ModelCheckpoint, EarlyStopping -from pytorch_lightning import LightningDataModule, LightningModule, Trainer, seed_everything - - -class UnetEDcoder(LightningModule): - def __init__(self, config): - super().__init__() - parameters = config['MODEL_PARAMETERS'] - model = eval('monai.networks.nets.BasicUNet(**parameters)') - - self.encoder = nn.Sequential( - model.conv_0, - model.down_1, - model.down_2, - model.down_3, - model.down_4, - ) - self.decoder = nn.Sequential( - model.upcat_4, - model.upcat_3, - model.upcat_2, - model.upcat_1, - model.final_conv, - ) - - def forward(self, x): - x0 = self.encoder[0](x) - x1 = self.encoder[1](x0) - x2 = self.encoder[2](x1) - x3 = self.encoder[3](x2) - x4 = self.encoder[4](x3) - - u4 = self.decoder[0](x4, x3) - u3 = self.decoder[1](u4, x2) - u2 = self.decoder[2](u3, x1) - u1 = self.decoder[3](u2, x0) - out = self.decoder[4](u1) - return out diff --git a/Models/UnetEncoder.py b/Models/UnetEncoder.py deleted file mode 100644 index 798aae6..0000000 --- a/Models/UnetEncoder.py +++ /dev/null @@ -1,27 +0,0 @@ -from torch import nn -from monai.networks import blocks, nets - -class UnetEncoder(nn.Module): - def __init__(self, depth, wf, in_channels, spatial_dims=3, kernel_size=None, stride=None): - super(UnetEncoder, self).__init__() - self.encoder = nn.ModuleList() - for i in range(depth): - out_channels = 2 ** (wf + i) - down_block = blocks.UnetResBlock(spatial_dims=spatial_dims, in_channels=in_channels, - out_channels=out_channels, - kernel_size=kernel_size, - stride=stride, norm_name='batch', dropout=0.5) - self.encoder.append(down_block) - in_channels = out_channels - - self.out_channels = in_channels - - def forward(self, x): - for i, down in enumerate(self.encoder): - x = down(x) - return x - - - def weights_init(self, m): - if isinstance(m, nn.Conv3d) or isinstance(m, nn.Linear): - nn.init.xavier_uniform_(m.weight.data) \ No newline at end of file diff --git a/Models/fds.py b/Models/fds.py deleted file mode 100644 index 0f82167..0000000 --- a/Models/fds.py +++ /dev/null @@ -1,155 +0,0 @@ -import logging -import numpy as np -from scipy.ndimage import gaussian_filter1d -from scipy.signal.windows import triang -import torch -import torch.nn as nn -import torch.nn.functional as F - - -class FDS(nn.Module): - - def __init__(self, feature_dim, bucket_num=50, bucket_start=0, start_update=0, start_smooth=1, - kernel='gaussian', ks=5, sigma=2, momentum=0.9): - super(FDS, self).__init__() - self.feature_dim = feature_dim - self.bucket_num = bucket_num - self.bucket_start = bucket_start - self.kernel_window = self._get_kernel_window(kernel, ks, sigma) - self.half_ks = (ks - 1) // 2 - self.momentum = momentum - self.start_update = start_update - self.start_smooth = start_smooth - - self.register_buffer('epoch', torch.zeros(1).fill_(start_update).cuda()) - self.register_buffer('running_mean', torch.zeros(bucket_num - bucket_start, feature_dim).cuda()) - self.register_buffer('running_var', torch.ones(bucket_num - bucket_start, feature_dim).cuda()) - self.register_buffer('running_mean_last_epoch', torch.zeros(bucket_num - bucket_start, feature_dim).cuda()) - self.register_buffer('running_var_last_epoch', torch.ones(bucket_num - bucket_start, feature_dim).cuda()) - self.register_buffer('smoothed_mean_last_epoch', torch.zeros(bucket_num - bucket_start, feature_dim).cuda()) - self.register_buffer('smoothed_var_last_epoch', torch.ones(bucket_num - bucket_start, feature_dim).cuda()) - self.register_buffer('num_samples_tracked', torch.zeros(bucket_num - bucket_start).cuda()) - - @staticmethod - def _get_kernel_window(kernel, ks, sigma): - assert kernel in ['gaussian', 'triang', 'laplace'] - half_ks = (ks - 1) // 2 - if kernel == 'gaussian': - base_kernel = [0.] * half_ks + [1.] + [0.] * half_ks - base_kernel = np.array(base_kernel, dtype=np.float32) - kernel_window = gaussian_filter1d(base_kernel, sigma=sigma) / sum( - gaussian_filter1d(base_kernel, sigma=sigma)) - elif kernel == 'triang': - kernel_window = triang(ks) / sum(triang(ks)) - else: - laplace = lambda x: np.exp(-abs(x) / sigma) / (2. * sigma) - kernel_window = list(map(laplace, np.arange(-half_ks, half_ks + 1))) / sum( - map(laplace, np.arange(-half_ks, half_ks + 1))) - - print(f'Using FDS: [{kernel.upper()}] ({ks}/{sigma})') - return torch.tensor(kernel_window, dtype=torch.float32).cuda() - - @staticmethod - def calibrate_mean_var(matrix, m1, v1, m2, v2, clip_min=0.1, clip_max=10): - if torch.sum(v1) < 1e-10: - return matrix - if (v1 == 0.).any(): - valid = (v1 != 0.) - factor = torch.clamp(v2[valid] / v1[valid], clip_min, clip_max) - matrix[:, valid] = (matrix[:, valid] - m1[valid]) * torch.sqrt(factor) + m2[valid] - return matrix - - factor = torch.clamp(v2 / v1, clip_min, clip_max) - return (matrix - m1) * torch.sqrt(factor) + m2 - - def _update_last_epoch_stats(self): - self.running_mean_last_epoch = self.running_mean - self.running_var_last_epoch = self.running_var - - self.smoothed_mean_last_epoch = F.conv1d( - input=F.pad(self.running_mean_last_epoch.unsqueeze(1).permute(2, 1, 0), - pad=(self.half_ks, self.half_ks), mode='reflect'), - weight=self.kernel_window.view(1, 1, -1), padding=0 - ).permute(2, 1, 0).squeeze(1) - self.smoothed_var_last_epoch = F.conv1d( - input=F.pad(self.running_var_last_epoch.unsqueeze(1).permute(2, 1, 0), - pad=(self.half_ks, self.half_ks), mode='reflect'), - weight=self.kernel_window.view(1, 1, -1), padding=0 - ).permute(2, 1, 0).squeeze(1) - - def reset(self): - self.running_mean.zero_() - self.running_var.fill_(1) - self.running_mean_last_epoch.zero_() - self.running_var_last_epoch.fill_(1) - self.smoothed_mean_last_epoch.zero_() - self.smoothed_var_last_epoch.fill_(1) - self.num_samples_tracked.zero_() - - def update_last_epoch_stats(self, epoch): - if epoch == self.epoch + 1: - self.epoch += 1 - self._update_last_epoch_stats() - print(f"Updated smoothed statistics on Epoch [{epoch}]!") - - def update_running_stats(self, features, labels, epoch): - if epoch < self.epoch: - return - - assert self.feature_dim == features.size(1), "Input feature dimension is not aligned!" - assert features.size(0) == labels.size(0), "Dimensions of features and labels are not aligned!" - - for label in torch.unique(labels): - if label > self.bucket_num - 1 or label < self.bucket_start: - continue - elif label == self.bucket_start: - curr_feats = features[labels <= label] - elif label == self.bucket_num - 1: - curr_feats = features[labels >= label] - else: - curr_feats = features[labels == label] - curr_num_sample = curr_feats.size(0) - curr_mean = torch.mean(curr_feats, 0) - curr_var = torch.var(curr_feats, 0, unbiased=True if curr_feats.size(0) != 1 else False) - - self.num_samples_tracked[int(label - self.bucket_start)] += curr_num_sample - factor = self.momentum if self.momentum is not None else \ - (1 - curr_num_sample / float(self.num_samples_tracked[int(label - self.bucket_start)])) - factor = 0 if epoch == self.start_update else factor - self.running_mean[int(label - self.bucket_start)] = \ - (1 - factor) * curr_mean + factor * self.running_mean[int(label - self.bucket_start)] - self.running_var[int(label - self.bucket_start)] = \ - (1 - factor) * curr_var + factor * self.running_var[int(label - self.bucket_start)] - - print(f"Updated running statistics with Epoch [{epoch}] features!") - - def smooth(self, features, labels, epoch): - if epoch < self.start_smooth: - return features - - # labels = labels.squeeze(1) - for label in torch.unique(labels): - if label > self.bucket_num - 1 or label < self.bucket_start: - continue - elif label == self.bucket_start: - features[labels <= label] = self.calibrate_mean_var( - features[labels <= label], - self.running_mean_last_epoch[int(label - self.bucket_start)], - self.running_var_last_epoch[int(label - self.bucket_start)], - self.smoothed_mean_last_epoch[int(label - self.bucket_start)], - self.smoothed_var_last_epoch[int(label - self.bucket_start)]) - elif label == self.bucket_num - 1: - features[labels >= label] = self.calibrate_mean_var( - features[labels >= label], - self.running_mean_last_epoch[int(label - self.bucket_start)], - self.running_var_last_epoch[int(label - self.bucket_start)], - self.smoothed_mean_last_epoch[int(label - self.bucket_start)], - self.smoothed_var_last_epoch[int(label - self.bucket_start)]) - else: - features.cuda()[labels == label] = self.calibrate_mean_var( - features.cuda()[labels == label].cuda(), - self.running_mean_last_epoch[int(label - self.bucket_start)].cuda(), - self.running_var_last_epoch[int(label - self.bucket_start)].cuda(), - self.smoothed_mean_last_epoch[int(label - self.bucket_start)].cuda(), - self.smoothed_var_last_epoch[int(label - self.bucket_start)].cuda()).cuda() - return features diff --git a/SubmitScript/submitjob.pbs b/SubmitScript/submitjob.pbs new file mode 100644 index 0000000..fb89aa9 --- /dev/null +++ b/SubmitScript/submitjob.pbs @@ -0,0 +1,39 @@ +#!/bin/bash +### Job Name +#PBS -N OP +### Project code +#PBS -A AI_OutcomePrediction +### Maximum time this job can run before being killed (here, 1 day) +#PBS -l walltime=15:00:00:00 +### Resource Request (must contain cpucore, memory, and gpu (even if requested amount is zero) +#PBS -l cpucore=30:memory=200gb:gpu=4 +### Output Options (default is stdout_and_stderr) +#PBS -l outputMode=stdout_and_stderr +##PBS -l outputMode=no_output +##PBS -l outputMode=stdout_only +##PBS -l outputMode=stderr_only + +#### ENVIRONMENT VARIABLES BELOW AUTOMATICALLY ADDED FROM QSUB #### +# >>> conda initialize >>> +# !! Contents within this block are managed by 'conda init' !! +__conda_setup="$('/home/dgs1/anaconda3/bin/conda' 'shell.bash' 'hook' 2> /dev/null)" +if [ $? -eq 0 ]; then + eval "$__conda_setup" +else + if [ -f "/home/dgs1/anaconda3/etc/profile.d/conda.sh" ]; then + . "/home/dgs1/anaconda3/etc/profile.d/conda.sh" + else + export PATH="/home/dgs1/anaconda3/bin:$PATH" + fi +fi +unset __conda_setup +# <<< conda initialize <<< +conda activate UCL_OP +export PYTHONPATH=$PYTHONPATH:/home/dgs1/Software/OutcomePrediction +export LS_COLORS="$LS_COLORS:ow=1;34:tw=1;34:" +#### ENVIRONMENT VARIABLES ABOVE AUTOMATICALLY ADDED FROM QSUB #### +#python Training/Regression.py Configs/Regression/SettingsRTOG_Multi_Apple.ini +python Training/Regression.py Configs/Regression/SettingsRTOG_Multi2.ini +### Run your job here +#printenv +#python Training/Preprocess.py ConfigDefault.ini diff --git a/submitjob2.pbs b/SubmitScript/submitjob2.pbs similarity index 98% rename from submitjob2.pbs rename to SubmitScript/submitjob2.pbs index 24dc9f7..ffff6e4 100644 --- a/submitjob2.pbs +++ b/SubmitScript/submitjob2.pbs @@ -32,7 +32,7 @@ conda activate UCL_OP export PYTHONPATH=$PYTHONPATH:/home/dgs1/Software/OutcomePrediction export LS_COLORS="$LS_COLORS:ow=1;34:tw=1;34:" #### ENVIRONMENT VARIABLES ABOVE AUTOMATICALLY ADDED FROM QSUB #### -python Training/Classification.py Configs/Classification/SettingsRTOG_All_Dicom.ini +python Training/Classification.py Configs/Classification/SettingsRTOG_All_PTV2.ini ### Run your job here #printenv diff --git a/SubmitScript/submitjob3.pbs b/SubmitScript/submitjob3.pbs new file mode 100644 index 0000000..001bada --- /dev/null +++ b/SubmitScript/submitjob3.pbs @@ -0,0 +1,39 @@ +#!/bin/bash +### Job Name +#PBS -N OP +### Project code +#PBS -A AI_OutcomePrediction +### Maximum time this job can run before being killed (here, 1 day) +#PBS -l walltime=15:00:00:00 +### Resource Request (must contain cpucore, memory, and gpu (even if requested amount is zero) +#PBS -l cpucore=30:memory=200gb:gpu=4 +### Output Options (default is stdout_and_stderr) +#PBS -l outputMode=stdout_and_stderr +##PBS -l outputMode=no_output +##PBS -l outputMode=stdout_only +##PBS -l outputMode=stderr_only + +#### ENVIRONMENT VARIABLES BELOW AUTOMATICALLY ADDED FROM QSUB #### +# >>> conda initialize >>> +# !! Contents within this block are managed by 'conda init' !! +__conda_setup="$('/home/dgs1/anaconda3/bin/conda' 'shell.bash' 'hook' 2> /dev/null)" +if [ $? -eq 0 ]; then + eval "$__conda_setup" +else + if [ -f "/home/dgs1/anaconda3/etc/profile.d/conda.sh" ]; then + . "/home/dgs1/anaconda3/etc/profile.d/conda.sh" + else + export PATH="/home/dgs1/anaconda3/bin:$PATH" + fi +fi +unset __conda_setup +# <<< conda initialize <<< +conda activate UCL_OP +export PYTHONPATH=$PYTHONPATH:/home/dgs1/Software/OutcomePrediction +export LS_COLORS="$LS_COLORS:ow=1;34:tw=1;34:" +#### ENVIRONMENT VARIABLES ABOVE AUTOMATICALLY ADDED FROM QSUB #### +python Training/Classification.py Configs/Classification/SettingsRTOG_All_PTV3.ini + +### Run your job here +#printenv +#python Training/Preprocess.py ConfigDefault.ini diff --git a/SubmitScript/submitjob4.pbs b/SubmitScript/submitjob4.pbs new file mode 100644 index 0000000..38fac5f --- /dev/null +++ b/SubmitScript/submitjob4.pbs @@ -0,0 +1,40 @@ +#!/bin/bash +### Job Name +#PBS -N OP +### Project code +#PBS -A AI_OutcomePrediction +### Maximum time this job can run before being killed (here, 1 day) +#PBS -l walltime=15:00:00:00 +### Resource Request (must contain cpucore, memory, and gpu (even if requested amount is zero) +#PBS -l cpucore=30:memory=200gb:gpu=4 +### Output Options (default is stdout_and_stderr) +#PBS -l outputMode=stdout_and_stderr +##PBS -l outputMode=no_output +##PBS -l outputMode=stdout_only +##PBS -l outputMode=stderr_only + +#### ENVIRONMENT VARIABLES BELOW AUTOMATICALLY ADDED FROM QSUB #### +# >>> conda initialize >>> +# !! Contents within this block are managed by 'conda init' !! +__conda_setup="$('/home/dgs1/anaconda3/bin/conda' 'shell.bash' 'hook' 2> /dev/null)" +if [ $? -eq 0 ]; then + eval "$__conda_setup" +else + if [ -f "/home/dgs1/anaconda3/etc/profile.d/conda.sh" ]; then + . "/home/dgs1/anaconda3/etc/profile.d/conda.sh" + else + export PATH="/home/dgs1/anaconda3/bin:$PATH" + fi +fi +unset __conda_setup +# <<< conda initialize <<< +conda activate UCL_OP +export PYTHONPATH=$PYTHONPATH:/home/dgs1/Software/OutcomePrediction +export LS_COLORS="$LS_COLORS:ow=1;34:tw=1;34:" +#### ENVIRONMENT VARIABLES ABOVE AUTOMATICALLY ADDED FROM QSUB #### +python Training/Classification.py Configs/Classification/SettingsRTOG_CT_Resnet.ini + +### Run your job here +#printenv +#python Training/Preprocess.py ConfigDefault.ini + diff --git a/SubmitScript/submitjob4.pbs.save b/SubmitScript/submitjob4.pbs.save new file mode 100644 index 0000000..72b47d1 --- /dev/null +++ b/SubmitScript/submitjob4.pbs.save @@ -0,0 +1,39 @@ +#!/bin/bash +### Job Name +#PBS -N OP +### Project code +#PBS -A AI_OutcomePrediction +### Maximum time this job can run before being killed (here, 1 day) +#PBS -l walltime=15:00:00:00 +### Resource Request (must contain cpucore, memory, and gpu (even if requested amount is zero) +#PBS -l cpucore=30:memory=200gb:gpu=4 +### Output Options (default is stdout_and_stderr) +#PBS -l outputMode=stdout_and_stderr +##PBS -l outputMode=no_output +##PBS -l outputMode=stdout_only +##PBS -l outputMode=stderr_only + +#### ENVIRONMENT VARIABLES BELOW AUTOMATICALLY ADDED FROM QSUB #### +# >>> conda initialize >>> +# !! Contents within this block are managed by 'conda init' !! +__conda_setup="$('/home/dgs1/anaconda3/bin/conda' 'shell.bash' 'hook' 2> /dev/null)" +if [ $? -eq 0 ]; then + eval "$__conda_setup" +else + if [ -f "/home/dgs1/anaconda3/etc/profile.d/conda.sh" ]; then + . "/home/dgs1/anaconda3/etc/profile.d/conda.sh" + else + export PATH="/home/dgs1/anaconda3/bin:$PATH" + fi +fi +unset __conda_setup +# <<< conda initialize <<< +conda activate UCL_OP +export PYTHONPATH=$PYTHONPATH:/home/dgs1/Software/OutcomePrediction +export LS_COLORS="$LS_COLORS:ow=1;34:tw=1;34:" +#### ENVIRONMENT VARIABLES ABOVE AUTOMATICALLY ADDED FROM QSUB #### +python Training/Classification.py Configs/Classification/SettingsRTOG_CT.ini + +### Run your job here +#printenv +#python Training/Preprocess.py ConfigDefault.ini diff --git a/SubmitScript/submitjob5.pbs b/SubmitScript/submitjob5.pbs new file mode 100644 index 0000000..dc366e2 --- /dev/null +++ b/SubmitScript/submitjob5.pbs @@ -0,0 +1,39 @@ +#!/bin/bash +### Job Name +#PBS -N OP +### Project code +#PBS -A AI_OutcomePrediction +### Maximum time this job can run before being killed (here, 1 day) +#PBS -l walltime=15:00:00:00 +### Resource Request (must contain cpucore, memory, and gpu (even if requested amount is zero) +#PBS -l cpucore=30:memory=200gb:gpu=4 +### Output Options (default is stdout_and_stderr) +#PBS -l outputMode=stdout_and_stderr +##PBS -l outputMode=no_output +##PBS -l outputMode=stdout_only +##PBS -l outputMode=stderr_only + +#### ENVIRONMENT VARIABLES BELOW AUTOMATICALLY ADDED FROM QSUB #### +# >>> conda initialize >>> +# !! Contents within this block are managed by 'conda init' !! +__conda_setup="$('/home/dgs1/anaconda3/bin/conda' 'shell.bash' 'hook' 2> /dev/null)" +if [ $? -eq 0 ]; then + eval "$__conda_setup" +else + if [ -f "/home/dgs1/anaconda3/etc/profile.d/conda.sh" ]; then + . "/home/dgs1/anaconda3/etc/profile.d/conda.sh" + else + export PATH="/home/dgs1/anaconda3/bin:$PATH" + fi +fi +unset __conda_setup +# <<< conda initialize <<< +conda activate UCL_OP +export PYTHONPATH=$PYTHONPATH:/home/dgs1/Software/OutcomePrediction +export LS_COLORS="$LS_COLORS:ow=1;34:tw=1;34:" +#### ENVIRONMENT VARIABLES ABOVE AUTOMATICALLY ADDED FROM QSUB #### +python Training/Classification.py Configs/Classification/SettingsRTOG_All_PTV.ini + +### Run your job here +#printenv +#python Training/Preprocess.py ConfigDefault.ini diff --git a/Training/Classification.py b/Training/Classification.py index 3c1d4b1..f1f0edd 100644 --- a/Training/Classification.py +++ b/Training/Classification.py @@ -8,7 +8,7 @@ import monai torch.cuda.empty_cache() ## Module - Dataloaders -from DataGenerator.DataGenerator_PTV import * +from DataGenerator.DataGenerator import * from Models.Classifier import Classifier from Models.Linear import Linear from Models.MixModel import MixModel @@ -48,7 +48,7 @@ val_transform = torchvision.transforms.Compose([ EnsureChannelFirstd(keys=img_keys), - ResampleToMatchd(list(set(img_keys).difference(set(['CT']))), key_dst='CT'), + #ResampleToMatchd(list(set(img_keys).difference(set(['CT']))), key_dst='CT'), monai.transforms.ScaleIntensityd(list(set(img_keys).difference(set(['Dose'])))), #monai.transforms.ResizeWithPadOrCropd(img_keys, spatial_size=config['DATA']['dim']), monai.transforms.Resized(keys=img_keys, spatial_size=config['DATA']['dim']), @@ -58,9 +58,7 @@ val_transform = None ## First Connect to XNAT -session = xnat.connect(config['SERVER']['Address'], user=config['SERVER']['User'],password=config['SERVER']['Password']) - - +session = xnat.connect(config['SERVER']['Address'], user=config['SERVER']['User'],password=config['SERVER']['Password']) SubjectList = QuerySubjectList(config, session) SynchronizeData(config, SubjectList) SubjectList.dropna(subset=['xnat_subjectdata_field_map_survival_months'], inplace=True) @@ -91,9 +89,9 @@ rd = [53414, 88536, 89901, 62594, 13787, 21781, 18215, 4182, 10695, 61645, 93967, 35446, 41063, 98435, 94558, 67665, 98831, 76684, 33670, 66239, 24417, 29551, 68018, 52785, 41160, 60264, 75053, 58354, 55180, 58358, 51182, 8260] -for iter in range(0, 25, 1): - seed_everything(rd[iter],workers=True) - +for iter in range(0, 15, 1): + #seed_everything(rd[iter],workers=True) + seed_everything(np.random.randint(0, 10000), workers=True) dataloader = DataModule(SubjectList, config=config, keys=config['MODALITY'].keys(), @@ -114,9 +112,9 @@ logger._version = iter callbacks = [ ModelCheckpoint(dirpath=Path(logger.log_dir, 'ckpt'), - monitor='val_loss', + monitor='val_loss_epoch', filename='Iter_' + str(iter), - save_top_k=3, + save_top_k=2, mode='min'), # EarlyStopping(monitor='val_loss', # check_finite=True), @@ -133,7 +131,7 @@ ) #model = torch.compile(model) trainer.fit(model, dataloader) - torch.save({'state_dict': model.state_dict(),}, Path(logger.log_dir, 'Iter_' + str(iter) + '.ckpt')) + #torch.save({'state_dict': model.state_dict(),}, Path(logger.log_dir, 'Iter_' + str(iter) + '.ckpt')) with open(logger.root_dir + "/Config.ini", "w+") as toml_file: toml.dump(config, toml_file) diff --git a/Training/Regression.py b/Training/Regression.py deleted file mode 100644 index 76c4296..0000000 --- a/Training/Regression.py +++ /dev/null @@ -1,144 +0,0 @@ -import torch -import torchvision -from torch import nn -from pytorch_lightning.callbacks import ModelCheckpoint, EarlyStopping -from pytorch_lightning.strategies import DDPStrategy -from pytorch_lightning import LightningDataModule, LightningModule, Trainer, seed_everything -import sys, os -import monai -torch.cuda.empty_cache() -## Module - Dataloaders -from DataGenerator.DataGenerator import * -from Models.Classifier import Classifier -from Models.Linear import Linear -from Models.MixModel import MixModel -from monai.transforms import EnsureChannelFirstd, ScaleIntensityd, ResampleToMatchd -## Main -from sklearn.preprocessing import StandardScaler, OneHotEncoder -import toml -from Utils.GenerateSmoothLabel import get_smoothed_label_distribution, get_module -from Utils.PredictionReports import PredictionReports -from pathlib import Path -from torchmetrics import ConfusionMatrix -import torchmetrics - -config = toml.load(sys.argv[1]) - -## 2D transform -img_keys = list(config['MODALITY'].keys()) -## Multichannel masks -#img_keys.remove('Structs') -#if 'Structs' in config['DATA'].keys(): -# for roi in config['DATA']['Structs']: -# img_keys.append('Struct_' + roi) - -if config['MODALITY'].values(): - train_transform = torchvision.transforms.Compose([ - EnsureChannelFirstd(keys=img_keys), - #ResampleToMatchd(list(set(img_keys).difference(set(['CT']))), key_dst='CT'), - monai.transforms.ScaleIntensityd(keys=list(set(img_keys).difference(set(['Dose'])))), - #monai.transforms.ResizeWithPadOrCropd(keys=img_keys, spatial_size=config['DATA']['dim']), - monai.transforms.Resized(keys=img_keys, spatial_size=config['DATA']['dim']), - monai.transforms.RandAffined(keys=img_keys), - monai.transforms.RandHistogramShiftd(keys=img_keys), - monai.transforms.RandAdjustContrastd(keys=img_keys), - monai.transforms.RandGaussianNoised(keys=img_keys), - - ]) - - val_transform = torchvision.transforms.Compose([ - EnsureChannelFirstd(keys=img_keys), - #ResampleToMatchd(list(set(img_keys).difference(set(['CT']))), key_dst='CT'), - monai.transforms.ScaleIntensityd(list(set(img_keys).difference(set(['Dose'])))), - #monai.transforms.ResizeWithPadOrCropd(img_keys, spatial_size=config['DATA']['dim']), - monai.transforms.Resized(keys=img_keys, spatial_size=config['DATA']['dim']), - ]) -else: - train_transform = None - val_transform = None - -## First Connect to XNAT -session = xnat.connect(config['SERVER']['Address'], user=config['SERVER']['User'],password=config['SERVER']['Password']) - - -SubjectList = QuerySubjectList(config, session) -SynchronizeData(config, SubjectList) -SubjectList.dropna(subset=['xnat_subjectdata_field_map_survival_months'], inplace=True) -# SubjectList.dropna(subset=['xnat_subjectdata_field_map_overall_stage'], inplace=True) - -module_dict = nn.ModuleDict() -if config['DATA']['Multichannel']: ## Single-Model Multichannel learning - if config['MODALITY'].keys(): - module_dict['Image'] = Classifier(config, 'Image') -else: - for key in config['MODALITY'].keys():# Multi-Model Single Channel learning - module_dict[key] = Classifier(config, key) - -if 'Records' in config.keys(): - SubjectList, clinical_cols = LoadClinicalData(config, SubjectList) - module_dict['Records'] = Linear(in_feat=len(clinical_cols), out_feat=42) -else: - clinical_cols = None - -## GeneratePath -for key in config['MODALITY'].keys(): - SubjectList[key+'_Path'] = "" -QuerySubjectInfo(config, SubjectList, session) -print(SubjectList) - -# threshold = config['DATA']['threshold'] -# ckpt_path = Path('./lightning_logs', total_backbone, 'ckpt') -rd = [53414, 88536, 89901, 62594, 13787, 21781, 18215, 4182, 10695, 61645, 93967, 35446, 41063, 98435, 94558, 67665, - 98831, 76684, 33670, 66239, 24417, 29551, 68018, 52785, 41160, 60264, 75053, 58354, 55180, 58358, 51182, 8260] - -for iter in range(0, 2, 1): - seed_everything(rd[iter],workers=True) - - dataloader = DataModule(SubjectList, - config=config, - keys=config['MODALITY'].keys(), - train_transform=train_transform, - val_transform=val_transform, - clinical_cols=clinical_cols, - inference=False) - - model = MixModel(module_dict, config) - model.apply(model.weights_reset) - #full_ckpt_path = Path(ckpt_path, 'Iter_'+ str(iter) + '.ckpt') - #model.load_state_dict(torch.load(full_ckpt_path)['state_dict']) - - filename = config['DATA']['LogFolder'] - - logger = PredictionReports(config=config, save_dir='lightning_logs', name=filename) - logger.log_text() - logger._version = iter - callbacks = [ - ModelCheckpoint(dirpath=Path(logger.log_dir, 'ckpt'), - monitor='val_loss', - filename='Iter_' + str(iter), - save_top_k=5, - mode='min'), - # EarlyStopping(monitor='val_loss', - # check_finite=True), - ] - - trainer = Trainer( - #gpus=1, - accelerator="gpu", - devices=[0,1], - strategy=DDPStrategy(find_unused_parameters=True), - max_epochs=30, - logger=logger, - callbacks=callbacks, - ) - #model = torch.compile(model) - trainer.fit(model, dataloader) - torch.save({'state_dict': model.state_dict(),}, Path(logger.log_dir, 'Iter_' + str(iter) + '.ckpt')) - -with open(logger.root_dir + "/Config.ini", "w+") as toml_file: - toml.dump(config, toml_file) - toml_file.write("Train transform:\n") - toml_file.write(str(train_transform)) - toml_file.write("Val/Test transform:\n") - toml_file.write(str(val_transform)) - diff --git a/Training/test.py b/Training/test.py index 86c44c6..247157a 100644 --- a/Training/test.py +++ b/Training/test.py @@ -2,15 +2,14 @@ import torchvision from torch import nn from pytorch_lightning.callbacks import ModelCheckpoint, EarlyStopping -from pytorch_lightning.strategies import DDPStrategy from pytorch_lightning import LightningDataModule, LightningModule, Trainer, seed_everything import sys, os # import torchio as tio import monai - +import numpy as np torch.cuda.empty_cache() ## Module - Dataloaders -from DataGenerator.DataGenerator_PTV import * +from DataGenerator.DataGenerator import * from Models.Classifier import Classifier from Models.Linear import Linear from Models.MixModel import MixModel @@ -24,7 +23,7 @@ # import torchio as tio from torchmetrics import ConfusionMatrix import torchmetrics - +import matplotlib.pyplot as plt config = toml.load(sys.argv[1]) ## 2D transform img_keys = list(config['MODALITY'].keys()) @@ -76,7 +75,7 @@ else: clinical_cols = None -print('clinical_cols:', len(clinical_cols)) +# print('clinical_cols:', len(clinical_cols)) ## GeneratePath for key in config['MODALITY'].keys(): SubjectList[key + '_Path'] = "" @@ -96,8 +95,8 @@ base_fpr = np.linspace(0, 1, 39) cm = ConfusionMatrix(num_classes=2) prediction_labels_full_list = [] -bidx = [21, 25, 0, 4, 6] -for iter in range(0, 5, 1): +bidx = [1, 21, 25, 26, 30] +for iter in range(0, 12, 1): # iter = bidx[it] # seed_everything(4200) dataloader = DataModule(SubjectList, @@ -106,25 +105,15 @@ train_transform=train_transform, val_transform=val_transform, clinical_cols=clinical_cols, - inference=False, - train_size=0.85) - + inference=False) logger = PredictionReports(config=config, save_dir='lightning_logs/test', name= config['DATA']['LogFolder']) model = MixModel(module_dict, config) filename = 'lightning_logs/' + config['DATA']['LogFolder'] + '/version_' + str(iter) - full_ckpt_path = Path(filename, 'Iter_' + str(iter) + '.ckpt') - # full_ckpt_path = Path(ckpt_path, 'ckpt', 'Iter_' + str(iter) + '.ckpt') - # full_ckpt_path = 'Iter_' + str(iter) + '.ckpt' + # full_ckpt_path = Path(filename, 'Iter_' + str(iter) + '.ckpt') + full_ckpt_path = Path(filename, 'ckpt', 'Iter_' + str(iter) + '.ckpt') model.load_state_dict(torch.load(full_ckpt_path)['state_dict']) - #model = MixModel(module_dict, config) - #filename = 'lightning_logs/' + config['DATA']['LogFolder'] + '/version_' + str(iter) - #full_ckpt_path = Path(filename, 'Iter_' + str(iter) + '.ckpt') - # full_ckpt_path = Path(ckpt_path, 'ckpt', 'Iter_' + str(iter) + '.ckpt') - # full_ckpt_path = 'Iter_' + str(iter) + '.ckpt' - #model.load_state_dict(torch.load(full_ckpt_path)['state_dict']) - # model.load_state_dict(torch.load(full_ckpt_path, map_location='cpu')['state_dict']) model.eval() print('start testing...') worstCase = 0 @@ -140,7 +129,32 @@ validation_censor_full = torch.cat([out['label'][0] for i, out in enumerate(outs)], dim=0) prediction_labels_full = torch.cat([out['prediction'] for i, out in enumerate(outs)], dim=0) roc_i = auroc(prediction_labels_full, validation_labels_full.int()) + roc_list.append(roc_i) print('roc_' + str(iter), roc_i) + fpr, tpr, _ = roc(prediction_labels_full, validation_labels_full) + #if iter == 1: + # plt.plot(fpr, tpr, 'b', alpha=0.15, label='ROC of each bootstrap') + #else: + # plt.plot(fpr, tpr, 'b', alpha=0.15) + tpr = np.interp(base_fpr, fpr, tpr) + tpr[0] = 0.0 + tprs.append(tpr) + + bcm = cm(prediction_labels_full.round(), validation_labels_full.int()) + tn = bcm[0][0] + tp = bcm[1][1] + fp = bcm[0][1] + fn = bcm[1][0] + + acc = bcm.diag().sum() / bcm.sum() + sensitivity = tp / (tp + fn) + precision = tp / (tp + fp) + spec = tn / (tn + fp) + sp_list.append(spec) + sensi_list.append(sensitivity) + acc_list.append(acc) + pre_list.append(precision) + prediction_labels_full_list.append(prediction_labels_full.tolist()) logger.report_test(config, outs, model, prediction_labels_full, [validation_censor_full, validation_labels_full], 'test_') with open(logger.log_dir + "/test_record.ini", "a") as toml_file: @@ -157,8 +171,28 @@ prediction_labels = torch.tensor(prediction_labels_full_list).mean(dim=0) validation_labels = validation_labels_full -#print(prediction_labels_full_list) -#print(validation_labels) +mean_tprs = np.array(tprs).mean(axis=0) +std = np.array(tprs).std(axis=0) +tprs_upper = np.minimum(mean_tprs + std, 1) +tprs_lower = mean_tprs - std + +plt.plot(base_fpr, mean_tprs, 'darkorange', label='Bootstrap-averaged ROC') +plt.legend(facecolor=[230/255, 236/255, 237/255]) +plt.fill_between(base_fpr, tprs_lower, tprs_upper, color='grey', alpha=0.3) +plt.plot(base_fpr, base_fpr, 'r--') +plt.title('Test ROC') +plt.xlabel('False Positive Rate') +plt.xlim((0, 1)) +plt.ylabel('True Positive rate') +plt.ylim((0, 1)) +plt.show() + +print('roc_list: ', roc_list) +print('spec:', sp_list) +print('sens:', sensi_list) +print('acc',acc_list) +fig.savefig('temp.png', dpi=fig.dpi, transparent=True) + roc = auroc(prediction_labels, validation_labels.int()) bcm = cm(prediction_labels.round(), validation_labels.int()) tn = bcm[0][0] @@ -170,9 +204,35 @@ precision = tp / (tp + fp) spec = tn / (tn + fp) -print('avg_roc', str(roc)) -print('avg_specificity', str(spec)) -print('avg_sensitivity', str(sensitivity)) -print('avg_accuracy', str(acc)) -print('avg_precision', str(precision)) +print('enm_roc', str(roc)) +print('enm_specificity', str(spec)) +print('enm_sensitivity', str(sensitivity)) +print('enm_accuracy', str(acc)) +print('enm_precision', str(precision)) print('finish test') + + +roc_avg = torch.mean(torch.tensor(roc_list)) +sp_avg = torch.mean(torch.tensor(sp_list)) +sensi_avg = torch.mean(torch.tensor(sensi_list)) +acc_avg = torch.mean(torch.tensor(acc_list)) +pre_avg = torch.mean(torch.tensor(pre_list)) + + +roc_std = torch.std(torch.tensor(roc_list), unbiased=False) +sp_std = torch.std(torch.tensor(sp_list), unbiased=False) +sensi_std = torch.std(torch.tensor(sensi_list), unbiased=False) +acc_std = torch.std(torch.tensor(acc_list), unbiased=False) +pre_std = torch.std(torch.tensor(pre_list), unbiased=False) + +print('avg_roc', str(roc_avg)) +print('avg_specificity', str(sp_avg)) +print('avg_sensitivity', str(sensi_avg)) +print('avg_accuracy', str(acc_avg)) +print('avg_precision', str(pre_avg)) + +print('std_roc', str(roc_std)) +print('std_specificity', str(sp_std)) +print('std_sensitivity', str(sensi_std)) +print('std_accuracy', str(acc_std)) +print('std_precision', str(pre_std)) diff --git a/ConvertNii.py b/Utils/ConvertNii.py similarity index 100% rename from ConvertNii.py rename to Utils/ConvertNii.py diff --git a/Count.py b/Utils/Count.py similarity index 100% rename from Count.py rename to Utils/Count.py diff --git a/scripts/CropMask.py b/Utils/CropMask.py similarity index 100% rename from scripts/CropMask.py rename to Utils/CropMask.py diff --git a/scripts/DICOM2NIFTY.py b/Utils/DICOM2NIFTY.py similarity index 100% rename from scripts/DICOM2NIFTY.py rename to Utils/DICOM2NIFTY.py diff --git a/scripts/DoseNii.py b/Utils/DoseNii.py similarity index 100% rename from scripts/DoseNii.py rename to Utils/DoseNii.py diff --git a/scripts/FMaskDcm.py b/Utils/FMaskDcm.py similarity index 100% rename from scripts/FMaskDcm.py rename to Utils/FMaskDcm.py diff --git a/FormMasks.py b/Utils/FormMasks.py similarity index 100% rename from FormMasks.py rename to Utils/FormMasks.py diff --git a/FormMasksLocal.py b/Utils/FormMasksLocal.py similarity index 100% rename from FormMasksLocal.py rename to Utils/FormMasksLocal.py diff --git a/scripts/PTV.py b/Utils/PTV.py similarity index 100% rename from scripts/PTV.py rename to Utils/PTV.py diff --git a/scripts/RenameDicomPatientID.py b/Utils/RenameDicomPatientID.py similarity index 100% rename from scripts/RenameDicomPatientID.py rename to Utils/RenameDicomPatientID.py diff --git a/SegCT.py b/Utils/SegCT.py similarity index 100% rename from SegCT.py rename to Utils/SegCT.py diff --git a/scripts/check.py b/Utils/check.py similarity index 100% rename from scripts/check.py rename to Utils/check.py diff --git a/scripts/RTOG.py b/Utils/remove/RTOG.py similarity index 100% rename from scripts/RTOG.py rename to Utils/remove/RTOG.py diff --git a/scripts/RTOG_nii.py b/Utils/remove/RTOG_nii.py similarity index 100% rename from scripts/RTOG_nii.py rename to Utils/remove/RTOG_nii.py diff --git a/scripts/renameCT.py b/Utils/renameCT.py similarity index 100% rename from scripts/renameCT.py rename to Utils/renameCT.py diff --git a/scripts/sub_dose_analysis_total.py b/Utils/sub_dose_analysis_total.py similarity index 100% rename from scripts/sub_dose_analysis_total.py rename to Utils/sub_dose_analysis_total.py diff --git a/scripts/test.py b/Utils/test.py similarity index 100% rename from scripts/test.py rename to Utils/test.py diff --git a/scripts/xnat_data_uniformization.py b/Utils/xnat_data_uniformization.py similarity index 100% rename from scripts/xnat_data_uniformization.py rename to Utils/xnat_data_uniformization.py diff --git a/temp.png b/temp.png new file mode 100644 index 0000000000000000000000000000000000000000..6676701d9e27fb393102163c183a677592a8d6f9 GIT binary patch literal 43220 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 literal 0 HcmV?d00001 From fcd5f4884187cd3e40ad331370c1e8bb7192cfb6 Mon Sep 17 00:00:00 2001 From: Clara Date: Thu, 20 Apr 2023 12:05:47 +0100 Subject: [PATCH 05/12] remove comments --- .gitignore | 3 + DataGenerator/.DataGenerator.py.swp | 0 DataGenerator/DataGenerator.py | 519 ++++++++++++---------------- DataGenerator/DataGenerator2.py | 346 ------------------- Training/Classification.py | 94 ++--- Utils/ConvertNii.py | 65 ++-- Utils/DicomTools.py | 52 ++- 7 files changed, 333 insertions(+), 746 deletions(-) delete mode 100644 DataGenerator/.DataGenerator.py.swp delete mode 100644 DataGenerator/DataGenerator2.py diff --git a/.gitignore b/.gitignore index 03e8789..77b8b04 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,6 @@ Training/DeepSurv*.ckpt *.txt wandb/ Training/*.xml +Configs/* +*.png +Training/test*.py diff --git a/DataGenerator/.DataGenerator.py.swp b/DataGenerator/.DataGenerator.py.swp deleted file mode 100644 index e69de29..0000000 diff --git a/DataGenerator/DataGenerator.py b/DataGenerator/DataGenerator.py index 41625be..33fcda6 100644 --- a/DataGenerator/DataGenerator.py +++ b/DataGenerator/DataGenerator.py @@ -7,7 +7,7 @@ import xnat import matplotlib.pyplot as plt from monai.transforms import ( - LoadImage, EnsureChannelFirstd, ResampleToMatchd, ResizeWithPadOrCropd + LoadImage, EnsureChannelFirstd, ResampleToMatchd, ResizeWithPadOrCropd ) from Utils.DicomTools import * from Utils.XNATXML import XMLCreator @@ -24,323 +24,238 @@ class DataGenerator(torch.utils.data.Dataset): - def __init__(self, SubjectList, config=None, keys=['CT'], transform=None, inference=False, - clinical_cols=None, session=None, **kwargs): - super().__init__() - self.config = config - self.SubjectList = SubjectList - self.keys = keys - self.transform = transform - self.inference = inference - self.clinical_cols = clinical_cols - - def __len__(self): - return int(self.SubjectList.shape[0]) - - def __getitem__(self, i): - - data = {} - meta = {} - subject_id = self.SubjectList.loc[i, 'subjectid'] - slabel = self.SubjectList.loc[i, 'subject_label'] - data['slabel'] = slabel - ## Load CT - if 'CT' in self.keys: - CTPath = self.SubjectList.loc[i, 'CT_Path'] - if self.config['DATA']['Nifty']: - CTPath = Path(CTPath, 'CT.nii.gz') - data['CT'], meta['CT'] = LoadImage()(CTPath) - else: - data['CT'], meta['CT'] = LoadImage()(CTPath) - CTSession = ReadDicom(CTPath) - CTArray = sitk.GetArrayFromImage(CTSession) - if not (CTArray.shape == data['CT'].shape): - CTArray = CTArray.transpose((2, 1, 0)) - CTArray = np.flip(CTArray, axis=2) - mCT = MetaTensor(CTArray.copy(), meta=meta['CT']) - data['CT'] = mCT - - ## Load Dose - if 'Dose' in self.keys: - DosePath = self.SubjectList.loc[i, 'Dose_Path'] - if self.config['DATA']['Nifty']: - DosePath = Path(DosePath, 'Dose.nii.gz') - data['Dose'], meta['Dose'] = LoadImage()(DosePath) - data['Dose'] = data['Dose'] / 67 ## Probably need to make it a variable - if not self.config['DATA']['Nifty']: - data['Dose'] = data['Dose'] * np.double(meta['Dose']['3004|000e']) / 67 - - ## Load PET - if 'PET' in self.keys: - PETPath = self.SubjectList.loc[i, 'PET_Path'] - if self.config['DATA']['Nifty']: - PETPath = Path(PETPath, 'pet.nii.gz') - data['PET'], meta['PET'] = LoadImage()(PETPath) - - ## Load Mask - if 'Structs' in self.keys: - RSPath = self.SubjectList.loc[i, 'Structs_Path'] - if self.config['DATA']['Nifty']: - data['Structs'], meta['Structs'] = LoadImage()(Path(RSPath, self.config['DATA']['Structs'])) - - # for roi in self.config['DATA']['Structs']: - # data['Struct_' + roi], meta['Struct_' + roi] = LoadImage()(Path(RSPath,roi+'.nii.gz')) - # dt = distance_transform_edt(data['Struct_' + roi]) - # data['Struct_' + roi] = MetaTensor(dt, meta = meta['CT']) - - # masks_img = np.zeros_like(data['CT']) - # masks_img = get_nii_masks(slabel, masks_img, RSPath, self.config['DATA']['Structs']) - # masks_img = MetaTensor(masks_img.copy(), meta=meta['CT']) - # data['Structs'] = masks_img - else: - ## mask in multichannels - RS = RTStructBuilder.create_from(dicom_series_path=CTPath, rt_struct_path=RSPath) - # roi_names = RS.get_roi_names() - # for roi in self.config['DATA']['Structs']: - # if roi in roi_names: - # mask_img = RS.get_roi_mask_by_name(roi) - # mask_img = distance_transform_edt(mask_img) - # else: - # message = "No ROI of name " + self.targetROI + " found in RTStruct" - # raise ValueError(message) - # mask_img = np.rot90(mask_img) - # mask_img = np.flip(mask_img, 2) - # mask_img = np.flip(mask_img, 0) - # mask = MetaTensor(mask_img.copy(), meta = meta['CT']) - # data['Struct_' + roi] = mask - - ### masks images - masks_img = np.zeros_like(data['CT']) - masks_img = get_RS_masks(slabel, CTPath, masks_img, RSPath, self.config['DATA']['Structs']) - masks_img = np.rot90(masks_img) - masks_img = np.flip(masks_img, 0) - masks_img = MetaTensor(masks_img.copy(), meta=meta['CT']) - data['Structs'] = masks_img - #else: - # if 'CT' in self.keys: - # data['Structs'] = np.ones_like(data['CT']) ## No ROI target defined - - ## Apply transforms on all - if self.transform: data = self.transform(data) - - # mask_imgs = np.zeros_like(CTArray) - # for key in data.keys(): - # if 'Mask' in key: - # mask_imgs = mask_imgs + data[key] - - # for key in data.keys(): - # data[key] = get_masked_img_voxel(data[key], data['Mask']) - # Decide between multi-branch single-channel/multi-channel single-branch - - if self.config['DATA']['Multichannel']: - old_keys = list(self.keys) - data['Image'] = np.concatenate([data[key] for key in old_keys], axis=0) - for key in old_keys: data.pop(key) - else: - if 'Structs' in data.keys(): - data.pop('Structs') ## No need for mask in single-channel multi-branch - - # data = ResizeWithPadOrCropd(keys=data.keys(), spatial_size=self.config['DATA']['dim'])(data) - - ## Add clinical record at the end - if 'Records' in self.config.keys(): data['Records'] = torch.tensor(self.SubjectList.loc[i, self.clinical_cols], - dtype=torch.float32) - if self.inference: - return data - else: ##Training - label = torch.tensor(np.float(self.SubjectList.loc[i, "xnat_subjectdata_field_map_" + self.config['DATA']['target']])) - censored_label = not(np.int8(self.SubjectList.loc[i, 'xnat_subjectdata_field_map_' + self.config['DATA']['censor_label']]).astype('bool')) - if 'threshold' in self.config['DATA'].keys(): ## Classification - label = torch.where(label > self.config['DATA']['threshold'], 1, 0) - label = torch.as_tensor(label, dtype=torch.float32) - label = (censored_label, label) - return data, label + def __init__(self, SubjectList, config=None, keys=['CT'], transform=None, inference=False, + clinical_cols=None, session=None, **kwargs): + super().__init__() + self.config = config + self.SubjectList = SubjectList + self.keys = keys + self.transform = transform + self.inference = inference + self.clinical_cols = clinical_cols + + def __len__(self): + return int(self.SubjectList.shape[0]) + + def __getitem__(self, i): + + data = {} + meta = {} + subject_id = self.SubjectList.loc[i, 'subjectid'] + slabel = self.SubjectList.loc[i, 'subject_label'] + data['slabel'] = slabel + ## Load CT + if 'CT' in self.keys: + CTPath = self.SubjectList.loc[i, 'CT_Path'] + CTPath = Path(CTPath, 'CT.nii.gz') + data['CT'], meta['CT'] = LoadImage()(CTPath) + + ## Load Dose + if 'Dose' in self.keys: + DosePath = self.SubjectList.loc[i, 'Dose_Path'] + DosePath = Path(DosePath, 'Dose.nii.gz') + data['Dose'], meta['Dose'] = LoadImage()(DosePath) + data['Dose'] = data['Dose'] / 67 ## Probably need to make it a variable + + ## Load PET + if 'PET' in self.keys: + PETPath = self.SubjectList.loc[i, 'PET_Path'] + if self.config['DATA']['Nifty']: + PETPath = Path(PETPath, 'pet.nii.gz') + data['PET'], meta['PET'] = LoadImage()(PETPath) + + ## Load Mask + if 'Structs' in self.keys: + RSPath = self.SubjectList.loc[i, 'Structs_Path'] + data['Structs'], meta['Structs'] = LoadImage()(Path(RSPath, self.config['DATA']['Structs'])) + + if self.transform: data = self.transform(data) + + if self.config['DATA']['Multichannel']: + old_keys = list(self.keys) + data['Image'] = np.concatenate([data[key] for key in old_keys], axis=0) + for key in old_keys: data.pop(key) + else: + if 'Structs' in data.keys(): + data.pop('Structs') ## No need for mask in single-channel multi-branch + + ## Add clinical record at the end + if 'Records' in self.config.keys(): data['Records'] = torch.tensor(self.SubjectList.loc[i, self.clinical_cols], + dtype=torch.float32) + if self.inference: + return data + else: ##Training + label = torch.tensor( + np.float(self.SubjectList.loc[i, "xnat_subjectdata_field_map_" + self.config['DATA']['target']])) + censored_label = not (np.int8( + self.SubjectList.loc[i, 'xnat_subjectdata_field_map_' + self.config['DATA']['censor_label']]).astype( + 'bool')) + if 'threshold' in self.config['DATA'].keys(): ## Classification + label = torch.where(label > self.config['DATA']['threshold'], 1, 0) + label = torch.as_tensor(label, dtype=torch.float32) + label = (censored_label, label) + return data, label ### DataLoader class DataModule(LightningDataModule): - def __init__(self, SubjectList, config=None, train_transform=None, val_transform=None, train_size=0.85, num_workers=10, **kwargs): - super().__init__() - self.batch_size = config['MODEL']['batch_size'] - self.num_workers = num_workers - data_trans = class_stratify(SubjectList, config) - ## Split Test with fixed seed - train_val_list, test_list = train_test_split(SubjectList, train_size=train_size, random_state=75, - stratify=data_trans) - - data_trans = class_stratify(train_val_list, config) - ## Split train-val with random seed - train_list, val_list = train_test_split(train_val_list, train_size=train_size, - random_state=np.random.randint(10000), - stratify=data_trans) - - train_list = train_list.reset_index(drop=True) - val_list = val_list.reset_index(drop=True) - test_list = test_list.reset_index(drop=True) - - self.train_data = DataGenerator(train_list, config=config, transform=train_transform, **kwargs) - self.val_data = DataGenerator(val_list, config=config, transform=val_transform, **kwargs) - self.test_data = DataGenerator(test_list, config=config, transform=val_transform, **kwargs) - - def train_dataloader(self): return DataLoader(self.train_data, batch_size=self.batch_size, - num_workers=self.num_workers, pin_memory=True, drop_last=True, - shuffle=True) - - def val_dataloader(self): return DataLoader(self.val_data, batch_size=self.batch_size, - num_workers=self.num_workers, pin_memory=True, drop_last=True, - shuffle=False) - - def test_dataloader(self): return DataLoader(self.test_data, batch_size=self.batch_size, - num_workers=self.num_workers, pin_memory=True, drop_last=True) + def __init__(self, SubjectList, config=None, train_transform=None, val_transform=None, train_size=0.85, + num_workers=10, **kwargs): + super().__init__() + self.batch_size = config['MODEL']['batch_size'] + self.num_workers = num_workers + data_trans = class_stratify(SubjectList, config) + ## Split Test with fixed seed + train_val_list, test_list = train_test_split(SubjectList, train_size=train_size, random_state=75, + stratify=data_trans) + + data_trans = class_stratify(train_val_list, config) + ## Split train-val with random seed + train_list, val_list = train_test_split(train_val_list, train_size=train_size, + random_state=np.random.randint(10000), + stratify=data_trans) + + train_list = train_list.reset_index(drop=True) + val_list = val_list.reset_index(drop=True) + test_list = test_list.reset_index(drop=True) + + self.train_data = DataGenerator(train_list, config=config, transform=train_transform, **kwargs) + self.val_data = DataGenerator(val_list, config=config, transform=val_transform, **kwargs) + self.test_data = DataGenerator(test_list, config=config, transform=val_transform, **kwargs) + + def train_dataloader(self): return DataLoader(self.train_data, batch_size=self.batch_size, + num_workers=self.num_workers, pin_memory=True, drop_last=True, + shuffle=True) + + def val_dataloader(self): return DataLoader(self.val_data, batch_size=self.batch_size, + num_workers=self.num_workers, pin_memory=True, drop_last=True, + shuffle=False) + + def test_dataloader(self): return DataLoader(self.test_data, batch_size=self.batch_size, + num_workers=self.num_workers, pin_memory=True, drop_last=True) def QuerySubjectList(config, session): - root_element = "xnat:subjectData" - XML = XMLCreator(root_element) # , search_field, search_where) - print("Querying from Server") - ## Target - XML.Add_search_field( - {"element_name": "xnat:subjectData", "field_ID": "XNAT_SUBJECTDATA_FIELD_MAP=" + str(config['DATA']['target']), - "sequence": "1", "type": "int"}) - if 'censor_label' in config['DATA'].keys(): - XML.Add_search_field( - {"element_name": "xnat:subjectData", "field_ID": "XNAT_SUBJECTDATA_FIELD_MAP=" + str(config['DATA']['censor_label']), - "sequence": "1", "type": "int"}) - ## Label - XML.Add_search_field( - {"element_name": "xnat:subjectData", "field_ID": "SUBJECT_LABEL", "sequence": "1", "type": "string"}) - - if 'Records' in config.keys(): - feats_list = [item for sublist in config['Records'].values() for item in sublist] - for value in feats_list: - dict_temp = {"element_name": "xnat:subjectData", "field_ID": "XNAT_SUBJECTDATA_FIELD_MAP=" + str(value), - "sequence": "1", "type": "int"} - XML.Add_search_field(dict_temp) - - ## Where Condition - templist = [] - for value in config['SERVER']['Projects']: - templist.append({"schema_field": "xnat:subjectData.PROJECT", "comparison_type": "=", "value": str(value)}) - if (len(templist)): XML.Add_search_where(templist, "OR") - - templist = [] - for key, value in config['CRITERIA'].items(): - templist.append({"schema_field": "xnat:subjectData.XNAT_SUBJECTDATA_FIELD_MAP=" + key, "comparison_type": "=", - "value": str(value)}) - if (len(templist)): XML.Add_search_where(templist, "AND") ## if any items in here - - templist = [] - for key, value in config['MODALITY'].items(): - templist.append( - {"schema_field": "xnat:ctSessionData.SCAN_COUNT_TYPE=" + key, "comparison_type": ">=", "value": str(value)}) - if (len(templist)): XML.Add_search_where(templist, "AND") ## if any items in here - - templist = [] - if (config['FILTER']): - for value in config['FILTER']['patient_id']: - dict_temp = {"schema_field": "xnat:subjectData.SUBJECT_LABEL", "comparison_type": "!=", "value": str(value)} - - templist.append(dict_temp) - if (len(templist)): XML.Add_search_where(templist, "AND") - - xmlstr = XML.ConstructTree() - response = session.post(config['SERVER']['Address'] + '/data/search/', data=xmlstr, format='csv') - SubjectList = pd.read_csv(StringIO(response.text), dtype=str) - # print('Query: ', SubjectList) - return SubjectList + root_element = "xnat:subjectData" + XML = XMLCreator(root_element) # , search_field, search_where) + print("Querying from Server") + ## Target + XML.Add_search_field( + {"element_name": "xnat:subjectData", "field_ID": "XNAT_SUBJECTDATA_FIELD_MAP=" + str(config['DATA']['target']), + "sequence": "1", "type": "int"}) + if 'censor_label' in config['DATA'].keys(): + XML.Add_search_field( + {"element_name": "xnat:subjectData", + "field_ID": "XNAT_SUBJECTDATA_FIELD_MAP=" + str(config['DATA']['censor_label']), + "sequence": "1", "type": "int"}) + ## Label + XML.Add_search_field( + {"element_name": "xnat:subjectData", "field_ID": "SUBJECT_LABEL", "sequence": "1", "type": "string"}) + + if 'Records' in config.keys(): + feats_list = [item for sublist in config['Records'].values() for item in sublist] + for value in feats_list: + dict_temp = {"element_name": "xnat:subjectData", "field_ID": "XNAT_SUBJECTDATA_FIELD_MAP=" + str(value), + "sequence": "1", "type": "int"} + XML.Add_search_field(dict_temp) + + ## Where Condition + templist = [] + for value in config['SERVER']['Projects']: + templist.append({"schema_field": "xnat:subjectData.PROJECT", "comparison_type": "=", "value": str(value)}) + if (len(templist)): XML.Add_search_where(templist, "OR") + + templist = [] + for key, value in config['CRITERIA'].items(): + templist.append({"schema_field": "xnat:subjectData.XNAT_SUBJECTDATA_FIELD_MAP=" + key, "comparison_type": "=", + "value": str(value)}) + if (len(templist)): XML.Add_search_where(templist, "AND") ## if any items in here + + templist = [] + for key, value in config['MODALITY'].items(): + templist.append( + {"schema_field": "xnat:ctSessionData.SCAN_COUNT_TYPE=" + key, "comparison_type": ">=", "value": str(value)}) + if (len(templist)): XML.Add_search_where(templist, "AND") ## if any items in here + + templist = [] + if (config['FILTER']): + for value in config['FILTER']['patient_id']: + dict_temp = {"schema_field": "xnat:subjectData.SUBJECT_LABEL", "comparison_type": "!=", "value": str(value)} + + templist.append(dict_temp) + if (len(templist)): XML.Add_search_where(templist, "AND") + + xmlstr = XML.ConstructTree() + response = session.post(config['SERVER']['Address'] + '/data/search/', data=xmlstr, format='csv') + SubjectList = pd.read_csv(StringIO(response.text), dtype=str) + # print('Query: ', SubjectList) + return SubjectList + + +def class_stratify(SubjectList, config): + ptarget = SubjectList['xnat_subjectdata_field_map_' + config['DATA']['target']] + kbins = KBinsDiscretizer(n_bins=15, encode='ordinal', strategy='uniform') + ptarget = np.array(ptarget).reshape((len(ptarget), 1)) + data_trans = kbins.fit_transform(ptarget) + return data_trans def SynchronizeData(config, SubjectList): - session = xnat.connect(config['SERVER']['Address'], user=config['SERVER']['User'], - password=config['SERVER']['Password']) - for subjectlabel, subjectid in zip(SubjectList['subject_label'], SubjectList['subjectid']): - if (not Path(config['DATA']['DataFolder'], subjectlabel).is_dir()): - xnatsubject = session.create_object('/data/subjects/' + subjectid) - print("Synchronizing ", subjectid, subjectlabel) - xnatsubject.download_dir(config['DATA']['DataFolder']) ## Download data + session = xnat.connect(config['SERVER']['Address'], user=config['SERVER']['User'], + password=config['SERVER']['Password']) + for subjectlabel, subjectid in zip(SubjectList['subject_label'], SubjectList['subjectid']): + if (not Path(config['DATA']['DataFolder'], subjectlabel).is_dir()): + xnatsubject = session.create_object('/data/subjects/' + subjectid) + print("Synchronizing ", subjectid, subjectlabel) + xnatsubject.download_dir(config['DATA']['DataFolder']) ## Download data def get_subject_info(config, session, subjectid): - r = session.get(config['SERVER']['Address'] + '/data/subjects/' + subjectid, format='xml') - data = xmltodict.parse(r.text, force_list=True) - return data + r = session.get(config['SERVER']['Address'] + '/data/subjects/' + subjectid, format='xml') + data = xmltodict.parse(r.text, force_list=True) + return data def QuerySubjectInfo(config, SubjectList, session): - if config['DATA']['Nifty']: - for i in range(len(SubjectList)): - subject_label = SubjectList.loc[i, 'subject_label'] - for key in config['MODALITY'].keys(): - if key == 'Structs': - SubjectList.loc[i, key + '_Path'] = Path(config['DATA']['DataFolder'], subject_label, 'struct_TS') - else: - SubjectList.loc[i, key + '_Path'] = Path(config['DATA']['DataFolder'], subject_label) - else: - with ThreadPoolExecutor(max_workers=10) as executor: - future_to_url = {executor.submit(get_subject_info, config, session, subjectid) for subjectid in - SubjectList['subjectid']} - executor.shutdown(wait=True) - for future in concurrent.futures.as_completed(future_to_url): - subjectdata = future.result() - subjectid = subjectdata["xnat:Subject"][0]["@ID"] - for key in config['MODALITY'].keys(): - path = GeneratePath(subjectdata, Modality=key, config=config) - if key == 'CT': - SubjectList.loc[SubjectList.subjectid == subjectid, key + '_Path'] = path - else: - spath = glob.glob(path + '/*dcm') - SubjectList.loc[SubjectList.subjectid == subjectid, key + '_Path'] = spath[0] - - -def GeneratePath(subjectdata, Modality, config): - subject = subjectdata['xnat:Subject'][0] - subject_label = subject['@label'] - experiments = subject['xnat:experiments'][0]['xnat:experiment'] - - ## Won't work with many experiments yet - for experiment in experiments: - experiment_label = experiment['@label'] - scans = experiment['xnat:scans'][0]['xnat:scan'] - for scan in scans: - if (scan['@type'] in Modality): - scan_label = scan['@ID'] + '-' + scan['@type'] - resources_label = scan['xnat:file'][0]['@label'] - if resources_label == 'SNAPSHOTS': - resources_label = scan['xnat:file'][1]['@label'] - path = os.path.join(config['DATA']['DataFolder'], subject_label, experiment_label, 'scans', - scan_label, 'resources', resources_label, 'files') - return path + for i in range(len(SubjectList)): + subject_label = SubjectList.loc[i, 'subject_label'] + for key in config['MODALITY'].keys(): + if key == 'Structs': + SubjectList.loc[i, key + '_Path'] = Path(config['DATA']['DataFolder'], subject_label, 'struct_TS') + else: + SubjectList.loc[i, key + '_Path'] = Path(config['DATA']['DataFolder'], subject_label) def LoadClinicalData(config, PatientList): - category_cols = [] - numerical_cols = [] - for col in config['Records']['category_feat']: - category_cols.append('xnat_subjectdata_field_map_' + col) - - for row in config['Records']['numerical_feat']: - numerical_cols.append('xnat_subjectdata_field_map_' + row) - - target = config['DATA']['target'] - - ct = ColumnTransformer( - [("CatTrans", OneHotEncoder(), category_cols), - ("NumTrans", MinMaxScaler(), numerical_cols), ]) - - X = PatientList.loc[:, category_cols + numerical_cols] - yc = X[category_cols].astype('float32') - X[category_cols] = yc.fillna(yc.mean().astype('int')) - yn = X[numerical_cols].astype('float32') - X[numerical_cols] = yn.fillna(yn.mean()) # X.loc[:, numerical_cols] = yn.fillna(yn.mean()) - X_trans = ct.fit_transform(X) - if not isinstance(X_trans, (np.ndarray, np.generic)): X_trans = X_trans.toarray() - - df_trans = pd.DataFrame(X_trans, index=X.index, columns=ct.get_feature_names_out()) - clinical_col = list(df_trans.columns) - df_trans['xnat_subjectdata_field_map_' + target] = PatientList.loc[:, 'xnat_subjectdata_field_map_' + target] - df_trans['subject_label'] = PatientList.loc[:, 'subject_label'] - df_trans['subjectid'] = PatientList.loc[:, 'subjectid'] - if 'censor_label' in config['DATA'].keys(): - df_trans['xnat_subjectdata_field_map_' + config['DATA']['censor_label']] = PatientList.loc[:, 'xnat_subjectdata_field_map_' + config['DATA']['censor_label']] - return df_trans, clinical_col - + category_cols = [] + numerical_cols = [] + for col in config['Records']['category_feat']: + category_cols.append('xnat_subjectdata_field_map_' + col) + + for row in config['Records']['numerical_feat']: + numerical_cols.append('xnat_subjectdata_field_map_' + row) + + target = config['DATA']['target'] + + ct = ColumnTransformer( + [("CatTrans", OneHotEncoder(), category_cols), + ("NumTrans", MinMaxScaler(), numerical_cols), ]) + + X = PatientList.loc[:, category_cols + numerical_cols] + yc = X[category_cols].astype('float32') + X[category_cols] = yc.fillna(yc.mean().astype('int')) + yn = X[numerical_cols].astype('float32') + X[numerical_cols] = yn.fillna(yn.mean()) # X.loc[:, numerical_cols] = yn.fillna(yn.mean()) + X_trans = ct.fit_transform(X) + if not isinstance(X_trans, (np.ndarray, np.generic)): X_trans = X_trans.toarray() + + df_trans = pd.DataFrame(X_trans, index=X.index, columns=ct.get_feature_names_out()) + clinical_col = list(df_trans.columns) + df_trans['xnat_subjectdata_field_map_' + target] = PatientList.loc[:, 'xnat_subjectdata_field_map_' + target] + df_trans['subject_label'] = PatientList.loc[:, 'subject_label'] + df_trans['subjectid'] = PatientList.loc[:, 'subjectid'] + if 'censor_label' in config['DATA'].keys(): + df_trans['xnat_subjectdata_field_map_' + config['DATA']['censor_label']] = PatientList.loc[:, + 'xnat_subjectdata_field_map_' + + config['DATA']['censor_label']] + return df_trans, clinical_col diff --git a/DataGenerator/DataGenerator2.py b/DataGenerator/DataGenerator2.py deleted file mode 100644 index 7a670db..0000000 --- a/DataGenerator/DataGenerator2.py +++ /dev/null @@ -1,346 +0,0 @@ -from pytorch_lightning import LightningDataModule, LightningModule, Trainer, seed_everything -from torch.utils.data import DataLoader -import numpy as np -import torch -from monai.data import MetaTensor -from sklearn.model_selection import train_test_split, StratifiedShuffleSplit -import xnat -import matplotlib.pyplot as plt -from monai.transforms import ( - LoadImage, EnsureChannelFirstd, ResampleToMatchd, ResizeWithPadOrCropd -) -from Utils.DicomTools import * -from Utils.XNATXML import XMLCreator -from io import StringIO -import requests -import pandas as pd -import xmltodict -from sklearn.preprocessing import OneHotEncoder, MinMaxScaler, LabelEncoder, OrdinalEncoder -from rt_utils import RTStructBuilder -from sklearn.compose import ColumnTransformer -from pathlib import Path -from concurrent.futures import ThreadPoolExecutor -import concurrent - - -class DataGenerator(torch.utils.data.Dataset): - def __init__(self, SubjectList, config=None, keys=['CT'], transform=None, inference=False, - clinical_cols=None, session=None, **kwargs): - super().__init__() - self.config = config - self.SubjectList = SubjectList - self.keys = keys - self.transform = transform - self.inference = inference - self.clinical_cols = clinical_cols - - def __len__(self): - return int(self.SubjectList.shape[0]) - - def __getitem__(self, i): - - data = {} - meta = {} - subject_id = self.SubjectList.loc[i, 'subjectid'] - slabel = self.SubjectList.loc[i, 'subject_label'] - data['slabel'] = slabel - ## Load CT - if 'CT' in self.keys: - CTPath = self.SubjectList.loc[i, 'CT_Path'] - if self.config['DATA']['Nifty']: - CTPath = Path(CTPath, 'ct.nii.gz') - data['CT'], meta['CT'] = LoadImage()(CTPath) - else: - data['CT'], meta['CT'] = LoadImage()(CTPath) - CTSession = ReadDicom(CTPath) - CTArray = sitk.GetArrayFromImage(CTSession) - if not (CTArray.shape == data['CT'].shape): - CTArray = CTArray.transpose((2, 1, 0)) - CTArray = np.flip(CTArray, axis=2) - mCT = MetaTensor(CTArray.copy(), meta=meta['CT']) - data['CT'] = mCT - - ## Load Dose - if 'Dose' in self.keys: - DosePath = self.SubjectList.loc[i, 'Dose_Path'] - if self.config['DATA']['Nifty']: - DosePath = Path(DosePath, 'dose.nii.gz') - data['Dose'], meta['Dose'] = LoadImage()(DosePath) - data['Dose'] = data['Dose'] / 67 ## Probably need to make it a variable - if not self.config['DATA']['Nifty']: - data['Dose'] = data['Dose'] * np.double(meta['Dose']['3004|000e']) / 67 - - ## Load PET - if 'PET' in self.keys: - PETPath = self.SubjectList.loc[i, 'PET_Path'] - if self.config['DATA']['Nifty']: - PETPath = Path(PETPath, 'pet.nii.gz') - data['PET'], meta['PET'] = LoadImage()(PETPath) - - ## Load Mask - if 'Structs' in self.keys: - RSPath = self.SubjectList.loc[i, 'Structs_Path'] - if self.config['DATA']['Nifty']: - data['Structs'], meta['Structs'] = LoadImage()(Path(RSPath, self.config['DATA']['Structs'])) - - # for roi in self.config['DATA']['Structs']: - # data['Struct_' + roi], meta['Struct_' + roi] = LoadImage()(Path(RSPath,roi+'.nii.gz')) - # dt = distance_transform_edt(data['Struct_' + roi]) - # data['Struct_' + roi] = MetaTensor(dt, meta = meta['CT']) - - # masks_img = np.zeros_like(data['CT']) - # masks_img = get_nii_masks(slabel, masks_img, RSPath, self.config['DATA']['Structs']) - # masks_img = MetaTensor(masks_img.copy(), meta=meta['CT']) - # data['Structs'] = masks_img - else: - ## mask in multichannels - RS = RTStructBuilder.create_from(dicom_series_path=CTPath, rt_struct_path=RSPath) - # roi_names = RS.get_roi_names() - # for roi in self.config['DATA']['Structs']: - # if roi in roi_names: - # mask_img = RS.get_roi_mask_by_name(roi) - # mask_img = distance_transform_edt(mask_img) - # else: - # message = "No ROI of name " + self.targetROI + " found in RTStruct" - # raise ValueError(message) - # mask_img = np.rot90(mask_img) - # mask_img = np.flip(mask_img, 2) - # mask_img = np.flip(mask_img, 0) - # mask = MetaTensor(mask_img.copy(), meta = meta['CT']) - # data['Struct_' + roi] = mask - - ### masks images - masks_img = np.zeros_like(data['CT']) - masks_img = get_RS_masks(slabel, CTPath, masks_img, RSPath, self.config['DATA']['Structs']) - masks_img = np.rot90(masks_img) - masks_img = np.flip(masks_img, 0) - masks_img = MetaTensor(masks_img.copy(), meta=meta['CT']) - data['Structs'] = masks_img - #else: - # if 'CT' in self.keys: - # data['Structs'] = np.ones_like(data['CT']) ## No ROI target defined - - ## Apply transforms on all - if self.transform: data = self.transform(data) - - # mask_imgs = np.zeros_like(CTArray) - # for key in data.keys(): - # if 'Mask' in key: - # mask_imgs = mask_imgs + data[key] - - # for key in data.keys(): - # data[key] = get_masked_img_voxel(data[key], data['Mask']) - # Decide between multi-branch single-channel/multi-channel single-branch - - if self.config['DATA']['Multichannel']: - old_keys = list(self.keys) - data['Image'] = np.concatenate([data[key] for key in old_keys], axis=0) - for key in old_keys: data.pop(key) - else: - if 'Structs' in data.keys(): - data.pop('Structs') ## No need for mask in single-channel multi-branch - - # data = ResizeWithPadOrCropd(keys=data.keys(), spatial_size=self.config['DATA']['dim'])(data) - - ## Add clinical record at the end - if 'Records' in self.config.keys(): data['Records'] = torch.tensor(self.SubjectList.loc[i, self.clinical_cols], - dtype=torch.float32) - if self.inference: - return data - else: ##Training - label = torch.tensor(np.float(self.SubjectList.loc[i, "xnat_subjectdata_field_map_" + self.config['DATA']['target']])) - censored_label = not(np.int8(self.SubjectList.loc[i, 'xnat_subjectdata_field_map_' + self.config['DATA']['censor_label']]).astype('bool')) - if 'threshold' in self.config['DATA'].keys(): ## Classification - label = torch.where(label > self.config['DATA']['threshold'], 1, 0) - label = torch.as_tensor(label, dtype=torch.float32) - label = (censored_label, label) - return data, label - - -### DataLoader -class DataModule(LightningDataModule): - def __init__(self, SubjectList, config=None, train_transform=None, val_transform=None, train_size=0.85, num_workers=10, **kwargs): - super().__init__() - self.batch_size = config['MODEL']['batch_size'] - self.num_workers = num_workers - data_trans = class_stratify(SubjectList, config) - ## Split Test with fixed seed - train_val_list, test_list = train_test_split(SubjectList, train_size=train_size, random_state=75, - stratify=data_trans) - - data_trans = class_stratify(train_val_list, config) - ## Split train-val with random seed - train_list, val_list = train_test_split(train_val_list, train_size=train_size, - random_state=np.random.randint(10000), - stratify=data_trans) - - train_list = train_list.reset_index(drop=True) - val_list = val_list.reset_index(drop=True) - test_list = test_list.reset_index(drop=True) - - self.train_data = DataGenerator(train_list, config=config, transform=train_transform, **kwargs) - self.val_data = DataGenerator(val_list, config=config, transform=val_transform, **kwargs) - self.test_data = DataGenerator(test_list, config=config, transform=val_transform, **kwargs) - - def train_dataloader(self): return DataLoader(self.train_data, batch_size=self.batch_size, - num_workers=self.num_workers, pin_memory=True, drop_last=True, - shuffle=True) - - def val_dataloader(self): return DataLoader(self.val_data, batch_size=self.batch_size, - num_workers=self.num_workers, pin_memory=True, drop_last=True, - shuffle=False) - - def test_dataloader(self): return DataLoader(self.test_data, batch_size=self.batch_size, - num_workers=self.num_workers, pin_memory=True, drop_last=True) - - -def QuerySubjectList(config, session): - root_element = "xnat:subjectData" - XML = XMLCreator(root_element) # , search_field, search_where) - print("Querying from Server") - ## Target - XML.Add_search_field( - {"element_name": "xnat:subjectData", "field_ID": "XNAT_SUBJECTDATA_FIELD_MAP=" + str(config['DATA']['target']), - "sequence": "1", "type": "int"}) - if 'censor_label' in config['DATA'].keys(): - XML.Add_search_field( - {"element_name": "xnat:subjectData", "field_ID": "XNAT_SUBJECTDATA_FIELD_MAP=" + str(config['DATA']['censor_label']), - "sequence": "1", "type": "int"}) - ## Label - XML.Add_search_field( - {"element_name": "xnat:subjectData", "field_ID": "SUBJECT_LABEL", "sequence": "1", "type": "string"}) - - if 'Records' in config.keys(): - feats_list = [item for sublist in config['Records'].values() for item in sublist] - for value in feats_list: - dict_temp = {"element_name": "xnat:subjectData", "field_ID": "XNAT_SUBJECTDATA_FIELD_MAP=" + str(value), - "sequence": "1", "type": "int"} - XML.Add_search_field(dict_temp) - - ## Where Condition - templist = [] - for value in config['SERVER']['Projects']: - templist.append({"schema_field": "xnat:subjectData.PROJECT", "comparison_type": "=", "value": str(value)}) - if (len(templist)): XML.Add_search_where(templist, "OR") - - templist = [] - for key, value in config['CRITERIA'].items(): - templist.append({"schema_field": "xnat:subjectData.XNAT_SUBJECTDATA_FIELD_MAP=" + key, "comparison_type": "=", - "value": str(value)}) - if (len(templist)): XML.Add_search_where(templist, "AND") ## if any items in here - - templist = [] - for key, value in config['MODALITY'].items(): - templist.append( - {"schema_field": "xnat:ctSessionData.SCAN_COUNT_TYPE=" + key, "comparison_type": ">=", "value": str(value)}) - if (len(templist)): XML.Add_search_where(templist, "AND") ## if any items in here - - templist = [] - if (config['FILTER']): - for value in config['FILTER']['patient_id']: - dict_temp = {"schema_field": "xnat:subjectData.SUBJECT_LABEL", "comparison_type": "!=", "value": str(value)} - - templist.append(dict_temp) - if (len(templist)): XML.Add_search_where(templist, "AND") - - xmlstr = XML.ConstructTree() - response = session.post(config['SERVER']['Address'] + '/data/search/', data=xmlstr, format='csv') - SubjectList = pd.read_csv(StringIO(response.text), dtype=str) - # print('Query: ', SubjectList) - return SubjectList - - -def SynchronizeData(config, SubjectList): - session = xnat.connect(config['SERVER']['Address'], user=config['SERVER']['User'], - password=config['SERVER']['Password']) - for subjectlabel, subjectid in zip(SubjectList['subject_label'], SubjectList['subjectid']): - if (not Path(config['DATA']['DataFolder'], subjectlabel).is_dir()): - xnatsubject = session.create_object('/data/subjects/' + subjectid) - print("Synchronizing ", subjectid, subjectlabel) - xnatsubject.download_dir(config['DATA']['DataFolder']) ## Download data - - -def get_subject_info(config, session, subjectid): - r = session.get(config['SERVER']['Address'] + '/data/subjects/' + subjectid, format='xml') - data = xmltodict.parse(r.text, force_list=True) - return data - - -def QuerySubjectInfo(config, SubjectList, session): - if config['DATA']['Nifty']: - for i in range(len(SubjectList)): - subject_label = SubjectList.loc[i, 'subject_label'] - for key in config['MODALITY'].keys(): - #if key == 'Structs': - # SubjectList.loc[i, key + '_Path'] = Path(config['DATA']['DataFolder'], subject_label, 'struct_TS') - #else: - SubjectList.loc[i, key + '_Path'] = Path(config['DATA']['DataFolder'], subject_label) - else: - with ThreadPoolExecutor(max_workers=10) as executor: - future_to_url = {executor.submit(get_subject_info, config, session, subjectid) for subjectid in - SubjectList['subjectid']} - executor.shutdown(wait=True) - for future in concurrent.futures.as_completed(future_to_url): - subjectdata = future.result() - subjectid = subjectdata["xnat:Subject"][0]["@ID"] - for key in config['MODALITY'].keys(): - path = GeneratePath(subjectdata, Modality=key, config=config) - if key == 'CT': - SubjectList.loc[SubjectList.subjectid == subjectid, key + '_Path'] = path - else: - spath = glob.glob(path + '/*dcm') - SubjectList.loc[SubjectList.subjectid == subjectid, key + '_Path'] = spath[0] - - -def GeneratePath(subjectdata, Modality, config): - subject = subjectdata['xnat:Subject'][0] - subject_label = subject['@label'] - experiments = subject['xnat:experiments'][0]['xnat:experiment'] - - ## Won't work with many experiments yet - for experiment in experiments: - experiment_label = experiment['@label'] - scans = experiment['xnat:scans'][0]['xnat:scan'] - for scan in scans: - if (scan['@type'] in Modality): - scan_label = scan['@ID'] + '-' + scan['@type'] - resources_label = scan['xnat:file'][0]['@label'] - if resources_label == 'SNAPSHOTS': - resources_label = scan['xnat:file'][1]['@label'] - path = os.path.join(config['DATA']['DataFolder'], subject_label, experiment_label, 'scans', - scan_label, 'resources', resources_label, 'files') - return path - - -def LoadClinicalData(config, PatientList): - category_cols = [] - numerical_cols = [] - for col in config['Records']['category_feat']: - category_cols.append('xnat_subjectdata_field_map_' + col) - - for row in config['Records']['numerical_feat']: - numerical_cols.append('xnat_subjectdata_field_map_' + row) - - target = config['DATA']['target'] - - ct = ColumnTransformer( - [("CatTrans", OneHotEncoder(), category_cols), - ("NumTrans", MinMaxScaler(), numerical_cols), ]) - - X = PatientList.loc[:, category_cols + numerical_cols] - yc = X[category_cols].astype('float32') - X[category_cols] = yc.fillna(yc.mean().astype('int')) - yn = X[numerical_cols].astype('float32') - X[numerical_cols] = yn.fillna(yn.mean()) # X.loc[:, numerical_cols] = yn.fillna(yn.mean()) - X_trans = ct.fit_transform(X) - if not isinstance(X_trans, (np.ndarray, np.generic)): X_trans = X_trans.toarray() - - df_trans = pd.DataFrame(X_trans, index=X.index, columns=ct.get_feature_names_out()) - clinical_col = list(df_trans.columns) - df_trans['xnat_subjectdata_field_map_' + target] = PatientList.loc[:, 'xnat_subjectdata_field_map_' + target] - df_trans['subject_label'] = PatientList.loc[:, 'subject_label'] - df_trans['subjectid'] = PatientList.loc[:, 'subjectid'] - if 'censor_label' in config['DATA'].keys(): - df_trans['xnat_subjectdata_field_map_' + config['DATA']['censor_label']] = PatientList.loc[:, 'xnat_subjectdata_field_map_' + config['DATA']['censor_label']] - return df_trans, clinical_col - diff --git a/Training/Classification.py b/Training/Classification.py index f1f0edd..74cf486 100644 --- a/Training/Classification.py +++ b/Training/Classification.py @@ -26,31 +26,22 @@ ## 2D transform img_keys = list(config['MODALITY'].keys()) -## Multichannel masks -#img_keys.remove('Structs') -#if 'Structs' in config['DATA'].keys(): -# for roi in config['DATA']['Structs']: -# img_keys.append('Struct_' + roi) if config['MODALITY'].values(): train_transform = torchvision.transforms.Compose([ EnsureChannelFirstd(keys=img_keys), - #ResampleToMatchd(list(set(img_keys).difference(set(['CT']))), key_dst='CT'), monai.transforms.ScaleIntensityd(keys=list(set(img_keys).difference(set(['Dose'])))), - #monai.transforms.ResizeWithPadOrCropd(keys=img_keys, spatial_size=config['DATA']['dim']), monai.transforms.Resized(keys=img_keys, spatial_size=config['DATA']['dim']), - monai.transforms.RandAffined(keys=img_keys), - monai.transforms.RandHistogramShiftd(keys=img_keys), - monai.transforms.RandAdjustContrastd(keys=img_keys), - monai.transforms.RandGaussianNoised(keys=img_keys), + # monai.transforms.RandAffined(keys=img_keys), + # monai.transforms.RandHistogramShiftd(keys=img_keys), + # monai.transforms.RandAdjustContrastd(keys=img_keys), + # monai.transforms.RandGaussianNoised(keys=img_keys), ]) val_transform = torchvision.transforms.Compose([ EnsureChannelFirstd(keys=img_keys), - #ResampleToMatchd(list(set(img_keys).difference(set(['CT']))), key_dst='CT'), monai.transforms.ScaleIntensityd(list(set(img_keys).difference(set(['Dose'])))), - #monai.transforms.ResizeWithPadOrCropd(img_keys, spatial_size=config['DATA']['dim']), monai.transforms.Resized(keys=img_keys, spatial_size=config['DATA']['dim']), ]) else: @@ -62,7 +53,6 @@ SubjectList = QuerySubjectList(config, session) SynchronizeData(config, SubjectList) SubjectList.dropna(subset=['xnat_subjectdata_field_map_survival_months'], inplace=True) -# SubjectList.dropna(subset=['xnat_subjectdata_field_map_overall_stage'], inplace=True) module_dict = nn.ModuleDict() if config['DATA']['Multichannel']: ## Single-Model Multichannel learning @@ -84,13 +74,7 @@ QuerySubjectInfo(config, SubjectList, session) print(SubjectList) -# threshold = config['DATA']['threshold'] -# ckpt_path = Path('./lightning_logs', total_backbone, 'ckpt') -rd = [53414, 88536, 89901, 62594, 13787, 21781, 18215, 4182, 10695, 61645, 93967, 35446, 41063, 98435, 94558, 67665, - 98831, 76684, 33670, 66239, 24417, 29551, 68018, 52785, 41160, 60264, 75053, 58354, 55180, 58358, 51182, 8260] - for iter in range(0, 15, 1): - #seed_everything(rd[iter],workers=True) seed_everything(np.random.randint(0, 10000), workers=True) dataloader = DataModule(SubjectList, config=config, @@ -101,42 +85,36 @@ inference=False) model = MixModel(module_dict, config) - model.apply(model.weights_reset) - #full_ckpt_path = Path(ckpt_path, 'Iter_'+ str(iter) + '.ckpt') - #model.load_state_dict(torch.load(full_ckpt_path)['state_dict']) - - filename = config['DATA']['LogFolder'] - - logger = PredictionReports(config=config, save_dir='lightning_logs', name=filename) - logger.log_text() - logger._version = iter - callbacks = [ - ModelCheckpoint(dirpath=Path(logger.log_dir, 'ckpt'), - monitor='val_loss_epoch', - filename='Iter_' + str(iter), - save_top_k=2, - mode='min'), - # EarlyStopping(monitor='val_loss', - # check_finite=True), - ] - - trainer = Trainer( - #gpus=1, - accelerator="gpu", - devices=[0,1,2,3], - strategy=DDPStrategy(find_unused_parameters=True), - max_epochs=40, - logger=logger, - callbacks=callbacks, - ) - #model = torch.compile(model) - trainer.fit(model, dataloader) - #torch.save({'state_dict': model.state_dict(),}, Path(logger.log_dir, 'Iter_' + str(iter) + '.ckpt')) - -with open(logger.root_dir + "/Config.ini", "w+") as toml_file: - toml.dump(config, toml_file) - toml_file.write("Train transform:\n") - toml_file.write(str(train_transform)) - toml_file.write("Val/Test transform:\n") - toml_file.write(str(val_transform)) +# model.apply(model.weights_reset) +# filename = config['DATA']['LogFolder'] +# +# logger = PredictionReports(config=config, save_dir='lightning_logs', name=filename) +# logger.log_text() +# logger._version = iter +# callbacks = [ +# ModelCheckpoint(dirpath=Path(logger.log_dir, 'ckpt'), +# monitor='val_loss_epoch', +# filename='Iter_' + str(iter), +# save_top_k=2, +# mode='min'), +# # EarlyStopping(monitor='val_loss', +# # check_finite=True), +# ] +# +# trainer = Trainer( +# accelerator="gpu", +# devices=[0, 1, 2, 3], +# strategy=DDPStrategy(find_unused_parameters=True), +# max_epochs=40, +# logger=logger, +# callbacks=callbacks, +# ) +# trainer.fit(model, dataloader) +# +# with open(logger.root_dir + "/Config.ini", "w+") as toml_file: +# toml.dump(config, toml_file) +# toml_file.write("Train transform:\n") +# toml_file.write(str(train_transform)) +# toml_file.write("Val/Test transform:\n") +# toml_file.write(str(val_transform)) diff --git a/Utils/ConvertNii.py b/Utils/ConvertNii.py index 9413f5e..eb76858 100644 --- a/Utils/ConvertNii.py +++ b/Utils/ConvertNii.py @@ -1,44 +1,39 @@ import sys -#sys.path.insert(0, '/home/dgs1/Software/OutcomePrediction/') +# sys.path.insert(0, '/home/dgs1/Software/OutcomePrediction/') import toml import glob import nibabel as nib import numpy as np from Utils.DicomTools import * from pathlib import Path -from DataGenerator.DataGenerator import QuerySubjectList, SynchronizeData, QuerySubjectInfo +from DataGenerator.DataGenerator import QuerySubjectList, SynchronizeData from Utils.DicomTools import * import os from rt_utils import RTStructBuilder import xnat from Utils.FixRTSS import * + session = xnat.connect('http://128.16.11.124:8080/xnat', user='admin', password='mortavar1977') import csv -from monai.transforms import Spacing, LoadImage, EnsureChannelFirst +from monai.transforms import Spacing, LoadImage, EnsureChannelFirst, ResampleToMatch from monai.data import MetaTensor import itk + config = toml.load(sys.argv[1]) from scipy import ndimage -session = xnat.connect(config['SERVER']['Address'], user=config['SERVER']['User'],password=config['SERVER']['Password']) + +session = xnat.connect(config['SERVER']['Address'], user=config['SERVER']['User'], + password=config['SERVER']['Password']) SubjectList = QuerySubjectList(config, session) print(SubjectList) SynchronizeData(config, SubjectList) for key in config['MODALITY'].keys(): - SubjectList[key+'_Path'] = "" + SubjectList[key + '_Path'] = "" + QuerySubjectInfo(config, SubjectList, session) -# -# roi_series = ['Heart', 'Oesophagus', 'Spinal Canal', 'Prox bronch tree', 'Proximal trachea', 'Cwall & ribs', 'L Lung', -# 'R Lung', 'Brachial Plexus'] -# roi_name = ['HEART', 'ESOPHAGUS', 'SPINAL_CORD', 'PROX_BRONCH_TREE', 'PROXIMAL_TRACHEA', 'CWALL_RIBS', 'LUNG_LEFT', -# 'LUNG_RIGHT', 'BRACHIAL_PLEXUS'] -# roi_series = ['Heart', 'Esophagus', 'Lung_L', 'Lung_R','SpinalCord'] -# roi_name = ['HEART', 'ESOPHAGUS', 'LUNG_LEFT','LUNG_RIGHT', 'SPINAL_CORD'] - -# roi_series = ['HEART', 'PTV', 'LUNG_IPSI', 'LUNG_CNTR', 'SPINAL_CORD', 'ESOPHAGUS'] -# roi_name = ['HEART', 'PTV', 'LUNG_IPSI', 'LUNG_CNTR', 'SPINAL_CORD', 'ESOPHAGUS'] -roi_name = 'gtv' +roi_name = 'gtv' path = config['DATA']['NiiFolder'] sPatient = SubjectList @@ -46,11 +41,18 @@ for i in range(0, len(SubjectList), 1): subjectid = sPatient.loc[i, 'subjectid'] - subject_label = sPatient.loc[i,'subject_label'] - CTPath = SubjectList[SubjectList.subjectid == subjectid]['CT_Path'] + subject_label = sPatient.loc[i, 'subject_label'] + # CTPath = SubjectList[SubjectList.subjectid == subjectid]['CT_Path'] + CTPath = SubjectList.loc[i, 'CT_Path'] CTArray, meta = LoadImage()(CTPath) + DosePath = SubjectList.loc[i, 'Dose_Path'] + DoseArray, dmeta = LoadImage()(DosePath) + DoseArray = DoseArray * np.double(dmeta['3004|000e']) + ct = EnsureChannelFirst()(CTArray) ct = Spacing(pixdim=(1, 1, 3))(ct) + DoseArray = EnsureChannelFirst()(DoseArray) + dose = ResampleToMatch()(DoseArray, ct) names_generator = itk.GDCMSeriesFileNames.New() names_generator.SetUseSeriesDetails(True) @@ -63,20 +65,24 @@ print(series_uid) else: ct_array = ct.array.squeeze() + + ni_ct = nib.Nifti1Image(np.double(ct_array), affine=ct.affine) + dose_array = dose.array.squeeze() + ni_dose = nib.Nifti1Image(np.double(dose_array), affine=dose.affine) + spath = Path(path, subject_label) + + if not os.path.isdir(spath): + os.mkdir(spath) + nib.save(ni_ct, Path(spath, 'ct.nii.gz')) + nib.save(ni_dose, Path(spath, 'dose.nii.gz')) + # First define the ROI based on target RSPath = SubjectList[SubjectList.subjectid == subjectid]['Structs_Path'] # FixRTSS(RSPath[0], list(CTPath)[0]) RS = RTStructBuilder.create_from(dicom_series_path=list(CTPath)[0], rt_struct_path=list(RSPath)[0]) - #%% + # %% roi_names = RS.get_roi_names() strList = [x.lower() for x in roi_names] - ni_img = nib.Nifti1Image(np.double(ct_array), affine=ct.affine) - spath = Path(path, subject_label) - - if not os.path.isdir(spath): - os.mkdir(spath) - nib.save(ni_img, Path(spath, 'ct.nii.gz')) - r1 = [r for r in strList if roi_name in r] mask = np.zeros_like(CTArray) @@ -89,13 +95,10 @@ mask_img = np.flip(mask_img, 0) mask = mask + mask_img - mask = ndimage.binary_dilation(mask, structure=se, iterations=3) + mask = ndimage.binary_dilation(mask, structure=se, iterations=3) ## if needs expansion mask = MetaTensor(mask.copy(), meta=meta) mask = EnsureChannelFirst()(mask) mask = Spacing(pixdim=(1, 1, 3))(mask) mask_array = mask.array.squeeze() ni_mask = nib.Nifti1Image(mask_array.astype('int'), affine=mask.affine, dtype='uint8') - nib.save(ni_mask, Path(spath, 'AI_target.nii.gz')) - - - + nib.save(ni_mask, Path(spath, 'AI_target.nii.gz')) diff --git a/Utils/DicomTools.py b/Utils/DicomTools.py index be33775..8b67365 100644 --- a/Utils/DicomTools.py +++ b/Utils/DicomTools.py @@ -15,7 +15,8 @@ sitk.ProcessObject_SetGlobalWarningDisplay(False) from rt_utils import RTStructBuilder from scipy.ndimage import * - +from concurrent.futures import ThreadPoolExecutor +import concurrent def get_bbox_from_mask(mask, img_shape): pos = np.where(mask) @@ -124,14 +125,6 @@ def get_masked_img_voxel(ImageVoxel, mask_voxel): # return transform -def class_stratify(SubjectList, config): - ptarget = SubjectList['xnat_subjectdata_field_map_' + config['DATA']['target']] - kbins = KBinsDiscretizer(n_bins=15, encode='ordinal', strategy='uniform') - ptarget = np.array(ptarget).reshape((len(ptarget), 1)) - data_trans = kbins.fit_transform(ptarget) - return data_trans - - def get_RS_masks(slabel, CTPath, mask_imgs, RSfile, mask_names): #RS = RTStructBuilder.create_from(dicom_series_path=CTPath, rt_struct_path=RSfile) #roi_names = RS.get_roi_names() @@ -186,3 +179,44 @@ def BitSet(n, p, b): mask = 1 << p bm = b << p return (n & ~mask) | bm + +def QuerySubjectInfo(config, SubjectList, session): + if config['DATA']['Nifty']: + for i in range(len(SubjectList)): + subject_label = SubjectList.loc[i,'subject_label'] + for key in config['MODALITY'].keys(): + SubjectList.loc[i, key + '_Path'] = Path(config['DATA']['DataFolder'], subject_label) + else: + with ThreadPoolExecutor(max_workers=10) as executor: + future_to_url = {executor.submit(get_subject_info, config, session, subjectid) for subjectid in + SubjectList['subjectid']} + executor.shutdown(wait=True) + for future in concurrent.futures.as_completed(future_to_url): + subjectdata = future.result() + subjectid = subjectdata["xnat:Subject"][0]["@ID"] + for key in config['MODALITY'].keys(): + path = GeneratePath(subjectdata, Modality=key, config=config) + if key == 'CT': + SubjectList.loc[SubjectList.subjectid == subjectid, key + '_Path'] = path + else: + spath = glob.glob(path + '/*dcm') + SubjectList.loc[SubjectList.subjectid == subjectid, key + '_Path'] = spath[0] + +def GeneratePath(subjectdata, Modality, config): + subject = subjectdata['xnat:Subject'][0] + subject_label = subject['@label'] + experiments = subject['xnat:experiments'][0]['xnat:experiment'] + + ## Won't work with many experiments yet + for experiment in experiments: + experiment_label = experiment['@label'] + scans = experiment['xnat:scans'][0]['xnat:scan'] + for scan in scans: + if (scan['@type'] in Modality): + scan_label = scan['@ID'] + '-' + scan['@type'] + resources_label = scan['xnat:file'][0]['@label'] + if resources_label == 'SNAPSHOTS': + resources_label = scan['xnat:file'][1]['@label'] + path = os.path.join(config['DATA']['DataFolder'], subject_label, experiment_label, 'scans', + scan_label, 'resources', resources_label, 'files') + return path From 20f96e0220efbe0f4ed0feae2e889c995334604f Mon Sep 17 00:00:00 2001 From: Clara Date: Thu, 20 Apr 2023 14:09:38 +0100 Subject: [PATCH 06/12] use crop transformer --- .gitignore | 3 ++- DataGenerator/DataGenerator.py | 2 +- Models/Classifier.py | 8 +++++-- Training/Classification.py | 28 ++++++++++++++-------- Utils/ConvertNii.py | 43 +++++++++++++++++----------------- 5 files changed, 49 insertions(+), 35 deletions(-) diff --git a/.gitignore b/.gitignore index 77b8b04..1804d68 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ Training/DeepSurv*.ckpt *.txt wandb/ Training/*.xml -Configs/* +Configs/Classification/*.ini +Configs/Regression/*.ini *.png Training/test*.py diff --git a/DataGenerator/DataGenerator.py b/DataGenerator/DataGenerator.py index 33fcda6..843a46d 100644 --- a/DataGenerator/DataGenerator.py +++ b/DataGenerator/DataGenerator.py @@ -216,7 +216,7 @@ def get_subject_info(config, session, subjectid): return data -def QuerySubjectInfo(config, SubjectList, session): +def QuerySubjectInfo(config, SubjectList): for i in range(len(SubjectList)): subject_label = SubjectList.loc[i, 'subject_label'] for key in config['MODALITY'].keys(): diff --git a/Models/Classifier.py b/Models/Classifier.py index 16f49e3..052ae36 100644 --- a/Models/Classifier.py +++ b/Models/Classifier.py @@ -21,8 +21,12 @@ def __init__(self, config, module_str): model_str = 'nets.' + model + '(**parameters)' self.backbone = eval(model_str) - # self.out_feat = config['MODEL_PARAMETERS']['out_channels'] - self.out_feat = config['MODEL_PARAMETERS']['num_classes'] + if 'out_channels' in config['MODEL_PARAMETERS'].keys(): + self.out_feat = config['MODEL_PARAMETERS']['out_channels'] + elif 'num_classes' in config['MODEL_PARAMETERS'].keys(): + self.out_feat = config['MODEL_PARAMETERS']['num_classes'] + + def forward(self, x): return self.backbone(x) diff --git a/Training/Classification.py b/Training/Classification.py index 74cf486..7a3b8bb 100644 --- a/Training/Classification.py +++ b/Training/Classification.py @@ -6,32 +6,39 @@ from pytorch_lightning import LightningDataModule, LightningModule, Trainer, seed_everything import sys, os import monai + torch.cuda.empty_cache() ## Module - Dataloaders from DataGenerator.DataGenerator import * from Models.Classifier import Classifier from Models.Linear import Linear from Models.MixModel import MixModel -from monai.transforms import EnsureChannelFirstd, ScaleIntensityd, ResampleToMatchd +from monai.transforms import EnsureChannelFirstd, ScaleIntensityd, ResampleToMatchd, BoundingRectd ## Main from sklearn.preprocessing import StandardScaler, OneHotEncoder import toml from Utils.GenerateSmoothLabel import get_smoothed_label_distribution, get_module from Utils.PredictionReports import PredictionReports -from pathlib import Path +from pathlib import Path # from torchmetrics import ConfusionMatrix import torchmetrics config = toml.load(sys.argv[1]) + +def threshold_at_one(x): + return x > 0 + + ## 2D transform img_keys = list(config['MODALITY'].keys()) if config['MODALITY'].values(): train_transform = torchvision.transforms.Compose([ EnsureChannelFirstd(keys=img_keys), - monai.transforms.ScaleIntensityd(keys=list(set(img_keys).difference(set(['Dose'])))), + monai.transforms.CropForegroundd(keys=img_keys, source_key='Structs', select_fn=threshold_at_one), monai.transforms.Resized(keys=img_keys, spatial_size=config['DATA']['dim']), + monai.transforms.ScaleIntensityd(keys=list(set(img_keys).difference(set(['Dose'])))), # monai.transforms.RandAffined(keys=img_keys), # monai.transforms.RandHistogramShiftd(keys=img_keys), # monai.transforms.RandAdjustContrastd(keys=img_keys), @@ -41,25 +48,27 @@ val_transform = torchvision.transforms.Compose([ EnsureChannelFirstd(keys=img_keys), - monai.transforms.ScaleIntensityd(list(set(img_keys).difference(set(['Dose'])))), + monai.transforms.CropForegroundd(keys=img_keys, source_key='Structs', select_fn=threshold_at_one), monai.transforms.Resized(keys=img_keys, spatial_size=config['DATA']['dim']), + monai.transforms.ScaleIntensityd(list(set(img_keys).difference(set(['Dose'])))), ]) else: train_transform = None val_transform = None ## First Connect to XNAT -session = xnat.connect(config['SERVER']['Address'], user=config['SERVER']['User'],password=config['SERVER']['Password']) +session = xnat.connect(config['SERVER']['Address'], user=config['SERVER']['User'], + password=config['SERVER']['Password']) SubjectList = QuerySubjectList(config, session) SynchronizeData(config, SubjectList) SubjectList.dropna(subset=['xnat_subjectdata_field_map_survival_months'], inplace=True) module_dict = nn.ModuleDict() -if config['DATA']['Multichannel']: ## Single-Model Multichannel learning +if config['DATA']['Multichannel']: ## Single-Model Multichannel learning if config['MODALITY'].keys(): module_dict['Image'] = Classifier(config, 'Image') else: - for key in config['MODALITY'].keys():# Multi-Model Single Channel learning + for key in config['MODALITY'].keys(): # Multi-Model Single Channel learning module_dict[key] = Classifier(config, key) if 'Records' in config.keys(): @@ -70,8 +79,8 @@ ## GeneratePath for key in config['MODALITY'].keys(): - SubjectList[key+'_Path'] = "" -QuerySubjectInfo(config, SubjectList, session) + SubjectList[key + '_Path'] = "" +QuerySubjectInfo(config, SubjectList) print(SubjectList) for iter in range(0, 15, 1): @@ -117,4 +126,3 @@ # toml_file.write(str(train_transform)) # toml_file.write("Val/Test transform:\n") # toml_file.write(str(val_transform)) - diff --git a/Utils/ConvertNii.py b/Utils/ConvertNii.py index eb76858..4633144 100644 --- a/Utils/ConvertNii.py +++ b/Utils/ConvertNii.py @@ -6,7 +6,8 @@ import numpy as np from Utils.DicomTools import * from pathlib import Path -from DataGenerator.DataGenerator import QuerySubjectList, SynchronizeData +from DataGenerator.DataGenerator import SynchronizeData, QuerySubjectList +from Utils.DicomTools import QuerySubjectInfo from Utils.DicomTools import * import os from rt_utils import RTStructBuilder @@ -33,7 +34,7 @@ QuerySubjectInfo(config, SubjectList, session) -roi_name = 'gtv' +roi_name = 'ptv' path = config['DATA']['NiiFolder'] sPatient = SubjectList @@ -73,8 +74,8 @@ if not os.path.isdir(spath): os.mkdir(spath) - nib.save(ni_ct, Path(spath, 'ct.nii.gz')) - nib.save(ni_dose, Path(spath, 'dose.nii.gz')) + nib.save(ni_ct, Path(spath, 'CT.nii.gz')) + nib.save(ni_dose, Path(spath, 'Dose.nii.gz')) # First define the ROI based on target RSPath = SubjectList[SubjectList.subjectid == subjectid]['Structs_Path'] @@ -85,20 +86,20 @@ strList = [x.lower() for x in roi_names] r1 = [r for r in strList if roi_name in r] - mask = np.zeros_like(CTArray) - - for j in range(len(r1)): - roi = r1[j] - index = strList.index(roi.lower()) - mask_img = RS.get_roi_mask_by_name(roi_names[index]) - mask_img = np.rot90(mask_img) - mask_img = np.flip(mask_img, 0) - mask = mask + mask_img - - mask = ndimage.binary_dilation(mask, structure=se, iterations=3) ## if needs expansion - mask = MetaTensor(mask.copy(), meta=meta) - mask = EnsureChannelFirst()(mask) - mask = Spacing(pixdim=(1, 1, 3))(mask) - mask_array = mask.array.squeeze() - ni_mask = nib.Nifti1Image(mask_array.astype('int'), affine=mask.affine, dtype='uint8') - nib.save(ni_mask, Path(spath, 'AI_target.nii.gz')) + if len(r1) > 0: + mask = np.zeros_like(CTArray) + for j in range(len(r1)): + roi = r1[j] + index = strList.index(roi.lower()) + mask_img = RS.get_roi_mask_by_name(roi_names[index]) + mask_img = np.rot90(mask_img) + mask_img = np.flip(mask_img, 0) + mask = mask + mask_img + + mask = ndimage.binary_dilation(mask, structure=se, iterations=3) ## if needs expansion + mask = MetaTensor(mask.copy(), meta=meta) + mask = EnsureChannelFirst()(mask) + mask = Spacing(pixdim=(1, 1, 3))(mask) + mask_array = mask.array.squeeze() + ni_mask = nib.Nifti1Image(mask_array.astype('int'), affine=mask.affine, dtype='uint8') + nib.save(ni_mask, Path(spath, 'struct_TS', 'AI_target.nii.gz')) From 6301d0efc3525b03246deee96c79cb4c660ca478 Mon Sep 17 00:00:00 2001 From: Clara Date: Fri, 21 Apr 2023 12:08:07 +0100 Subject: [PATCH 07/12] DataLoader label changes --- Training/Classification.py | 64 +++++----- Training/ShapAnalysis.py | 125 ------------------- Training/test.py | 238 ------------------------------------- Training/testR.py | 157 ------------------------ 4 files changed, 32 insertions(+), 552 deletions(-) delete mode 100644 Training/ShapAnalysis.py delete mode 100644 Training/test.py delete mode 100644 Training/testR.py diff --git a/Training/Classification.py b/Training/Classification.py index 7a3b8bb..0d5d361 100644 --- a/Training/Classification.py +++ b/Training/Classification.py @@ -94,35 +94,35 @@ def threshold_at_one(x): inference=False) model = MixModel(module_dict, config) -# model.apply(model.weights_reset) -# filename = config['DATA']['LogFolder'] -# -# logger = PredictionReports(config=config, save_dir='lightning_logs', name=filename) -# logger.log_text() -# logger._version = iter -# callbacks = [ -# ModelCheckpoint(dirpath=Path(logger.log_dir, 'ckpt'), -# monitor='val_loss_epoch', -# filename='Iter_' + str(iter), -# save_top_k=2, -# mode='min'), -# # EarlyStopping(monitor='val_loss', -# # check_finite=True), -# ] -# -# trainer = Trainer( -# accelerator="gpu", -# devices=[0, 1, 2, 3], -# strategy=DDPStrategy(find_unused_parameters=True), -# max_epochs=40, -# logger=logger, -# callbacks=callbacks, -# ) -# trainer.fit(model, dataloader) -# -# with open(logger.root_dir + "/Config.ini", "w+") as toml_file: -# toml.dump(config, toml_file) -# toml_file.write("Train transform:\n") -# toml_file.write(str(train_transform)) -# toml_file.write("Val/Test transform:\n") -# toml_file.write(str(val_transform)) + model.apply(model.weights_reset) + filename = config['DATA']['LogFolder'] + + logger = PredictionReports(config=config, save_dir='lightning_logs', name=filename) + logger.log_text() + logger._version = iter + callbacks = [ + ModelCheckpoint(dirpath=Path(logger.log_dir, 'ckpt'), + monitor='val_loss', + filename='Iter_' + str(iter), + save_top_k=2, + mode='min'), + # EarlyStopping(monitor='val_loss', + # check_finite=True), + ] + + trainer = Trainer( + accelerator="gpu", + devices=[0, 1, 2, 3], + strategy=DDPStrategy(find_unused_parameters=True), + max_epochs=40, + logger=logger, + callbacks=callbacks, + ) + trainer.fit(model, dataloader) + +with open(logger.root_dir + "/Config.ini", "w+") as toml_file: + toml.dump(config, toml_file) + toml_file.write("Train transform:\n") + toml_file.write(str(train_transform)) + toml_file.write("Val/Test transform:\n") + toml_file.write(str(val_transform)) diff --git a/Training/ShapAnalysis.py b/Training/ShapAnalysis.py deleted file mode 100644 index 09ffa33..0000000 --- a/Training/ShapAnalysis.py +++ /dev/null @@ -1,125 +0,0 @@ -import torch -import torchvision -from torch import nn -import sys, os -import monai - -torch.cuda.empty_cache() -## Module - Dataloaders -from DataGenerator.DataGenerator import * -from Models.Classifier import Classifier -from Models.Linear import Linear -from Models.MixModel import MixModel -from monai.transforms import EnsureChannelFirstd, ScaleIntensityd, ResampleToMatchd -## Main -import toml -from pathlib import Path -from torchmetrics import ConfusionMatrix -import torchmetrics -#import shap - -config = toml.load(sys.argv[1]) -## 2D transform -img_keys = list(config['MODALITY'].keys()) -# img_keys.remove('Structs') -# if 'Structs' in config['DATA'].keys(): -# for roi in config['DATA']['Structs']: -# img_keys.append('Struct_' + roi) - -train_transform = torchvision.transforms.Compose([ - EnsureChannelFirstd(keys=img_keys), - # ResampleToMatchd(list(set(img_keys).difference(set(['CT']))), key_dst='CT'), - monai.transforms.ScaleIntensityd(keys=img_keys), - # monai.transforms.ResizeWithPadOrCropd(keys=img_keys, spatial_size=config['DATA']['dim']), - monai.transforms.Resized(keys=img_keys, spatial_size=config['DATA']['dim']), - monai.transforms.RandAffined(keys=img_keys), - monai.transforms.RandHistogramShiftd(keys=img_keys), - monai.transforms.RandAdjustContrastd(keys=img_keys), - monai.transforms.RandGaussianNoised(keys=img_keys), - -]) - -val_transform = torchvision.transforms.Compose([ - EnsureChannelFirstd(keys=img_keys), - # ResampleToMatchd(list(set(img_keys).difference(set(['CT']))), key_dst='CT'), - monai.transforms.ScaleIntensityd(img_keys), - # monai.transforms.ResizeWithPadOrCropd(img_keys, spatial_size=config['DATA']['dim']), - monai.transforms.Resized(keys=img_keys, spatial_size=config['DATA']['dim']), -]) - -## First Connect to XNAT -session = xnat.connect(config['SERVER']['Address'], user=config['SERVER']['User'], - password=config['SERVER']['Password']) - -SubjectList = QuerySubjectList(config, session) -SynchronizeData(config, SubjectList) -SubjectList.dropna(subset=['xnat_subjectdata_field_map_survival_months'], inplace=True) - -module_dict = nn.ModuleDict() -if config['DATA']['Multichannel']: ## Single-Model Multichannel learning - if config['MODALITY'].keys(): - module_dict['Image'] = Classifier(config, 'Image') -else: - for key in config['MODALITY'].keys(): # Multi-Model Single Channel learning - module_dict[key] = Classifier(config, key) - -if 'Records' in config.keys(): - SubjectList, clinical_cols = LoadClinicalData(config, SubjectList) - module_dict['Records'] = Linear(in_feat=len(clinical_cols), out_feat=42) -else: - clinical_cols = None - -## GeneratePath -for key in config['MODALITY'].keys(): - SubjectList[key + '_Path'] = "" -QuerySubjectInfo(config, SubjectList, session) - -threshold = config['DATA']['threshold'] -roc_list = [] -sp_list = [] -sensi_list = [] -acc_list = [] -pre_list = [] - -tprs = [] -roc = torchmetrics.ROC() -auroc = torchmetrics.AUROC() -fig = plt.figure() -base_fpr = np.linspace(0, 1, 39) -cm = ConfusionMatrix(num_classes=2) -prediction_labels_full_list = [] -bidx = [21, 25, 0, 4, 6] - -for it in range(0, 5, 1): - iter = bidx[it] - # seed_everything(4200) - dataloader = DataModule(SubjectList, - config=config, - keys=config['MODALITY'].keys(), - train_transform=train_transform, - val_transform=val_transform, - clinical_cols=clinical_cols, - inference=False, - train_size=0.85) - - model = MixModel(module_dict, config) - filename = 'lightning_logs/random_seed_75_Seg/version_' + str(iter) - full_ckpt_path = Path(filename, 'Iter_' + str(iter) + '.ckpt') - model.load_state_dict(torch.load(full_ckpt_path)['state_dict']) - - image = torch.cat([out for i, out in enumerate(dataloader.train_dataloader())], dim=0) - label = torch.cat([out[1] for i, out in enumerate(dataloader.train_dataloader())], dim=0) - - to_explain = dataloader.test_dataloader() - #e = shap.GradientExplainer((model, model.module_dict['Image'].backbone.features[7]), dataloader.train_dataloader()) - #shap_values, indexes = e.shap_values(to_explain, ranked_outputs=2, nsamples=200) - - # get the names for the classes - # index_names = np.vectorize(lambda x: class_names[str(x)][1])(indexes) - - # plot the explanations - #shap_values = [np.swapaxes(np.swapaxes(s, 2, 3), 1, -1) for s in shap_values] - - # shap.image_plot(shap_values, to_explain, index_names) - - diff --git a/Training/test.py b/Training/test.py deleted file mode 100644 index 247157a..0000000 --- a/Training/test.py +++ /dev/null @@ -1,238 +0,0 @@ -import torch -import torchvision -from torch import nn -from pytorch_lightning.callbacks import ModelCheckpoint, EarlyStopping -from pytorch_lightning import LightningDataModule, LightningModule, Trainer, seed_everything -import sys, os -# import torchio as tio -import monai -import numpy as np -torch.cuda.empty_cache() -## Module - Dataloaders -from DataGenerator.DataGenerator import * -from Models.Classifier import Classifier -from Models.Linear import Linear -from Models.MixModel import MixModel -from monai.transforms import EnsureChannelFirstd, ScaleIntensityd, ResampleToMatchd -## Main -from sklearn.preprocessing import StandardScaler, OneHotEncoder -import toml -from Utils.GenerateSmoothLabel import get_smoothed_label_distribution, get_module -from Utils.PredictionReports import PredictionReports -from pathlib import Path -# import torchio as tio -from torchmetrics import ConfusionMatrix -import torchmetrics -import matplotlib.pyplot as plt -config = toml.load(sys.argv[1]) -## 2D transform -img_keys = list(config['MODALITY'].keys()) -# img_keys.remove('Structs') -# if 'Structs' in config['DATA'].keys(): -# for roi in config['DATA']['Structs']: -# img_keys.append('Struct_' + roi) - -train_transform = torchvision.transforms.Compose([ - EnsureChannelFirstd(keys=img_keys), - # ResampleToMatchd(list(set(img_keys).difference(set(['CT']))), key_dst='CT'), - monai.transforms.ScaleIntensityd(keys=img_keys), - #monai.transforms.ResizeWithPadOrCropd(keys=img_keys, spatial_size=config['DATA']['dim']), - monai.transforms.Resized(keys=img_keys, spatial_size=config['DATA']['dim']), - monai.transforms.RandAffined(keys=img_keys), - monai.transforms.RandHistogramShiftd(keys=img_keys), - monai.transforms.RandAdjustContrastd(keys=img_keys), - monai.transforms.RandGaussianNoised(keys=img_keys), - -]) - -val_transform = torchvision.transforms.Compose([ - EnsureChannelFirstd(keys=img_keys), - # ResampleToMatchd(list(set(img_keys).difference(set(['CT']))), key_dst='CT'), - monai.transforms.ScaleIntensityd(img_keys), - #monai.transforms.ResizeWithPadOrCropd(img_keys, spatial_size=config['DATA']['dim']), - monai.transforms.Resized(keys=img_keys, spatial_size=config['DATA']['dim']), -]) - -## First Connect to XNAT -session = xnat.connect(config['SERVER']['Address'], user=config['SERVER']['User'], - password=config['SERVER']['Password']) - -SubjectList = QuerySubjectList(config, session) -SynchronizeData(config, SubjectList) -SubjectList.dropna(subset=['xnat_subjectdata_field_map_survival_months'], inplace=True) - -module_dict = nn.ModuleDict() -if config['DATA']['Multichannel']: ## Single-Model Multichannel learning - if config['MODALITY'].keys(): - module_dict['Image'] = Classifier(config, 'Image') -else: - for key in config['MODALITY'].keys(): # Multi-Model Single Channel learning - module_dict[key] = Classifier(config, key) - -if 'Records' in config.keys(): - SubjectList, clinical_cols = LoadClinicalData(config, SubjectList) - module_dict['Records'] = Linear(in_feat=len(clinical_cols), out_feat=42) -else: - clinical_cols = None - -# print('clinical_cols:', len(clinical_cols)) -## GeneratePath -for key in config['MODALITY'].keys(): - SubjectList[key + '_Path'] = "" -QuerySubjectInfo(config, SubjectList, session) - -threshold = config['DATA']['threshold'] -roc_list = [] -sp_list = [] -sensi_list = [] -acc_list = [] -pre_list = [] - -tprs = [] -roc = torchmetrics.ROC() -auroc = torchmetrics.AUROC() -fig = plt.figure() -base_fpr = np.linspace(0, 1, 39) -cm = ConfusionMatrix(num_classes=2) -prediction_labels_full_list = [] -bidx = [1, 21, 25, 26, 30] -for iter in range(0, 12, 1): - # iter = bidx[it] - # seed_everything(4200) - dataloader = DataModule(SubjectList, - config=config, - keys=config['MODALITY'].keys(), - train_transform=train_transform, - val_transform=val_transform, - clinical_cols=clinical_cols, - inference=False) - - logger = PredictionReports(config=config, save_dir='lightning_logs/test', name= config['DATA']['LogFolder']) - model = MixModel(module_dict, config) - filename = 'lightning_logs/' + config['DATA']['LogFolder'] + '/version_' + str(iter) - # full_ckpt_path = Path(filename, 'Iter_' + str(iter) + '.ckpt') - full_ckpt_path = Path(filename, 'ckpt', 'Iter_' + str(iter) + '.ckpt') - model.load_state_dict(torch.load(full_ckpt_path)['state_dict']) - - model.eval() - print('start testing...') - worstCase = 0 - with torch.no_grad(): - outs = [] - for i, data in enumerate(dataloader.test_dataloader()): - truth = data[1] - x = data[0] - output = model.test_step(data, i) - outs.append(output) - - validation_labels_full = torch.cat([out['label'][1] for i, out in enumerate(outs)], dim=0) - validation_censor_full = torch.cat([out['label'][0] for i, out in enumerate(outs)], dim=0) - prediction_labels_full = torch.cat([out['prediction'] for i, out in enumerate(outs)], dim=0) - roc_i = auroc(prediction_labels_full, validation_labels_full.int()) - roc_list.append(roc_i) - print('roc_' + str(iter), roc_i) - fpr, tpr, _ = roc(prediction_labels_full, validation_labels_full) - #if iter == 1: - # plt.plot(fpr, tpr, 'b', alpha=0.15, label='ROC of each bootstrap') - #else: - # plt.plot(fpr, tpr, 'b', alpha=0.15) - tpr = np.interp(base_fpr, fpr, tpr) - tpr[0] = 0.0 - tprs.append(tpr) - - bcm = cm(prediction_labels_full.round(), validation_labels_full.int()) - tn = bcm[0][0] - tp = bcm[1][1] - fp = bcm[0][1] - fn = bcm[1][0] - - acc = bcm.diag().sum() / bcm.sum() - sensitivity = tp / (tp + fn) - precision = tp / (tp + fp) - spec = tn / (tn + fp) - sp_list.append(spec) - sensi_list.append(sensitivity) - acc_list.append(acc) - pre_list.append(precision) - - prediction_labels_full_list.append(prediction_labels_full.tolist()) - logger.report_test(config, outs, model, prediction_labels_full, [validation_censor_full, validation_labels_full], 'test_') - with open(logger.log_dir + "/test_record.ini", "a") as toml_file: - toml_file.write('\n') - toml_file.write('label_iter_' + str(iter) + ':\n') - toml_file.write(str(validation_labels_full)) - toml_file.write('\n') - toml_file.write('censor_iter_' + str(iter) + ':\n') - toml_file.write(str(validation_censor_full)) - toml_file.write('\n') - toml_file.write('prediction_iter_' + str(iter) + ':\n') - toml_file.write(str(prediction_labels_full)) - toml_file.write('\n') - -prediction_labels = torch.tensor(prediction_labels_full_list).mean(dim=0) -validation_labels = validation_labels_full -mean_tprs = np.array(tprs).mean(axis=0) -std = np.array(tprs).std(axis=0) -tprs_upper = np.minimum(mean_tprs + std, 1) -tprs_lower = mean_tprs - std - -plt.plot(base_fpr, mean_tprs, 'darkorange', label='Bootstrap-averaged ROC') -plt.legend(facecolor=[230/255, 236/255, 237/255]) -plt.fill_between(base_fpr, tprs_lower, tprs_upper, color='grey', alpha=0.3) -plt.plot(base_fpr, base_fpr, 'r--') -plt.title('Test ROC') -plt.xlabel('False Positive Rate') -plt.xlim((0, 1)) -plt.ylabel('True Positive rate') -plt.ylim((0, 1)) -plt.show() - -print('roc_list: ', roc_list) -print('spec:', sp_list) -print('sens:', sensi_list) -print('acc',acc_list) -fig.savefig('temp.png', dpi=fig.dpi, transparent=True) - -roc = auroc(prediction_labels, validation_labels.int()) -bcm = cm(prediction_labels.round(), validation_labels.int()) -tn = bcm[0][0] -tp = bcm[1][1] -fp = bcm[0][1] -fn = bcm[1][0] -acc = bcm.diag().sum() / bcm.sum() -sensitivity = tp / (tp + fn) -precision = tp / (tp + fp) -spec = tn / (tn + fp) - -print('enm_roc', str(roc)) -print('enm_specificity', str(spec)) -print('enm_sensitivity', str(sensitivity)) -print('enm_accuracy', str(acc)) -print('enm_precision', str(precision)) -print('finish test') - - -roc_avg = torch.mean(torch.tensor(roc_list)) -sp_avg = torch.mean(torch.tensor(sp_list)) -sensi_avg = torch.mean(torch.tensor(sensi_list)) -acc_avg = torch.mean(torch.tensor(acc_list)) -pre_avg = torch.mean(torch.tensor(pre_list)) - - -roc_std = torch.std(torch.tensor(roc_list), unbiased=False) -sp_std = torch.std(torch.tensor(sp_list), unbiased=False) -sensi_std = torch.std(torch.tensor(sensi_list), unbiased=False) -acc_std = torch.std(torch.tensor(acc_list), unbiased=False) -pre_std = torch.std(torch.tensor(pre_list), unbiased=False) - -print('avg_roc', str(roc_avg)) -print('avg_specificity', str(sp_avg)) -print('avg_sensitivity', str(sensi_avg)) -print('avg_accuracy', str(acc_avg)) -print('avg_precision', str(pre_avg)) - -print('std_roc', str(roc_std)) -print('std_specificity', str(sp_std)) -print('std_sensitivity', str(sensi_std)) -print('std_accuracy', str(acc_std)) -print('std_precision', str(pre_std)) diff --git a/Training/testR.py b/Training/testR.py deleted file mode 100644 index 211f489..0000000 --- a/Training/testR.py +++ /dev/null @@ -1,157 +0,0 @@ -import torch -import torchvision -from torch import nn -from pytorch_lightning.callbacks import ModelCheckpoint, EarlyStopping -from pytorch_lightning.strategies import DDPStrategy -from pytorch_lightning import LightningDataModule, LightningModule, Trainer, seed_everything -import sys, os -# import torchio as tio -import monai -from Utils.GenerateSmoothLabel import generate_cumulative_dynamic_auc -torch.cuda.empty_cache() -from lifelines.utils import concordance_index -## Module - Dataloaders -from DataGenerator.DataGenerator import * -from Models.Classifier import Classifier -from Models.Linear import Linear -from Models.MixModel import MixModel -from monai.transforms import EnsureChannelFirstd, ScaleIntensityd, ResampleToMatchd -## Main -from sklearn.preprocessing import StandardScaler, OneHotEncoder -import toml -from Utils.GenerateSmoothLabel import get_smoothed_label_distribution, get_module -from Utils.PredictionReports import PredictionReports -from pathlib import Path -from Utils.PredictionReports import r2_index -# import torchio as tio -from torchmetrics import ConfusionMatrix -import torchmetrics -config = toml.load(sys.argv[1]) -## 2D transform -img_keys = list(config['MODALITY'].keys()) -# img_keys.remove('Structs') -# if 'Structs' in config['DATA'].keys(): -# for roi in config['DATA']['Structs']: -# img_keys.append('Struct_' + roi) - -if config['MODALITY'].values(): - train_transform = torchvision.transforms.Compose([ - EnsureChannelFirstd(keys=img_keys), - #ResampleToMatchd(list(set(img_keys).difference(set(['CT']))), key_dst='CT'), - monai.transforms.ScaleIntensityd(keys=list(set(img_keys).difference(set(['Dose'])))), - # monai.transforms.ResizeWithPadOrCropd(keys=img_keys, spatial_size=config['DATA']['dim']), - monai.transforms.Resized(keys=img_keys, spatial_size=config['DATA']['dim']), - monai.transforms.RandAffined(keys=img_keys), - monai.transforms.RandHistogramShiftd(keys=img_keys), - monai.transforms.RandAdjustContrastd(keys=img_keys), - monai.transforms.RandGaussianNoised(keys=img_keys), - - ]) - - val_transform = torchvision.transforms.Compose([ - EnsureChannelFirstd(keys=img_keys), - #ResampleToMatchd(list(set(img_keys).difference(set(['CT']))), key_dst='CT'), - monai.transforms.ScaleIntensityd(list(set(img_keys).difference(set(['Dose'])))), - # monai.transforms.ResizeWithPadOrCropd(img_keys, spatial_size=config['DATA']['dim']), - monai.transforms.Resized(keys=img_keys, spatial_size=config['DATA']['dim']), - ]) -else: - train_transform = None - val_transform = None - -## First Connect to XNAT -session = xnat.connect(config['SERVER']['Address'], user=config['SERVER']['User'], - password=config['SERVER']['Password']) - -SubjectList = QuerySubjectList(config, session) -SynchronizeData(config, SubjectList) -SubjectList.dropna(subset=['xnat_subjectdata_field_map_survival_months'], inplace=True) - -module_dict = nn.ModuleDict() -if config['DATA']['Multichannel']: ## Single-Model Multichannel learning - if config['MODALITY'].keys(): - module_dict['Image'] = Classifier(config, 'Image') -else: - for key in config['MODALITY'].keys(): # Multi-Model Single Channel learning - module_dict[key] = Classifier(config, key) - -if 'Records' in config.keys(): - SubjectList, clinical_cols = LoadClinicalData(config, SubjectList) - module_dict['Records'] = Linear(in_feat=len(clinical_cols), out_feat=42) -else: - clinical_cols = None - -#print('clinical_cols:', len(clinical_cols)) -## GeneratePath -for key in config['MODALITY'].keys(): - SubjectList[key + '_Path'] = "" -QuerySubjectInfo(config, SubjectList, session) - -cindexs =[] -prediction_labels_full_list = [] -# bidx = [21, 25, 0, 4, 6] -auroc = torchmetrics.AUROC(num_classes=1) -for iter in range(0, 2, 1): - # iter = bidx[it] - # seed_everything(4200) - dataloader = DataModule(SubjectList, - config=config, - keys=config['MODALITY'].keys(), - train_transform=train_transform, - val_transform=val_transform, - clinical_cols=clinical_cols, - inference=False, - train_size=0.85) - - - - #censored_label = not(np.int8(dataloader.test_list.loc[i, 'xnat_subjectdata_field_map_' + config['DATA']['censor_label']]).astype('bool')) - logger = PredictionReports(config=config, save_dir='lightning_logs/test', name= config['DATA']['LogFolder']) - model = MixModel(module_dict, config) - filename = 'lightning_logs/' + config['DATA']['LogFolder'] + '/version_' + str(iter) - full_ckpt_path = Path(filename, 'Iter_' + str(iter) + '.ckpt') - # full_ckpt_path = Path(ckpt_path, 'ckpt', 'Iter_' + str(iter) + '.ckpt') - # full_ckpt_path = 'Iter_' + str(iter) + '.ckpt' - model.load_state_dict(torch.load(full_ckpt_path)['state_dict']) - # model.load_state_dict(torch.load(full_ckpt_path, map_location='cpu')['state_dict']) - model.eval() - print('start testing...') - worstCase = 0 - with torch.no_grad(): - outs = [] - for i, data in enumerate(dataloader.test_dataloader()): - truth = data[1] - x = data[0] - output = model.test_step(data, i) - outs.append(output) - - validation_labels_full = torch.cat([out['label'][1] for i, out in enumerate(outs)], dim=0) - validation_censor_full = torch.cat([out['label'][0] for i, out in enumerate(outs)], dim=0) - prediction_labels_full = torch.cat([out['prediction'] for i, out in enumerate(outs)], dim=0) - cindex = concordance_index(validation_labels_full, prediction_labels_full, validation_censor_full) - print('cindex_' + str(iter), cindex) - cindexs.append(cindex) - prediction_labels_full_list.append(prediction_labels_full.tolist()) - logger.report_test(config, outs, model, prediction_labels_full, [validation_censor_full, validation_labels_full], 'test_') - with open(logger.log_dir + "/test_record.ini", "a") as toml_file: - toml_file.write('\n') - toml_file.write('label_iter_' + str(iter) + ':\n') - toml_file.write(str(validation_labels_full)) - toml_file.write('\n') - toml_file.write('censor_iter_' + str(iter) + ':\n') - toml_file.write(str(validation_censor_full)) - toml_file.write('\n') - toml_file.write('prediction_iter_' + str(iter) + ':\n') - toml_file.write(str(prediction_labels_full)) - toml_file.write('\n') - -prediction_labels = torch.tensor(prediction_labels_full_list).mean(dim=0) -validation_labels = validation_labels_full -fig_path = 'lightning_logs/test/' + config['DATA']['LogFolder'] - -cindex_tol = concordance_index(validation_labels, prediction_labels, validation_censor_full) -r2_tol = r2_index(prediction_labels, validation_labels) - -print('cindex series', cindexs) -print('avg_roc', str(cindex_tol)) -print('avg_r2', str(r2_tol)) From ed8159d43b5b2deb96aaf60e781b5f13023e92ba Mon Sep 17 00:00:00 2001 From: Clara Date: Fri, 21 Apr 2023 12:08:53 +0100 Subject: [PATCH 08/12] label censor --- DataGenerator/DataGenerator.py | 6 +-- Models/MixModel.py | 93 +++++++++++++++++----------------- SubmitScript/submitjob4.pbs | 2 +- Utils/PredictionReports.py | 39 +++++++------- submitjob.pbs | 39 -------------- 5 files changed, 70 insertions(+), 109 deletions(-) delete mode 100644 submitjob.pbs diff --git a/DataGenerator/DataGenerator.py b/DataGenerator/DataGenerator.py index 843a46d..489017d 100644 --- a/DataGenerator/DataGenerator.py +++ b/DataGenerator/DataGenerator.py @@ -87,14 +87,14 @@ def __getitem__(self, i): else: ##Training label = torch.tensor( np.float(self.SubjectList.loc[i, "xnat_subjectdata_field_map_" + self.config['DATA']['target']])) - censored_label = not (np.int8( + censor_status = not (np.int8( self.SubjectList.loc[i, 'xnat_subjectdata_field_map_' + self.config['DATA']['censor_label']]).astype( 'bool')) if 'threshold' in self.config['DATA'].keys(): ## Classification label = torch.where(label > self.config['DATA']['threshold'], 1, 0) label = torch.as_tensor(label, dtype=torch.float32) - label = (censored_label, label) - return data, label + #label = (censored_label, label) + return data, censor_status, label ### DataLoader diff --git a/Models/MixModel.py b/Models/MixModel.py index 9b888be..bceb51d 100644 --- a/Models/MixModel.py +++ b/Models/MixModel.py @@ -17,7 +17,9 @@ def __init__(self, module_dict, config, loss_fcn=torch.nn.BCEWithLogitsLoss()): self.loss_fcn = getattr(torch.nn, self.config["MODEL"]["Loss_Function"])(pos_weight=torch.tensor(1.18)) self.activation = getattr(torch.nn, self.config["MODEL"]["Activation"])() self.classifier = nn.Sequential( - nn.Linear(out_feat, 120), + nn.Linear(out_feat, 256), + nn.Dropout(0.05), + nn.Linear(256, 120), nn.Dropout(0.05), nn.Linear(120, 40), nn.Dropout(0.05), @@ -31,79 +33,78 @@ def forward(self, data_dict): return prediction def training_step(self, batch, batch_idx): - data_dict, label = batch ## Data_dict is [B, NM, Sx, Sy, Sz, C], Label is [B,(1,1)] + data_dict, censor_status, label = batch prediction = self.forward(data_dict).squeeze(dim=1) - loss = self.loss_fcn(prediction, label[-1]) - self.log("train_loss", loss, on_step=True, on_epoch=True, sync_dist=True) - MAE = torch.abs(prediction - label[-1]) + loss = self.loss_fcn(prediction, label) + self.log("train_loss", loss, on_step=False, on_epoch=True, sync_dist=True) + MAE = torch.abs(prediction - label) out = copy.deepcopy(data_dict) out['MAE'] = MAE.detach() out['prediction'] = prediction.detach() - out['label'] = label + out['label'] = label + out['censor_status'] = censor_status out['loss'] = loss return out def training_epoch_end(self, step_outputs): - labels = [] - for j in range(0, len(step_outputs[0]['label'])): - labels.append(torch.cat([out['label'][j] for i, out in enumerate(step_outputs)], dim=0)) + labels = torch.cat([out['label'] for i, out in enumerate(step_outputs)], dim=0) + censor_status = torch.cat([out['censor_status'] for i, out in enumerate(step_outputs)], dim=0) prediction = torch.cat([out['prediction'] for i, out in enumerate(step_outputs)], dim=0) - self.logger.report_epoch(prediction, labels, step_outputs,self.current_epoch, 'train_epoch_') - with open(self.logger.log_dir + "/train_record.ini", "a") as toml_file: - toml_file.write('\n') - toml_file.write('label_epoch_' + str(self.current_epoch) + ':\n') - toml_file.write(str(labels[1])) - toml_file.write('\n') - toml_file.write('censor_epoch_' + str(self.current_epoch) + ':\n') - toml_file.write(str(labels[0])) - toml_file.write('\n') - toml_file.write('prediction_epoch_' + str(self.current_epoch) + ':\n') - toml_file.write(str(prediction)) - toml_file.write('\n') + self.logger.report_epoch(prediction, censor_status, labels, step_outputs,self.current_epoch, 'train_epoch_') + # with open(self.logger.log_dir + "/train_record.ini", "a") as toml_file: + # toml_file.write('\n') + # toml_file.write('label_epoch_' + str(self.current_epoch) + ':\n') + # toml_file.write(str(labels[1])) + # toml_file.write('\n') + # toml_file.write('censor_epoch_' + str(self.current_epoch) + ':\n') + # toml_file.write(str(labels[0])) + # toml_file.write('\n') + # toml_file.write('prediction_epoch_' + str(self.current_epoch) + ':\n') + # toml_file.write(str(prediction)) + # toml_file.write('\n') def validation_step(self, batch, batch_idx): - data_dict, label = batch + data_dict, censor_status, label = batch prediction = self.forward(data_dict).squeeze(dim=1) - loss = self.loss_fcn(prediction, label[-1]) - self.log("val_loss", loss, on_step=True, on_epoch=True, sync_dist=True) - #acc = nn.MSELoss()(prediction.round(), label[-1]) - #self.log("val_acc", acc, on_step=True, on_epoch=True, sync_dist=True) - MAE = torch.abs(prediction - label[-1]) + loss = self.loss_fcn(prediction, label) + self.log("val_loss", loss, on_step=False, on_epoch=True, sync_dist=True) + MAE = torch.abs(prediction - label) out = copy.deepcopy(data_dict) out['MAE'] = MAE out['prediction'] = prediction + out['censor_status'] = censor_status out['label'] = label out['loss'] = loss return out def validation_epoch_end(self, step_outputs): - labels = [] - for j in range(0, len(step_outputs[0]['label'])): - labels.append(torch.cat([out['label'][j] for i, out in enumerate(step_outputs)], dim=0)) + labels = torch.cat([out['label'] for i, out in enumerate(step_outputs)], dim=0) + censor_status = torch.cat([out['censor_status'] for i, out in enumerate(step_outputs)], dim=0) prediction = torch.cat([out['prediction'] for i, out in enumerate(step_outputs)], dim=0) - self.logger.report_epoch(prediction.squeeze(), labels, step_outputs, self.current_epoch,'val_epoch_') - with open(self.logger.log_dir + "/val_record.ini", "a") as toml_file: - toml_file.write('\n') - toml_file.write('label_epoch_' + str(self.current_epoch) + ':\n') - toml_file.write(str(labels[1])) - toml_file.write('\n') - toml_file.write('censor_epoch_' + str(self.current_epoch) + ':\n') - toml_file.write(str(labels[0])) - toml_file.write('\n') - toml_file.write('prediction_epoch_' + str(self.current_epoch) + ':\n') - toml_file.write(str(prediction)) - toml_file.write('\n') + self.logger.report_epoch(prediction, censor_status, labels, step_outputs, self.current_epoch, 'val_epoch_') + # with open(self.logger.log_dir + "/val_record.ini", "a") as toml_file: + # toml_file.write('\n') + # toml_file.write('label_epoch_' + str(self.current_epoch) + ':\n') + # toml_file.write(str(labels[1])) + # toml_file.write('\n') + # toml_file.write('censor_epoch_' + str(self.current_epoch) + ':\n') + # toml_file.write(str(labels[0])) + # toml_file.write('\n') + # toml_file.write('prediction_epoch_' + str(self.current_epoch) + ':\n') + # toml_file.write(str(prediction)) + # toml_file.write('\n') def test_step(self, batch, batch_idx): - data_dict, label = batch + data_dict, censor_status, label = batch prediction = self.forward(data_dict).squeeze(dim=1) - loss = self.loss_fcn(prediction, label[-1]) - MAE = torch.abs(prediction - label[-1]) + loss = self.loss_fcn(prediction, label) + MAE = torch.abs(prediction - label) out = copy.deepcopy(data_dict) out['MAE'] = MAE out['prediction'] = prediction out['label'] = label - out['loss'] = loss + out['censor_status'] = censor_status + out['loss'] = loss return out def weights_init(self, m): diff --git a/SubmitScript/submitjob4.pbs b/SubmitScript/submitjob4.pbs index 38fac5f..eb3bf9c 100644 --- a/SubmitScript/submitjob4.pbs +++ b/SubmitScript/submitjob4.pbs @@ -32,7 +32,7 @@ conda activate UCL_OP export PYTHONPATH=$PYTHONPATH:/home/dgs1/Software/OutcomePrediction export LS_COLORS="$LS_COLORS:ow=1;34:tw=1;34:" #### ENVIRONMENT VARIABLES ABOVE AUTOMATICALLY ADDED FROM QSUB #### -python Training/Classification.py Configs/Classification/SettingsRTOG_CT_Resnet.ini +python Training/Classification.py Configs/Classification/SettingsRTOG_All_Nii_Resnet.ini ### Run your job here #printenv diff --git a/Utils/PredictionReports.py b/Utils/PredictionReports.py index 74ce792..17daac7 100644 --- a/Utils/PredictionReports.py +++ b/Utils/PredictionReports.py @@ -21,6 +21,7 @@ from pytorch_lightning.loggers import TensorBoardLogger from torchmetrics import ConfusionMatrix + class PredictionReports(TensorBoardLogger): def __init__(self, config, save_dir: str, @@ -86,16 +87,15 @@ def log_text(self) -> None: # str(self.config['MODALITY'].keys()) self.experiment.add_text('configurations:', configurations) - def regression_matrix(self, prediction, label, prefix): ## matrix should be metrics + def regression_matrix(self, prediction, censor_status, label, prefix): ## matrix should be metrics r_out = {} if 'cindex' in self.config['CHECKPOINT']['matrix']: - cindex = concordance_index(label[1].cpu().detach().numpy(), prediction.cpu().detach().numpy(), label[0].cpu().detach().numpy()) + cindex = concordance_index(label.cpu().detach().numpy(), prediction.cpu().detach().numpy(), + censor_status.cpu().detach().numpy()) r_out[prefix + 'cindex'] = cindex if 'r2' in self.config['CHECKPOINT']['matrix']: - r2 = r2_index(prediction.detach(), label[1].detach()) + r2 = r2_index(prediction.detach(), label.detach()) r_out[prefix + 'r2'] = r2 - if 'train' in prefix: - print('r2', r2) return r_out def classification_matrix(self, prediction, label, prefix): @@ -111,7 +111,7 @@ def classification_matrix(self, prediction, label, prefix): accuracy = auroc(prediction, label.int()) c_out[prefix + 'roc'] = accuracy if 'Specificity' in self.config['CHECKPOINT']['matrix']: - spec = tn /(tn + fp) + spec = tn / (tn + fp) c_out[prefix + 'specificity'] = spec if 'Sensitivity' in self.config['CHECKPOINT']['matrix']: @@ -176,7 +176,8 @@ def worst_case_show(self, validation_step_outputs, prefix): if 'CT' in self.config['MODALITY'].keys(): worst_img = data['Image'][idx][0, :, :, :] if 'Dose' in self.config['MODALITY'].keys(): - worst_dose = data['Image'][idx][1, :, :, :] ### this index needs to be careful when adding pet image + worst_dose = data['Image'][idx][1, :, :, + :] ### this index needs to be careful when adding pet image worst_AE = loss[idx] label = data['slabel'][idx] out[prefix + 'worst_AE'] = worst_AE @@ -187,19 +188,17 @@ def worst_case_show(self, validation_step_outputs, prefix): out[prefix + 'slabel'] = label return out - def report_epoch(self, prediction, label, validation_step_outputs, + def report_epoch(self, prediction, censor_status, label, validation_step_outputs, current_epoch, prefix) -> None: if self.config['MODEL']['Prediction_type'] == 'Regression': - regression_out = self.regression_matrix(prediction, label, prefix) - if 'train' in prefix: - print('regression_matrix:', regression_out) + regression_out = self.regression_matrix(prediction, censor_status, label, prefix) self.log_metrics(regression_out, current_epoch) if self.config['MODEL']['Prediction_type'] == 'Classification': - classification_out = self.classification_matrix(prediction.squeeze(), label[1], prefix) + classification_out = self.classification_matrix(prediction.squeeze(), label, prefix) self.log_metrics(classification_out, current_epoch) if 'AUROC' in self.config['CHECKPOINT']['matrix']: - self.plot_AUROC(prediction.squeeze(), label[0], prefix, current_epoch) + self.plot_AUROC(prediction.squeeze(), label, prefix, current_epoch) if 'WorstCase' in self.config['CHECKPOINT']['matrix']: worst_record = self.worst_case_show(validation_step_outputs, prefix) @@ -212,7 +211,7 @@ def report_epoch(self, prediction, label, validation_step_outputs, text = 'validate_worst_case_dose' self.log_image(worst_record[prefix + 'worst_dose'], text, current_epoch) - def report_test(self, config, outs, model, prediction_labels, validation_labels, prefix): + def report_test(self, config, outs, model, prediction_labels, validation_censor, validation_labels, prefix): if 'WorstCase' in config['CHECKPOINT']['matrix']: worst_record = self.worst_case_show(outs, prefix) self.experiment.add_text('worst_test_AE: ', str(worst_record[prefix + 'worst_AE'])) @@ -225,16 +224,16 @@ def report_test(self, config, outs, model, prediction_labels, validation_labels, self.log_image(worst_record[prefix + 'worst_dose'], text) if config['MODEL']['Prediction_type'] == 'Regression': - self.experiment.add_text('test loss: ', str(model.loss_fcn(prediction_labels, validation_labels[1]))) - regression_out = self.regression_matrix(prediction_labels, validation_labels, prefix) + self.experiment.add_text('test loss: ', str(model.loss_fcn(prediction_labels, validation_labels))) + regression_out = self.regression_matrix(prediction_labels, validation_censor, validation_labels, prefix) self.experiment.add_text('test_cindex: ', str(regression_out[prefix + 'cindex'])) self.experiment.add_text('test_r2: ', str(regression_out[prefix + 'r2'])) return regression_out if config['MODEL']['Prediction_type'] == 'Classification': - classification_out = self.classification_matrix(prediction_labels.squeeze(), validation_labels[1], prefix) + classification_out = self.classification_matrix(prediction_labels.squeeze(), validation_labels, prefix) if 'AUROC' in config['CHECKPOINT']['matrix']: - self.plot_AUROC(prediction_labels, validation_labels[0], prefix) + self.plot_AUROC(prediction_labels, validation_labels, prefix) self.experiment.add_text('test_AUROC: ', str(classification_out[prefix + 'roc'])) if 'Specificity' in config['CHECKPOINT']['matrix']: self.experiment.add_text('Specificity:', str(classification_out[prefix + 'specificity'])) @@ -250,8 +249,8 @@ def report_test(self, config, outs, model, prediction_labels, validation_labels, def r2_index(prediction, label): - loss = nn.MSELoss(reduction='sum') - SSres = loss(prediction, label) + loss = nn.MSELoss(reduction='sum') + SSres = loss(prediction, label) SStotal = torch.sum(torch.square(label - torch.mean(label))) r2 = 1 - SSres / SStotal return r2 diff --git a/submitjob.pbs b/submitjob.pbs deleted file mode 100644 index fb89aa9..0000000 --- a/submitjob.pbs +++ /dev/null @@ -1,39 +0,0 @@ -#!/bin/bash -### Job Name -#PBS -N OP -### Project code -#PBS -A AI_OutcomePrediction -### Maximum time this job can run before being killed (here, 1 day) -#PBS -l walltime=15:00:00:00 -### Resource Request (must contain cpucore, memory, and gpu (even if requested amount is zero) -#PBS -l cpucore=30:memory=200gb:gpu=4 -### Output Options (default is stdout_and_stderr) -#PBS -l outputMode=stdout_and_stderr -##PBS -l outputMode=no_output -##PBS -l outputMode=stdout_only -##PBS -l outputMode=stderr_only - -#### ENVIRONMENT VARIABLES BELOW AUTOMATICALLY ADDED FROM QSUB #### -# >>> conda initialize >>> -# !! Contents within this block are managed by 'conda init' !! -__conda_setup="$('/home/dgs1/anaconda3/bin/conda' 'shell.bash' 'hook' 2> /dev/null)" -if [ $? -eq 0 ]; then - eval "$__conda_setup" -else - if [ -f "/home/dgs1/anaconda3/etc/profile.d/conda.sh" ]; then - . "/home/dgs1/anaconda3/etc/profile.d/conda.sh" - else - export PATH="/home/dgs1/anaconda3/bin:$PATH" - fi -fi -unset __conda_setup -# <<< conda initialize <<< -conda activate UCL_OP -export PYTHONPATH=$PYTHONPATH:/home/dgs1/Software/OutcomePrediction -export LS_COLORS="$LS_COLORS:ow=1;34:tw=1;34:" -#### ENVIRONMENT VARIABLES ABOVE AUTOMATICALLY ADDED FROM QSUB #### -#python Training/Regression.py Configs/Regression/SettingsRTOG_Multi_Apple.ini -python Training/Regression.py Configs/Regression/SettingsRTOG_Multi2.ini -### Run your job here -#printenv -#python Training/Preprocess.py ConfigDefault.ini From 1b3f6b35498f524aa11547d74965f8c62879e340 Mon Sep 17 00:00:00 2001 From: Clara Date: Fri, 21 Apr 2023 14:44:43 +0100 Subject: [PATCH 09/12] clean Util --- Training/Classification.py | 2 +- Utils/#DicomTools.py# | 170 ------------------------------ Utils/ConvertNii.py | 105 ------------------ Utils/Count.py | 68 ------------ Utils/CropMask.py | 32 ------ Utils/DICOM2NIFTY.py | 66 ------------ Utils/DoseNii.py | 71 ------------- Utils/FMaskDcm.py | 75 ------------- Utils/FormMasks.py | 70 ------------ Utils/FormMasksLocal.py | 58 ---------- Utils/PTV.py | 103 ------------------ Utils/RenameDicomPatientID.py | 48 --------- Utils/SegCT.py | 24 ----- Utils/check.py | 54 ---------- Utils/remove/RTOG.py | 54 ---------- Utils/remove/RTOG_nii.py | 38 ------- Utils/renameCT.py | 99 ----------------- Utils/sub_dose_analysis_total.py | 88 ---------------- Utils/test.py | 52 --------- Utils/xnat_data_uniformization.py | 95 ----------------- temp.png | Bin 43220 -> 0 bytes 21 files changed, 1 insertion(+), 1371 deletions(-) delete mode 100644 Utils/#DicomTools.py# delete mode 100644 Utils/ConvertNii.py delete mode 100644 Utils/Count.py delete mode 100644 Utils/CropMask.py delete mode 100644 Utils/DICOM2NIFTY.py delete mode 100644 Utils/DoseNii.py delete mode 100644 Utils/FMaskDcm.py delete mode 100644 Utils/FormMasks.py delete mode 100644 Utils/FormMasksLocal.py delete mode 100644 Utils/PTV.py delete mode 100644 Utils/RenameDicomPatientID.py delete mode 100644 Utils/SegCT.py delete mode 100644 Utils/check.py delete mode 100644 Utils/remove/RTOG.py delete mode 100644 Utils/remove/RTOG_nii.py delete mode 100644 Utils/renameCT.py delete mode 100644 Utils/sub_dose_analysis_total.py delete mode 100644 Utils/test.py delete mode 100644 Utils/xnat_data_uniformization.py delete mode 100644 temp.png diff --git a/Training/Classification.py b/Training/Classification.py index 0d5d361..40f3fc9 100644 --- a/Training/Classification.py +++ b/Training/Classification.py @@ -83,7 +83,7 @@ def threshold_at_one(x): QuerySubjectInfo(config, SubjectList) print(SubjectList) -for iter in range(0, 15, 1): +for iter in range(0, 3, 1): seed_everything(np.random.randint(0, 10000), workers=True) dataloader = DataModule(SubjectList, config=config, diff --git a/Utils/#DicomTools.py# b/Utils/#DicomTools.py# deleted file mode 100644 index c6a625c..0000000 --- a/Utils/#DicomTools.py# +++ /dev/null @@ -1,170 +0,0 @@ -import os -import glob -import cv2 -import SimpleITK as sitk -import pydicom as dicom -from pydicom import dcmread -import numpy as np -import matplotlib -import matplotlib.pyplot as plt -from matplotlib.path import Path -from monai.data import ITKReader, PILReader -import torchio as tio -from sklearn.preprocessing import KBinsDiscretizer - -sitk.ProcessObject_SetGlobalWarningDisplay(False) -from rt_utils import RTStructBuilder -from scipy.ndimage import * - - -def get_bbox_from_mask(mask, img_shape): - pos = np.where(mask) - if pos[0].shape[0] == 0: - bbox = np.zeros((0, 4)) - else: - xmin = np.min(pos[3]) - xmax = np.max(pos[3]) - ymin = np.min(pos[2]) - ymax = np.max(pos[2]) - zmin = np.min(pos[1]) - zmax = np.max(pos[1]) - bbox = [zmin, zmax, ymin, ymax, xmin, xmax] - return bbox - - -def ReadDicom(dicom_path, view_image=False): - Reader = sitk.ImageSeriesReader() - filenames = sorted(glob.glob(dicom_path + '/*.dcm')) - Reader.SetFileNames(sorted(filenames)) - - assert len(filenames) > 0 - Session = Reader.Execute() - return Session - - -def ResamplingITK(Session, Reference, is_label=False, pad_value=0): - resample = sitk.ResampleImageFilter() - resample.SetOutputSpacing(Reference.GetSpacing()) - resample.SetSize(Reference.GetSize()) - resample.SetOutputDirection(Reference.GetDirection()) - resample.SetOutputOrigin(Reference.GetOrigin()) - resample.SetTransform(sitk.Transform()) - resample.SetDefaultPixelValue(Session.GetPixelIDValue()) - - if is_label: - resample.SetInterpolator(sitk.sitkNearestNeighbor) - else: - resample.SetInterpolator(sitk.sitkLinear) - Resampled = resample.Execute(Session) - return Resampled - - -def RStoContour(rs_path, targetROI='PTV'): - rs_file = glob.glob(rs_path + '*.dcm') - ds = dcmread(rs_file[0]) - for item in ds.StructureSetROISequence: - if item.ROIName == targetROI: - ROI = ds.ROIContourSequence[item.ROINumber - 1] - contours = [contour for contour in ROI.ContourSequence] - return contours - - -def poly_to_mask(polygon, img_shape): - x, y = np.meshgrid(np.arange(img_shape[0]), np.arange(img_shape[1])) - x, y = x.flatten(), y.flatten() - points = np.vstack((x, y)).T - path = Path(polygon) - mask = path.contains_points(points) - mask = mask.reshape(img_shape) - - return mask - - -def ViewROI(patient_id, img_array, mask_array, ROIbox, Inputbox): - masked = np.ma.masked_where(mask_array == 0, mask_array) - plt.subplot(1, 3, 1) - plt.title('{} ROI mask'.format(patient_id)) - plt.imshow(img_array, cmap='gray') - plt.imshow(masked, vmin=0, vmax=1, alpha=0.5) - plt.subplot(1, 3, 2) - plt.title('ROI Box') - plt.imshow(ROIbox, cmap='gray') - plt.subplot(1, 3, 3) - plt.title('Input Box') - plt.imshow(Inputbox, cmap='gray') - plt.show() - - -def get_masked_img_voxel(ImageVoxel, mask_voxel): - bbox = get_bbox_from_mask(mask_voxel, np.shape(ImageVoxel)) - assert len(mask_voxel) == ImageVoxel.shape[0] - img_masked = ImageVoxel[:, bbox[0]:bbox[1], bbox[2]:bbox[3], bbox[4]:bbox[5]] - return img_masked - - -def img_train_transform(img_dim): - transform = tio.Compose([ - tio.transforms.ZNormalization(), - tio.RandomAffine(), - tio.RandomFlip(), - tio.RandomNoise(), - tio.RandomMotion(), - tio.transforms.Resize(img_dim), - tio.RescaleIntensity(out_min_max=(0, 1)) - ]) - return transform - - -def img_val_transform(img_dim): - transform = tio.Compose([ - tio.transforms.ZNormalization(), - tio.transforms.Resize(img_dim), - tio.RescaleIntensity(out_min_max=(0, 1)) - ]) - return transform - - -def class_stratify(SubjectList, config): - ptarget = SubjectList['xnat_subjectdata_field_map_' + config['DATA']['target']] - kbins = KBinsDiscretizer(n_bins=5, encode='ordinal', strategy='uniform') - ptarget = np.array(ptarget).reshape((len(ptarget), 1)) - data_trans = kbins.fit_transform(ptarget) - return data_trans - - -def get_RS_masks(slabel, CTPath, mask_imgs, RSfile, mask_names): - #RS = RTStructBuilder.create_from(dicom_series_path=CTPath, rt_struct_path=RSfile) - #roi_names = RS.get_roi_names() - #strList = [x.lower() for x in roi_names] - #for idx, roi in enumerate(mask_names): - # if roi.lower() in strList: - # roi_s = roi_names[strList.index(roi.lower())] - # mask_img = RS.get_roi_mask_by_name(roi_s) - # # mask_img = distance_transform_edt(mask_img) - # mask_imgs = BitSet(mask_imgs, idx * np.ones_like(mask_imgs), mask_img) - # else: - # raise ValueError(slabel + " has no ROI of name " + roi + " found in RTStruct") - # - #return mask_imgs - - RS = RTStructBuilder.create_from(dicom_series_path=CTPath, rt_struct_path=RSfile) - roi_names = RS.get_roi_names() - strList = [x.lower() for x in roi_names] - for idx, roi in enumerate(mask_names): - if roi.lower() in strList: - roi_s = roi_names[strList.index(roi.lower())] - mask_img = RS.get_roi_mask_by_name(roi_s) - mask_img = distance_transform_edt(mask_img) - mask_imgs = mask_imgs + mask_img - else: - raise ValueError(slabel + " has no ROI of name " + roi + " found in RTStruct") - - return mask_imgs - - -def BitSet(n, p, b): - p = p.astype(int) - n = n.astype(int) - mask = 1 << p - bm = b << p - return (n & ~mask) | bm diff --git a/Utils/ConvertNii.py b/Utils/ConvertNii.py deleted file mode 100644 index 4633144..0000000 --- a/Utils/ConvertNii.py +++ /dev/null @@ -1,105 +0,0 @@ -import sys -# sys.path.insert(0, '/home/dgs1/Software/OutcomePrediction/') -import toml -import glob -import nibabel as nib -import numpy as np -from Utils.DicomTools import * -from pathlib import Path -from DataGenerator.DataGenerator import SynchronizeData, QuerySubjectList -from Utils.DicomTools import QuerySubjectInfo -from Utils.DicomTools import * -import os -from rt_utils import RTStructBuilder -import xnat -from Utils.FixRTSS import * - -session = xnat.connect('http://128.16.11.124:8080/xnat', user='admin', password='mortavar1977') -import csv -from monai.transforms import Spacing, LoadImage, EnsureChannelFirst, ResampleToMatch -from monai.data import MetaTensor -import itk - -config = toml.load(sys.argv[1]) -from scipy import ndimage - -session = xnat.connect(config['SERVER']['Address'], user=config['SERVER']['User'], - password=config['SERVER']['Password']) -SubjectList = QuerySubjectList(config, session) -print(SubjectList) -SynchronizeData(config, SubjectList) - -for key in config['MODALITY'].keys(): - SubjectList[key + '_Path'] = "" - -QuerySubjectInfo(config, SubjectList, session) - -roi_name = 'ptv' -path = config['DATA']['NiiFolder'] -sPatient = SubjectList - -se = ndimage.generate_binary_structure(3, 3) - -for i in range(0, len(SubjectList), 1): - subjectid = sPatient.loc[i, 'subjectid'] - subject_label = sPatient.loc[i, 'subject_label'] - # CTPath = SubjectList[SubjectList.subjectid == subjectid]['CT_Path'] - CTPath = SubjectList.loc[i, 'CT_Path'] - CTArray, meta = LoadImage()(CTPath) - DosePath = SubjectList.loc[i, 'Dose_Path'] - DoseArray, dmeta = LoadImage()(DosePath) - DoseArray = DoseArray * np.double(dmeta['3004|000e']) - - ct = EnsureChannelFirst()(CTArray) - ct = Spacing(pixdim=(1, 1, 3))(ct) - DoseArray = EnsureChannelFirst()(DoseArray) - dose = ResampleToMatch()(DoseArray, ct) - - names_generator = itk.GDCMSeriesFileNames.New() - names_generator.SetUseSeriesDetails(True) - names_generator.AddSeriesRestriction("0008|0021") # Series Date - names_generator.SetDirectory(list(CTPath)[0]) - series_uid = names_generator.GetSeriesUIDs() - print('No.{}:'.format(i) + str(subject_label)) - - if len(series_uid) > 1: - print(series_uid) - else: - ct_array = ct.array.squeeze() - - ni_ct = nib.Nifti1Image(np.double(ct_array), affine=ct.affine) - dose_array = dose.array.squeeze() - ni_dose = nib.Nifti1Image(np.double(dose_array), affine=dose.affine) - spath = Path(path, subject_label) - - if not os.path.isdir(spath): - os.mkdir(spath) - nib.save(ni_ct, Path(spath, 'CT.nii.gz')) - nib.save(ni_dose, Path(spath, 'Dose.nii.gz')) - - # First define the ROI based on target - RSPath = SubjectList[SubjectList.subjectid == subjectid]['Structs_Path'] - # FixRTSS(RSPath[0], list(CTPath)[0]) - RS = RTStructBuilder.create_from(dicom_series_path=list(CTPath)[0], rt_struct_path=list(RSPath)[0]) - # %% - roi_names = RS.get_roi_names() - strList = [x.lower() for x in roi_names] - r1 = [r for r in strList if roi_name in r] - - if len(r1) > 0: - mask = np.zeros_like(CTArray) - for j in range(len(r1)): - roi = r1[j] - index = strList.index(roi.lower()) - mask_img = RS.get_roi_mask_by_name(roi_names[index]) - mask_img = np.rot90(mask_img) - mask_img = np.flip(mask_img, 0) - mask = mask + mask_img - - mask = ndimage.binary_dilation(mask, structure=se, iterations=3) ## if needs expansion - mask = MetaTensor(mask.copy(), meta=meta) - mask = EnsureChannelFirst()(mask) - mask = Spacing(pixdim=(1, 1, 3))(mask) - mask_array = mask.array.squeeze() - ni_mask = nib.Nifti1Image(mask_array.astype('int'), affine=mask.affine, dtype='uint8') - nib.save(ni_mask, Path(spath, 'struct_TS', 'AI_target.nii.gz')) diff --git a/Utils/Count.py b/Utils/Count.py deleted file mode 100644 index fb24ac7..0000000 --- a/Utils/Count.py +++ /dev/null @@ -1,68 +0,0 @@ -import sys -import toml -import glob -import nibabel as nib -import itertools -import numpy as np -from Utils.DicomTools import * -from pathlib import Path -from DataGenerator.DataGenerator import QuerySubjectList, SynchronizeData, QuerySubjectInfo -from Utils.DicomTools import * -import os -from rt_utils import RTStructBuilder -import xnat -from Utils.FixRTSS import * -session = xnat.connect('http://128.16.11.124:8080/xnat', user='admin', password='mortavar1977') -import csv -from monai.transforms import Spacing, LoadImage, EnsureChannelFirst -from monai.data import MetaTensor -import itk -from collections import Counter -config = toml.load(sys.argv[1]) - -session = xnat.connect(config['SERVER']['Address'], user=config['SERVER']['User'],password=config['SERVER']['Password']) -SubjectList = QuerySubjectList(config, session) -SubjectList.dropna(subset=['xnat_subjectdata_field_map_survival_months'], inplace=True) -print(SubjectList) -SynchronizeData(config, SubjectList) - -for key in config['MODALITY'].keys(): - SubjectList[key+'_Path'] = "" -QuerySubjectInfo(config, SubjectList, session) -roi_series = [] - -path = config['DATA']['NiiFolder'] -sPatient = SubjectList -r1 = [x.lower() for x in roi_series] - -for i in range(0, len(SubjectList), 1): - print(i) - subjectid = sPatient.loc[i, 'subjectid'] - subject_label = sPatient.loc[i,'subject_label'] - CTPath = SubjectList[SubjectList.subjectid == subjectid]['CT_Path'] - CTArray, meta = LoadImage()(CTPath) - ct = EnsureChannelFirst()(CTArray) - ct = Spacing(pixdim=(1, 1, 3))(ct) - - names_generator = itk.GDCMSeriesFileNames.New() - names_generator.SetUseSeriesDetails(True) - names_generator.AddSeriesRestriction("0008|0021") # Series Date - names_generator.SetDirectory(list(CTPath)[0]) - series_uid = names_generator.GetSeriesUIDs() - if len(series_uid) > 1: - print(subject_label) - else: - ct_array = ct.array.squeeze() - # First define the ROI based on target - RSPath = SubjectList[SubjectList.subjectid == subjectid]['Structs_Path'] - # FixRTSS(list(RSPath)[0], list(CTPath)[0]) - try: - RS = RTStructBuilder.create_from(dicom_series_path=list(CTPath)[0], rt_struct_path=list(RSPath)[0]) - roi_names = RS.get_roi_names() - strList = [x.lower() for x in roi_names] - roi_series.append(strList) - except: - print('RS error: pid', subject_label) - -letter_counts = Counter(itertools.chain.from_iterable(roi_series)) -print(letter_counts) diff --git a/Utils/CropMask.py b/Utils/CropMask.py deleted file mode 100644 index ecd4150..0000000 --- a/Utils/CropMask.py +++ /dev/null @@ -1,32 +0,0 @@ -import nibabel as nib -import sys, glob -import matplotlib.pyplot as plt -import numpy as np -import pandas as pd -from scipy import ndimage -from pathlib import Path - -path = '/home/dgs1/data/InnerEye/nii_root_folder' -data = pd.read_csv('/home/dgs1/data/InnerEye/CSV/dataset_lung_spinal.csv') -patient_id = data['subject'].unique() - -crop_oar = set(data['channel'].unique()).difference(set(['lung_left','lung_right','ct'])) - -for i in range(len(patient_id)): - pat = data[data.subject == i] - - mask_nii = pat[pat.channel == 'lung_left']['filePath'] - info = nib.load(Path(path, list(mask_nii)[0])) - temp = info.get_fdata() - - mask_nii = pat[pat.channel == 'lung_right']['filePath'] - info = nib.load(Path(path, list(mask_nii)[0])) - temp = temp + info.get_fdata() - - Zm = np.sum(temp, axis=(0,1)) - index = np.argwhere(Zm == 0).squeeze() - for oar in crop_oar: - mask_nii = pat[pat.channel == oar]['filePath'] - info = nib.load(Path(path, list(mask_nii)[0])) - img = info.get_fdata() - img[:, :, index] = np.nan diff --git a/Utils/DICOM2NIFTY.py b/Utils/DICOM2NIFTY.py deleted file mode 100644 index bcfb5b7..0000000 --- a/Utils/DICOM2NIFTY.py +++ /dev/null @@ -1,66 +0,0 @@ -import torch -import sys, os -torch.cuda.empty_cache() -from DataGenerator.DataGenerator import * -import toml -from pathlib import Path -import nibabel as nib -from monai.transforms import Spacing, LoadImage, EnsureChannelFirst -from dcmrtstruct2nii import dcmrtstruct2nii, list_rt_structs -config = toml.load(sys.argv[1]) -## First Connect to XNAT -session = xnat.connect(config['SERVER']['Address'], user=config['SERVER']['User'],password=config['SERVER']['Password']) - - -SubjectList = QuerySubjectList(config, session) -SynchronizeData(config, SubjectList) -## GeneratePath -for key in config['MODALITY'].keys(): - SubjectList[key+'_Path'] = "" -QuerySubjectInfo(config, SubjectList, session) -print(SubjectList) - -save_path = '/home/dgs1/data/Nifty_Data/UCLH/' - -for i in range(253, len(SubjectList)): - sub = SubjectList.iloc[i] - patient_label = sub['subject_label'] - out_folder = Path(save_path, patient_label) - RSPath = sub['Structs_Path'] - CTPath = sub['CT_Path'] - DosePath = sub['Dose_Path'] - # command = f'plastimatch convert --input {CTPath} --output-img {out_folder}/CT.nii.gz --spacing "1 1 3"' - # os.system(command) - # DoseName = 'Dose' - # command = f'plastimatch convert --input-dose-img {DosePath} --referenced-ct {CTPath} --resize-dose --output-dose-img {out_folder}/{DoseName}.nii.gz --fixed {out_folder}/CT.nii.gz' - # os.system(command) - - # command = f'plastimatch convert --input {RSPath} --fixed {out_folder}/CT.nii.gz --output-prefix {out_folder}/struct_DICOM --referenced-ct {CTPath} --prefix-format nii.gz' - # os.system(command) - # - # - Struct_folder = Path(out_folder, 'struct_DICOM') - print('before ', i, patient_label) - if not os.path.exists(Struct_folder): - os.system(f'mkdir {Struct_folder}') - # dcmrtstruct2nii(RSPath, CTPath, Path(out_folder, 'struct_DICOM')) - CTArray, meta = LoadImage()(CTPath) - RS = RTStructBuilder.create_from(dicom_series_path=CTPath, rt_struct_path=RSPath) - # %% - roi_names = RS.get_roi_names() - - for j in range(len(roi_names)): - mask_img = RS.get_roi_mask_by_name(roi_names[j]) - mask_img = np.rot90(mask_img) - mask_img = np.flip(mask_img, 0) - mask = MetaTensor(mask_img.copy(), meta=meta) - mask = EnsureChannelFirst()(mask) - mask = Spacing(pixdim=(1, 1, 3))(mask) - mask_array = mask.array.squeeze() - ni_mask = nib.Nifti1Image(mask_array.astype('int'), affine=mask.affine, dtype='uint8') - nib.save(ni_mask, Path(out_folder, 'struct_DICOM', roi_names[j]+'.nii.gz')) - - print(i) - - - diff --git a/Utils/DoseNii.py b/Utils/DoseNii.py deleted file mode 100644 index b417b01..0000000 --- a/Utils/DoseNii.py +++ /dev/null @@ -1,71 +0,0 @@ -import sys -import toml -import glob -import nibabel as nib -import numpy as np -from Utils.DicomTools import * -from pathlib import Path -from DataGenerator.DataGenerator import QuerySubjectList, SynchronizeData, QuerySubjectInfo -from Utils.DicomTools import * -import os -from rt_utils import RTStructBuilder -import xnat -from Utils.FixRTSS import * -session = xnat.connect('http://128.16.11.124:8080/xnat', user='admin', password='mortavar1977') -import csv -from monai.transforms import Spacing, LoadImage, EnsureChannelFirst, ResampleToMatch -from monai.data import MetaTensor -import itk -config = toml.load(sys.argv[1]) - -session = xnat.connect(config['SERVER']['Address'], user=config['SERVER']['User'],password=config['SERVER']['Password']) -SubjectList = QuerySubjectList(config, session) -print(SubjectList) -SynchronizeData(config, SubjectList) - -for key in config['MODALITY'].keys(): - SubjectList[key+'_Path'] = "" -QuerySubjectInfo(config, SubjectList, session) - -path = config['DATA']['NiiFolder'] -sPatient = SubjectList - -for i in range(301,len(sPatient), 1): - print(i) - subjectid = sPatient.loc[i, 'subjectid'] - subject_label = sPatient.loc[i,'subject_label'] - - CTPath = SubjectList.loc[i, 'CT_Path'] - CTArray, meta = LoadImage()(CTPath) - DosePath = SubjectList.loc[i,'Dose_Path'] - DoseArray, dmeta = LoadImage()(DosePath) - DoseArray = DoseArray * np.double(dmeta['3004|000e']) - - ct = EnsureChannelFirst()(CTArray) - ct = Spacing(pixdim=(1, 1, 3))(ct) - DoseArray = EnsureChannelFirst()(DoseArray) - dose = ResampleToMatch()(DoseArray, ct) - - names_generator = itk.GDCMSeriesFileNames.New() - names_generator.SetUseSeriesDetails(True) - names_generator.AddSeriesRestriction("0008|0021") # Series Date - names_generator.SetDirectory(list(CTPath)[0]) - series_uid = names_generator.GetSeriesUIDs() - - if len(series_uid) > 1: - print(series_uid) - else: - ct_array = ct.array.squeeze() - ni_ct = nib.Nifti1Image(np.double(ct_array), affine=ct.affine) - dose_array = dose.array.squeeze() - ni_dose = nib.Nifti1Image(np.double(dose_array), affine=dose.affine) - spath = Path(path, subject_label) - print(subject_label) - - if not os.path.isdir(spath): - os.mkdir(spath) - nib.save(ni_ct, Path(spath, 'ct.nii.gz')) - nib.save(ni_dose, Path(spath, 'dose.nii.gz')) - - - diff --git a/Utils/FMaskDcm.py b/Utils/FMaskDcm.py deleted file mode 100644 index 058281d..0000000 --- a/Utils/FMaskDcm.py +++ /dev/null @@ -1,75 +0,0 @@ -import sys -import os -import csv -from pathlib import Path -from monai.transforms import LoadImage -import nibabel as nib -import numpy as np - -base_path = '/home/dgs1/data/InnerEye/nii_root_folder/RTOG/' - -subjects = os.listdir(base_path) -# roi = ['heart', 'oesophagus', 'spinal canal', 'prox bronch tree', 'proximal trachea', 'cwall & ribs', 'lt lung','rt lung'] -# roi_name = ['heart', 'oesophagus', 'spinal_canal','prox_bronch_tree', 'proximal_trachea','cwall_ribs','lt_lung','rt_lung'] -# roi = ['lung_cntr','lung_ipsi'] -# roi_name = ['lung_cntr','lung_ipsi'] -roi = ['esophagus', 'lung', 'heart', 'ptv'] -header = ['subject', 'filePath', 'channel'] -count = 0 -rm_sub = [] - - -# for i in range(len(subjects)): -# sub = subjects[i] -# subpath = Path(base_path, sub) -# basename = os.listdir(subpath) -# if os.path.exists(Path(subpath, 'lung_left.nii.gz')) and os.path.exists(Path(subpath, 'lung_right.nii.gz')): -# LungL, meta = LoadImage()(Path(subpath, 'lung_left.nii.gz')) -# LungR, meta = LoadImage()(Path(subpath, 'lung_right.nii.gz')) -# Lung = LungL + LungR -# ni_mask = nib.Nifti1Image(Lung.astype('int'), affine=meta['affine'], dtype='uint8') -# nib.save(ni_mask, Path(subpath, 'lung.nii.gz')) -# else: -# rm_sub.append(sub) -# print(rm_sub) - -def BitSet(n, p, b): - p = p.astype(int) - n = n.astype(int) - b = b.astype(int) - mask = 1 << p - bm = b << p - return (n & ~mask) | bm - - -for i in range(len(subjects)): - sub = subjects[i] - subpath = Path(base_path, sub) - basename = os.listdir(subpath) - ct_name = ['ct.nii.gz', 'dose.nii.gz'] - oars = set(basename).difference(set(ct_name)) - oar_list = [] - - for oar in oars: - oar_name = oar.split(".") - oar_list.append(oar_name[0].lower()) - - if set(roi).issubset(set(oar_list)): - count = count + 1 - for idx, r in enumerate(roi): - try: - data, meta = LoadImage()(Path(subpath, r+'.nii.gz')) - if idx == 0: - masks_img = np.zeros_like(data) - except: - raise ValueError(sub + " has no ROI of name " + roi + " found in RTStruct") - masks_img = BitSet(masks_img, idx * np.ones_like(masks_img), data) - - ni_img = nib.Nifti1Image(np.double(masks_img), affine=meta['affine']) - nib.save(ni_img, Path(subpath, 'masks.nii.gz')) - else: - rm_sub.append(sub) - - -print('Counts: ', count) -print(rm_sub) \ No newline at end of file diff --git a/Utils/FormMasks.py b/Utils/FormMasks.py deleted file mode 100644 index 71f5c43..0000000 --- a/Utils/FormMasks.py +++ /dev/null @@ -1,70 +0,0 @@ -import torch -import torchvision -from torch import nn -from pytorch_lightning.callbacks import ModelCheckpoint, EarlyStopping -from pytorch_lightning.strategies import DDPStrategy -from pytorch_lightning import LightningDataModule, LightningModule, Trainer, seed_everything -import sys, os -#import torchio as tio -import monai -torch.cuda.empty_cache() -## Module - Dataloaders -from DataGenerator.DataGenerator import * -from Models.Classifier import Classifier -from Models.Linear import Linear -from Models.MixModel import MixModel -from monai.transforms import EnsureChannelFirstd, ScaleIntensityd, ResampleToMatchd -## Main -from sklearn.preprocessing import StandardScaler, OneHotEncoder -import toml -from Utils.GenerateSmoothLabel import get_smoothed_label_distribution, get_module -from Utils.PredictionReports import PredictionReports -from pathlib import Path -#import torchio as tio -from torchmetrics import ConfusionMatrix -import torchmetrics -import nibabel as nib - -config = toml.load(sys.argv[1]) -## First Connect to XNAT -session = xnat.connect(config['SERVER']['Address'], user=config['SERVER']['User'],password=config['SERVER']['Password']) - - -SubjectList = QuerySubjectList(config, session) -SynchronizeData(config, SubjectList) -## GeneratePath -for key in config['MODALITY'].keys(): - SubjectList[key+'_Path'] = "" -QuerySubjectInfo(config, SubjectList, session) - -path = '/home/dgs1/data/Nifty_Data/RTOG0617/' - -for i in range(len(SubjectList)): - print(i) - sub = SubjectList.iloc[i] - print(sub) - patient_label = sub['subject_label'] - RSPath = sub['Structs_Path'] - for idx, roi in enumerate(config['DATA']['Structs']): - try: - data, meta = LoadImage()(Path(RSPath, roi + '.nii.gz')) - print(data.shape) - if idx == 0: - masks_img = np.zeros_like(data) - except: - raise ValueError(patient_label + " has no ROI of name " + roi + " found in RTStruct") - masks_img = BitSet(masks_img, idx * np.ones_like(masks_img), data) - - ni_img = nib.Nifti1Image(np.double(masks_img), affine=meta['affine']) - spath = Path(path, patient_label, 'struct_TS') - nib.save(ni_img, Path(spath, 'maskswc.nii.gz')) - - - -def BitSet(n, p, b): - p = p.astype(int) - n = n.astype(int) - b = b.astype(int) - mask = 1 << p - bm = b << p - return (n & ~mask) | bm diff --git a/Utils/FormMasksLocal.py b/Utils/FormMasksLocal.py deleted file mode 100644 index 656e01a..0000000 --- a/Utils/FormMasksLocal.py +++ /dev/null @@ -1,58 +0,0 @@ -import torch -import torchvision -from torch import nn -from pytorch_lightning.callbacks import ModelCheckpoint, EarlyStopping -from pytorch_lightning.strategies import DDPStrategy -from pytorch_lightning import LightningDataModule, LightningModule, Trainer, seed_everything -import sys, os -#import torchio as tio -import monai -torch.cuda.empty_cache() -## Module - Dataloaders -from DataGenerator.DataGenerator import * -from Models.Classifier import Classifier -from Models.Linear import Linear -from Models.MixModel import MixModel -from monai.transforms import EnsureChannelFirstd, ScaleIntensityd, ResampleToMatchd -## Main -from sklearn.preprocessing import StandardScaler, OneHotEncoder -import toml -from Utils.GenerateSmoothLabel import get_smoothed_label_distribution, get_module -from Utils.PredictionReports import PredictionReports -from pathlib import Path -#import torchio as tio -from torchmetrics import ConfusionMatrix -import torchmetrics -import nibabel as nib - -config = toml.load(sys.argv[1]) -## First Connect to XNAT -path = '/home/dgs1/data/Nifty_Data/RTOG0617/test' -subjects = os.listdir(path) -for i in range(0, len(subjects)): - patient_label = subjects[i] - print(i) - print(patient_label) - RSPath = Path(path, patient_label, 'struct_TS') - for idx, roi in enumerate(config['DATA']['Structs']): - try: - data, meta = LoadImage()(Path(RSPath, roi + '.nii.gz')) - #print(data.shape) - if idx == 0: - masks_img = np.zeros_like(data) - except: - raise ValueError(patient_label + " has no ROI of name " + roi + " found in RTStruct") - masks_img = BitSet(masks_img, idx * np.ones_like(masks_img), data) - - ni_img = nib.Nifti1Image(np.double(masks_img), affine=meta['affine']) - nib.save(ni_img, Path(RSPath, 'maskswc.nii.gz')) - - - -def BitSet(n, p, b): - p = p.astype(int) - n = n.astype(int) - b = b.astype(int) - mask = 1 << p - bm = b << p - return (n & ~mask) | bm diff --git a/Utils/PTV.py b/Utils/PTV.py deleted file mode 100644 index c2a51e3..0000000 --- a/Utils/PTV.py +++ /dev/null @@ -1,103 +0,0 @@ -import sys -#sys.path.insert(0, '/home/dgs1/Software/OutcomePrediction/') -import toml -import glob -import nibabel as nib -import numpy as np -from Utils.DicomTools import * -from pathlib import Path -from DataGenerator.DataGenerator import QuerySubjectList, SynchronizeData, QuerySubjectInfo -from Utils.DicomTools import * -import os -from rt_utils import RTStructBuilder -import xnat -from Utils.FixRTSS import * -session = xnat.connect('http://128.16.11.124:8080/xnat', user='admin', password='mortavar1977') -import csv -from monai.transforms import Spacing, LoadImage, EnsureChannelFirst -from monai.data import MetaTensor -import itk -config = toml.load(sys.argv[1]) -from scipy import ndimage -session = xnat.connect(config['SERVER']['Address'], user=config['SERVER']['User'],password=config['SERVER']['Password']) -SubjectList = QuerySubjectList(config, session) -print(SubjectList) -SynchronizeData(config, SubjectList) - -for key in config['MODALITY'].keys(): - SubjectList[key+'_Path'] = "" -QuerySubjectInfo(config, SubjectList, session) -# -# roi_series = ['Heart', 'Oesophagus', 'Spinal Canal', 'Prox bronch tree', 'Proximal trachea', 'Cwall & ribs', 'L Lung', -# 'R Lung', 'Brachial Plexus'] -# roi_name = ['HEART', 'ESOPHAGUS', 'SPINAL_CORD', 'PROX_BRONCH_TREE', 'PROXIMAL_TRACHEA', 'CWALL_RIBS', 'LUNG_LEFT', -# 'LUNG_RIGHT', 'BRACHIAL_PLEXUS'] -# roi_series = ['Heart', 'Esophagus', 'Lung_L', 'Lung_R','SpinalCord'] -# roi_name = ['HEART', 'ESOPHAGUS', 'LUNG_LEFT','LUNG_RIGHT', 'SPINAL_CORD'] - -# roi_series = ['HEART', 'PTV', 'LUNG_IPSI', 'LUNG_CNTR', 'SPINAL_CORD', 'ESOPHAGUS'] -# roi_name = ['HEART', 'PTV', 'LUNG_IPSI', 'LUNG_CNTR', 'SPINAL_CORD', 'ESOPHAGUS'] -roi_name = 'ptv' - -path = config['DATA']['NiiFolder'] -sPatient = SubjectList - -for i in range(0, len(SubjectList), 1): - subjectid = sPatient.loc[i, 'subjectid'] - subject_label = sPatient.loc[i,'subject_label'] - CTPath = SubjectList[SubjectList.subjectid == subjectid]['CT_Path'] - CTArray, meta = LoadImage()(CTPath) - ct = EnsureChannelFirst()(CTArray) - ct = Spacing(pixdim=(1, 1, 3))(ct) - - names_generator = itk.GDCMSeriesFileNames.New() - names_generator.SetUseSeriesDetails(True) - names_generator.AddSeriesRestriction("0008|0021") # Series Date - names_generator.SetDirectory(list(CTPath)[0]) - series_uid = names_generator.GetSeriesUIDs() - print('No.{}:'.format(i) + str(subject_label)) - - if len(series_uid) > 1: - print(series_uid) - else: - ct_array = ct.array.squeeze() - # First define the ROI based on target - RSPath = SubjectList[SubjectList.subjectid == subjectid]['Structs_Path'] - # FixRTSS(RSPath[0], list(CTPath)[0]) - RS = RTStructBuilder.create_from(dicom_series_path=list(CTPath)[0], rt_struct_path=list(RSPath)[0]) - #%% - roi_names = RS.get_roi_names() - strList = [x.lower() for x in roi_names] - ni_img = nib.Nifti1Image(np.double(ct_array), affine=ct.affine) - spath = Path(path, subject_label) - - if not os.path.isdir(spath): - os.mkdir(spath) - nib.save(ni_img, Path(spath, 'ct.nii.gz')) - - # r1 = [r for r in strList if roi_name in r] - - # mask = np.zeros_like(CTArray) - - # for j in range(len(r1)): - # roi = r1[j] - # index = strList.index(roi.lower()) - # mask_img = RS.get_roi_mask_by_name(roi_names[index]) - # mask_img = np.rot90(mask_img) - # mask_img = np.flip(mask_img, 0) - # mask = mask + mask_img - - index = strList.index(roi_name.lower()) - mask_img = RS.get_roi_mask_by_name(roi_names[index]) - mask_img = np.rot90(mask_img) - mask_img = np.flip(mask_img, 0) - - mask = MetaTensor(mask_img.copy(), meta=meta) - mask = EnsureChannelFirst()(mask) - mask = Spacing(pixdim=(1, 1, 3))(mask) - mask_array = mask.array.squeeze() - ni_mask = nib.Nifti1Image(mask_array.astype('int'), affine=mask.affine, dtype='uint8') - nib.save(ni_mask, Path(spath, 'AI_target.nii.gz')) - - - diff --git a/Utils/RenameDicomPatientID.py b/Utils/RenameDicomPatientID.py deleted file mode 100644 index 083edd2..0000000 --- a/Utils/RenameDicomPatientID.py +++ /dev/null @@ -1,48 +0,0 @@ -import os -from pydicom import dcmread -from pathlib import Path -import shutil - -path = '/home/dgs1/data/IDEAL/IDEAL/' -subjects = os.listdir(path) -for i, sub in enumerate(subjects): - print(i) - subpath = Path(path, sub) - if not str(subpath).endswith('.DS_Store'): - plan_dir = Path(subpath, 'planning_data') - ## - sub_plan_dir = os.listdir(plan_dir) - - # if len(sub_plan_dir) < 8: - # sub_plan_dir_planning = Path(plan_dir, 'planning') - # ddf = os.listdir(sub_plan_dir_planning) - # df = dcmread(Path(sub_plan_dir_planning, ddf[0]), force=True) - # sid_st = df.StudyInstanceUID - # - # for sf in set(sub_plan_dir).difference(set(['planning','.DS_Store'])): - # splan_dir = Path(plan_dir, sf) - # ddf = os.listdir(splan_dir) - # filename = Path(splan_dir,ddf[0]) - # df = dcmread(filename, force=True) - # df.StudyInstanceUID = sid_st - # df.save_as(filename) - # - # for dirpath, dirs, files in os.walk(subpath): - # for filename in files: - # fname = os.path.join(dirpath, filename) - # if not fname.endswith('.DS_Store'): - # ds = dcmread(fname, force=True) - # ds.PatientID = sub - # ds.PatientName = sub - # # print(ds.PatientID) - # ds.save_as(fname) - - - # if not str(subpath).endswith('.DS_Store'): - # rmdir = os.listdir(subpath) - # plan_dir = Path(subpath, 'planning_data') - # - # for rd in set(rmdir).difference(set(['planning_data'])): - # if not rd.endswith('.DS_Store'): - # shutil.rmtree(Path(subpath,rd)) - # print(rd) diff --git a/Utils/SegCT.py b/Utils/SegCT.py deleted file mode 100644 index 7449597..0000000 --- a/Utils/SegCT.py +++ /dev/null @@ -1,24 +0,0 @@ -import os -from pydicom import dcmread -from pathlib import Path -import shutil -from dcmrtstruct2nii import dcmrtstruct2nii, list_rt_structs - -path = '/home/dgs1/data/IDEAL/IDEAL/' -subjects = os.listdir(path) -for i, sub in enumerate(subjects): - print(i) - subpath = Path(path, sub) - if not str(subpath).endswith('.DS_Store'): - plan_dir = Path(subpath, 'planning_data') - ## - sub_plan_dir = os.listdir(plan_dir) - - if len(sub_plan_dir) < 8: - sub_plan_dir_planning = Path(plan_dir, 'planning') - ddf = os.listdir(sub_plan_dir_planning) - - sub_structs_dir_planning = Path(plan_dir, 'structures') - struct_dcm = os.listdir(sub_structs_dir_planning)[0] - - diff --git a/Utils/check.py b/Utils/check.py deleted file mode 100644 index 2e784f3..0000000 --- a/Utils/check.py +++ /dev/null @@ -1,54 +0,0 @@ -import toml -import sys -import glob -import nibabel as nib -import numpy as np -from Utils.DicomTools import * -from pathlib import Path -from DataGenerator.DataGenerator import QuerySubjectList, SynchronizeData, QuerySubjectInfo -import os -from rt_utils import RTStructBuilder -import xnat -from monai.transforms import LoadImage, ResampleToMatchd, EnsureChannelFirstd -from monai.data.image_writer import ITKWriter - -session = xnat.connect('http://128.16.11.124:8080/xnat', user='admin', password='mortavar1977') -config = toml.load(sys.argv[1]) -from pydicom import dcmread - -session = xnat.connect(config['SERVER']['Address'], user=config['SERVER']['User'], - password=config['SERVER']['Password']) -SubjectList = QuerySubjectList(config, session) -print(SubjectList) -SynchronizeData(config, SubjectList) -QuerySubjectInfo(config, SubjectList, session) -for i in range(0,len(SubjectList),1): - print(i) - CTPath = SubjectList['CT_Path'][i].split('/') - scanPath = '/'.join(CTPath[0:8]) - Dosefile = glob.glob(scanPath + '/*-RTDOSE') - data = {} - meta = {} - if len(Dosefile) > 1: - for j in range(len(Dosefile)): - file = glob.glob(Dosefile[j] + '/**/*.dcm', recursive=True) - if j == 0: - info = dcmread(file[0]) - data['Dose'+ str(j)], meta['Dose'+str(j)] = LoadImage()(file[0]) - - data = EnsureChannelFirstd(data.keys())(data) - data = ResampleToMatchd(list(set(data.keys()).difference(set(['Dose0']))), key_dst='Dose0')(data) - - total_array = np.zeros_like(data['Dose0']) - for key in data.keys(): - total_array = total_array + data[key]*np.float64(meta[key]['3004|000e'])/np.float64(meta['Dose0']['3004|000e']) - - total_array = total_array.squeeze() - total_array = np.transpose(total_array, (2, 1, 0)) - total_array = np.uint32(total_array) - - info.PixelData = total_array.tobytes() - if not os.path.isdir(Path('/home/dgs1/data/dose/', CTPath[5])): - os.mkdir(Path('/home/dgs1/data/dose/', CTPath[5])) - dpath = Path('/home/dgs1/data/dose/', CTPath[5], '1-1.dcm') - info.save_as(dpath) diff --git a/Utils/remove/RTOG.py b/Utils/remove/RTOG.py deleted file mode 100644 index 9cbeea5..0000000 --- a/Utils/remove/RTOG.py +++ /dev/null @@ -1,54 +0,0 @@ -import sys -#sys.path.insert(0, '/home/dgs1/Software/OutcomePrediction/') -import toml -import glob -import nibabel as nib -import numpy as np -from Utils.DicomTools import * -from pathlib import Path -from DataGenerator.DataGenerator import QuerySubjectList, SynchronizeData, QuerySubjectInfo -from Utils.DicomTools import * -import os -from rt_utils import RTStructBuilder -import xnat -from Utils.FixRTSS import * -session = xnat.connect('http://128.16.11.124:8080/xnat', user='admin', password='mortavar1977') -import csv -from monai.transforms import Spacing, LoadImage, EnsureChannelFirst -from monai.data import MetaTensor -import itk -import pandas as pd -from pydicom import dcmread -config = toml.load(sys.argv[1]) - -session = xnat.connect(config['SERVER']['Address'], user=config['SERVER']['User'],password=config['SERVER']['Password']) -SubjectList = QuerySubjectList(config, session) -print(SubjectList) -SynchronizeData(config, SubjectList) -data = pd.read_excel('/home/dgs1/Downloads/LUNG_EXCEL.xlsx') - -for key in config['MODALITY'].keys(): - SubjectList[key+'_Path'] = "" -QuerySubjectInfo(config, SubjectList, session) -sPatient = SubjectList - -roi_set = set(['LEFT','RIGHT']) - -for i in range(0, len(SubjectList), 1): - subjectid = sPatient.loc[i, 'subjectid'] - subject_label = sPatient.loc[i,'subject_label'] - print(subject_label) - RSPath = glob.glob(list(SubjectList[SubjectList.subjectid == subjectid]['Structs_Path'])[0] + '/*dcm') - info = dcmread(RSPath[0]) - ind = list(data[data['Patient ID'] == subject_label]['LUNG_CNTR'])[0] - value = list(data[data['Patient ID'] == subject_label]['LUNG_IPSI']) - if not (ind == 'NO TUMOUR?'): - for r in range(len(info.StructureSetROISequence)): - if info.StructureSetROISequence[r].ROIName == 'LUNG_IPSI': - info.StructureSetROISequence[r].ROIName = 'LUNG_' + value[0] - if info.StructureSetROISequence[r].ROIName == 'LUNG_CNTR': - info.StructureSetROISequence[r].ROIName = 'LUNG_' + list(roi_set.difference(set(value)))[0] - info.save_as(RSPath[0]) - - - diff --git a/Utils/remove/RTOG_nii.py b/Utils/remove/RTOG_nii.py deleted file mode 100644 index d6d2ba1..0000000 --- a/Utils/remove/RTOG_nii.py +++ /dev/null @@ -1,38 +0,0 @@ -import nibabel as nib -import sys, glob -import matplotlib.pyplot as plt -import numpy as np -import pandas as pd -from scipy import ndimage -from pathlib import Path -import os - -path = '/home/dgs1/data/InnerEye/nii_root_folder/RTOG/' -data = pd.read_csv('/home/dgs1/data/InnerEye/nii_root_folder/RTOG/dataset.csv') -patient_id = data['subject'].unique() -cdata = pd.read_excel('/home/dgs1/Downloads/LUNG_EXCEL.xlsx') -roi_set = set(['LEFT','RIGHT']) -for i in range(142, len(patient_id), 1): - pat = data[data.subject == i] - mask_nii = pat[pat.channel == 'lung_ipsi']['filePath'] - id = list(mask_nii)[0].split('/')[0] - ind = list(cdata[cdata['Patient ID'] == id]['LUNG_IPSI'])[0] - value = list(cdata[cdata['Patient ID'] == id]['LUNG_CNTR']) - ipsi_name = 'lung_' + list(roi_set.difference(set(value)))[0].lower() + '.nii.gz' - - old = Path(path, list(mask_nii)[0]) - new = Path(path, id, ipsi_name) - if not os.path.exists(new): - os.rename(old, new) - - mask_nii = pat[pat.channel == 'lung_cntr']['filePath'] - cntr_name = 'lung_' + value[0].lower() + '.nii.gz' - - old = Path(path, list(mask_nii)[0]) - new = Path(path, id, cntr_name) - if not os.path.exists(new): - os.rename(old, new) - - - - diff --git a/Utils/renameCT.py b/Utils/renameCT.py deleted file mode 100644 index 460d55c..0000000 --- a/Utils/renameCT.py +++ /dev/null @@ -1,99 +0,0 @@ -import sys -#sys.path.insert(0, '/home/dgs1/Software/OutcomePrediction/') -import toml -import glob -import nibabel as nib -import numpy as np -from Utils.DicomTools import * -from pathlib import Path -from DataGenerator.DataGenerator import QuerySubjectList, SynchronizeData, QuerySubjectInfo -from Utils.DicomTools import * -import os -from rt_utils import RTStructBuilder -import xnat -from Utils.FixRTSS import * -session = xnat.connect('http://128.16.11.124:8080/xnat', user='admin', password='mortavar1977') -import csv -from monai.transforms import Spacing, LoadImage, EnsureChannelFirst -from monai.data import MetaTensor -import itk -config = toml.load(sys.argv[1]) - -session = xnat.connect(config['SERVER']['Address'], user=config['SERVER']['User'],password=config['SERVER']['Password']) -SubjectList = QuerySubjectList(config, session) -print(SubjectList) -SynchronizeData(config, SubjectList) - -for key in config['MODALITY'].keys(): - SubjectList[key+'_Path'] = "" -QuerySubjectInfo(config, SubjectList, session) -# -# roi_series = ['Heart', 'Oesophagus', 'Spinal Canal', 'Prox bronch tree', 'Proximal trachea', 'Cwall & ribs', 'L Lung', -# 'R Lung', 'Brachial Plexus'] -# roi_name = ['HEART', 'ESOPHAGUS', 'SPINAL_CORD', 'PROX_BRONCH_TREE', 'PROXIMAL_TRACHEA', 'CWALL_RIBS', 'LUNG_LEFT', -# 'LUNG_RIGHT', 'BRACHIAL_PLEXUS'] -# roi_series = ['Heart', 'Esophagus', 'Lung_L', 'Lung_R','SpinalCord'] -# roi_name = ['HEART', 'ESOPHAGUS', 'LUNG_LEFT','LUNG_RIGHT', 'SPINAL_CORD'] - -#roi_series = ['HEART', 'PTV', 'LUNG_LEFT', 'LUNG_RIGHT', 'SPINAL_CORD', 'ESOPHAGUS'] -#roi_name = ['HEART', 'PTV', 'LUNG_LEFT', 'LUNG_RIGHT', 'SPINAL_CORD', 'ESOPHAGUS'] -roi_series = ['PTV'] -roi_name = ['PTV'] - -path = config['DATA']['NiiFolder'] -sPatient = SubjectList -r1 = [x.lower() for x in roi_series] - -for i in range(1, len(SubjectList), 1): - subjectid = sPatient.loc[i, 'subjectid'] - subject_label = sPatient.loc[i,'subject_label'] - CTPath = SubjectList[SubjectList.subjectid == subjectid]['CT_Path'] - CTArray, meta = LoadImage()(CTPath) - ct = EnsureChannelFirst()(CTArray) - ct = Spacing(pixdim=(1, 1, 3))(ct) - - names_generator = itk.GDCMSeriesFileNames.New() - names_generator.SetUseSeriesDetails(True) - names_generator.AddSeriesRestriction("0008|0021") # Series Date - names_generator.SetDirectory(list(CTPath)[0]) - series_uid = names_generator.GetSeriesUIDs() - if len(series_uid) > 1: - print(i) - else: - ct_array = ct.array.squeeze() - # First define the ROI based on target - RSPath = SubjectList[SubjectList.subjectid == subjectid]['Structs_Path'] - try: - RS = RTStructBuilder.create_from(dicom_series_path=list(CTPath)[0], rt_struct_path=list(RSPath)[0]) - #%% - roi_names = RS.get_roi_names() - strList = [x.lower() for x in roi_names] - ni_img = nib.Nifti1Image(np.double(ct_array), affine=ct.affine) - spath = Path(path, subject_label) - - if not os.path.isdir(spath): - os.mkdir(spath) - nib.save(ni_img, Path(spath, 'ct.nii.gz')) - - for i in range(len(r1)): - roi = r1[i] - if roi in strList: - #print(roi) - index = strList.index(roi.lower()) - mask_img = RS.get_roi_mask_by_name(roi_names[index]) - mask_img = np.rot90(mask_img) - mask_img = np.flip(mask_img, 0) - mask = MetaTensor(mask_img.copy(), meta=meta) - mask = EnsureChannelFirst()(mask) - mask = Spacing(pixdim=(1, 1, 3))(mask) - mask_array = mask.array.squeeze() - ni_mask = nib.Nifti1Image(mask_array.astype('int'), affine=mask.affine, dtype='uint8') - nib.save(ni_mask, Path(spath, roi_name[i].lower() + '.nii.gz')) - else: - print(subject_label) - except: - #print('No.{}:'.format(i)) - continue - - - diff --git a/Utils/sub_dose_analysis_total.py b/Utils/sub_dose_analysis_total.py deleted file mode 100644 index cb844c8..0000000 --- a/Utils/sub_dose_analysis_total.py +++ /dev/null @@ -1,88 +0,0 @@ -import sys -sys.path.insert(0, '/home/dgs1/Software/OutcomePrediction/') -import csv -import glob -import os -from pathlib import Path -import nibabel as nib -import numpy as np -import pandas -from scipy import ndimage -import toml -from DataGenerator.DataGenerator import QuerySubjectList, SynchronizeData, QuerySubjectInfo -from Utils.DicomTools import * -import xnat - -session = xnat.connect('http://128.16.11.124:8080/xnat', user='admin', password='mortavar1977') -config = toml.load(sys.argv[1]) -session = xnat.connect(config['SERVER']['Address'], user=config['SERVER']['User'], - password=config['SERVER']['Password']) -SubjectList = QuerySubjectList(config, session) -print(SubjectList) -SynchronizeData(config, SubjectList) - -if 'Records' in config.keys(): - SubjectList, clinical_cols = LoadClinicalData(config, SubjectList) - -else: - clinical_cols = None - -for key in config['MODALITY'].keys(): - SubjectList[key + '_Path'] = "" -QuerySubjectInfo(config, SubjectList, session) - -path = '/home/dgs1/data/InnerEye/Seg' -header = ['idx', 'subject', 'survival_months', 'ptv', 'heart_atrium_left', 'heart_atrium_right', 'heart_myocardium', - 'heart_ventricle_left', 'heart_ventricle_right', 'pulmonary_artery', 'position', 'position_label'] -idx = 0 -mask_list = ['ptv.nii.gz', 'heart_atrium_left.nii.gz', 'heart_atrium_right.nii.gz', 'heart_myocardium.nii.gz', - 'heart_ventricle_left.nii.gz', 'heart_ventricle_right.nii.gz', 'pulmonary_artery.nii.gz'] - -clinical_cols = ['xnat_subjectdata_field_map_survival_months', 'xnat_subjectdata_field_map_grade3_toxicity'] - -with open('position_dmean.csv', 'w', encoding='UTF8', newline='') as f: - writer = csv.writer(f) - writer.writerow(header) - for patient_folder in glob.glob(path + "/0617-*"): - id = patient_folder.split('/')[-1] - organs = os.listdir(patient_folder) - data = [idx, id] - - col_data = SubjectList.loc[ - SubjectList.subjectid == subjectid, 'xnat_subjectdata_field_map_' + config['DATA']['target']] - data.append(col_data) - - if 'ptv.nii.gz' in organs: - dose_info = nib.load(Path(patient_folder, 'dose.nii.gz')) - dose_img = dose_info.get_fdata() - - for mask in mask_list: - mask_info = nib.load(Path(patient_folder, mask)) - mask_img = mask_info.get_fdata() - dmean = dose_img[mask_img > 0].mean() - data.append(dmean) - - ptv_info = nib.load(Path(patient_folder, 'ptv.nii.gz')) - ptv_img = ptv_info.get_fdata() - gtv_img = ndimage.binary_erosion(ptv_img, structure=np.ones((10, 10, 5))).astype(ptv_img.dtype) - g_slice = gtv_img.sum(axis=(0, 1)) - g_index = [i for i, v in enumerate(g_slice) if v > 0] - - PTV_COM = list(map(int, ndimage.center_of_mass(ptv_img))) - # print('PTV_COM: ', PTV_COM) - T7_info = nib.load(Path(patient_folder, 'vertebrae_T7.nii.gz')) - T7_img = T7_info.get_fdata() - t_slice = T7_img.sum(axis=(0, 1)) - t_index = [i for i, v in enumerate(t_slice) if v > 0] - T7_COM = list(map(int, ndimage.center_of_mass(T7_img))) - pos = PTV_COM[2] - T7_COM[2] - data.append(pos) - intersect = set(g_index).intersection(set(t_index)) - # print('T7_COM:', T7_COM) - if len(intersect) == 0 & pos > 0: - data.append(0) - else: - data.append(1) - writer.writerow(data) - idx = idx + 1 -print(idx) diff --git a/Utils/test.py b/Utils/test.py deleted file mode 100644 index 67bc513..0000000 --- a/Utils/test.py +++ /dev/null @@ -1,52 +0,0 @@ -import sys -#sys.path.insert(0, '/home/dgs1/Software/OutcomePrediction/') -import toml -import glob -import nibabel as nib -import numpy as np -from Utils.DicomTools import * -from pathlib import Path -from DataGenerator.DataGenerator import QuerySubjectList, SynchronizeData, QuerySubjectInfo -from Utils.DicomTools import * -import os -from rt_utils import RTStructBuilder -import xnat -from Utils.FixRTSS import * -session = xnat.connect('http://128.16.11.124:8080/xnat', user='admin', password='mortavar1977') -import csv -from monai.transforms import Spacing, LoadImage, EnsureChannelFirst -from monai.data import MetaTensor -import itk -import pandas as pd -from pydicom import dcmread -config = toml.load('/home/dgs1/Software/OutcomePrediction/SettingsRTOG_Inner2.ini') - -session = xnat.connect(config['SERVER']['Address'], user=config['SERVER']['User'],password=config['SERVER']['Password']) -SubjectList = QuerySubjectList(config, session) -print(SubjectList) -# SynchronizeData(config, SubjectList) - -for key in config['MODALITY'].keys(): - SubjectList[key+'_Path'] = "" -QuerySubjectInfo(config, SubjectList, session) -sPatient = SubjectList - -roi_set = set(['LEFT','RIGHT']) -plist = [] - -for i in range(0, len(SubjectList), 1): - subjectid = sPatient.loc[i, 'subjectid'] - subject_label = sPatient.loc[i,'subject_label'] - # print(subject_label) - RSPath = list(SubjectList[SubjectList.subjectid == subjectid]['Structs_Path'])[0] - pl = RSPath.split('/') - plc = '/'.join(pl[0:8]) - ll = glob.glob(plc+'/*DOSE') - if len(ll) > 1: - plist.append(subject_label) - print(subject_label) - print(ll) - - -print('test') - diff --git a/Utils/xnat_data_uniformization.py b/Utils/xnat_data_uniformization.py deleted file mode 100644 index ee17a60..0000000 --- a/Utils/xnat_data_uniformization.py +++ /dev/null @@ -1,95 +0,0 @@ -from pyxnat import Interface -import pandas as pd -# set up connection with xnat by 'logging in' -interface= Interface(server='http://128.16.11.124:8080/xnat', user='yzhan', password='yzhan') -from pathlib import Path -# display the different projects -list(interface.select.projects()) - -# define the project of interest -pro= interface.select.projects().get() -prj=pro[6] - -#define subjects -subjs= interface.select.projects(prj).subjects().get() -# print(dir(subjs)) -#subjs.remove('XNAT01_S00032') #delete this subject from the 'testnlst' project because it does not have any scans and it sends an error whenever the code reaches it because of that - -## this loop replaces the name of the type of each scan in each experiment for every subject by the name of the modality (which is more consistent) -# run the loop on all the subjects within the chosen project -# df = pd.read_excel('C:\\Users\\clara\\Documents\\patientdata.xlsx') -# id_list = df['id'] - -for i in range (len(subjs)): - subj_i = interface.select.projects(prj).subjects(subjs[i]) - exps = subj_i.experiments().get() - id = subj_i.get(['xnat:subjectData/SUBJECT_ID']) - # item = df.loc[id_list == id[0].label()] - # path = item['path'] - # path = Path(list(path)[0]) - # print(id[0].label()) - #run the loop on all the experiments of each subject - for k in range(len(exps)): - exp_k=exps[k] - scns=interface.select.projects(prj).subjects(subjs[i]).experiments(exp_k).scans() - #run the loop on all the scans present in each experiment - for scn in scns: - mod = scn.attrs.get('modality') - typ=scn.attrs.get('type') - if mod == 'RTSTRUCT': - scn.attrs.set('type', 'Structs') - elif mod == 'RTDOSE': - # di = scn.dicom_dump() - # for x in di: - # if x['tag1'] == '(0008,0060)': - # print(x['value']) - # scn.__setattr__('type', str('')) - scn.attrs.set('type', 'Dose') - else: - scn.attrs.set('type',str(mod)) - # print('test') - - -# import xnat -# import numpy as np -# from pathlib import Path -# import nibabel as nib -# import os -# import pandas as pd -# -# session = xnat.connect('http://128.16.11.124:8080/xnat', user='yzhan', password='yzhan') -# project = session.projects['LUNG_IDEAL'] -# subjectS = project.subjects -# print(subjectS) -# new_path = '/home/dgs1/data/InnerEye/Seg/' -# df = pd.read_excel('/home/dgs1/data/Overall.xlsx') -# id_list = df['TrialNo'] -# for subject in subjectS.values(): -# # subject.fields['analysis_inclusion'] = 1 -# label = subject.label -# No = label[-3:] -# Os = df.loc[id_list == np.uint8(No)]['os_time'] -# try: -# subject.fields['survival_months'] = list(Os)[0] -# except: -# print('error') -# # # path = item['path'] - - # newpath = Path(new_path, label, 'ptv.nii.gz') - # if os.path.exists(newpath): - # ptv_info = nib.load(newpath) - # ptv = ptv_info.get_fdata() - # ptv_volume = numpy.sum(ptv)*3/1000 - # subject.fields['volume_ptv'] = ptv_volume - - # if 'patient_sex' in subject.fields: - # if subject.fields['patient_sex'] == 'Male': - # subject.fields['gender'] = 1 - # else: - # subject.fields['gender'] = 2 - # if 'year_of_birth' in subject.fields: - # scan = subject.experiments[0].scans[0] - # di = scan.dicom_dump() - # for x in di: - # if x['tag1'] == '(0008,0012)': - # subject.fields['age'] = numpy.int16(x['value'][0:4]) - numpy.int16(subject.fields['year_of_birth']) diff --git a/temp.png b/temp.png deleted file mode 100644 index 6676701d9e27fb393102163c183a677592a8d6f9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 43220 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 From 04512084676da7f8b06f3ee6c64d271c381e0e95 Mon Sep 17 00:00:00 2001 From: Clara Date: Fri, 21 Apr 2023 15:11:32 +0100 Subject: [PATCH 10/12] clean others --- .../old_settings/SettingsRTOG_Multi.ini.bak | 59 --------------- .../Classification/tSettingsRTOG_All_PTV.ini~ | 71 ------------------- .../tSettingsRTOG_Dose_Nii.ini~ | 61 ---------------- Configs/Regression/SettingsRTOG_Multi.ini~ | 61 ---------------- DataGenerator/DataGenerator.py | 3 +- Models/MixModel.py | 22 ------ Utils/DicomTools.py | 70 ------------------ 7 files changed, 1 insertion(+), 346 deletions(-) delete mode 100644 Configs/Classification/old_settings/SettingsRTOG_Multi.ini.bak delete mode 100644 Configs/Classification/tSettingsRTOG_All_PTV.ini~ delete mode 100644 Configs/Classification/tSettingsRTOG_Dose_Nii.ini~ delete mode 100644 Configs/Regression/SettingsRTOG_Multi.ini~ diff --git a/Configs/Classification/old_settings/SettingsRTOG_Multi.ini.bak b/Configs/Classification/old_settings/SettingsRTOG_Multi.ini.bak deleted file mode 100644 index 0cc94e0..0000000 --- a/Configs/Classification/old_settings/SettingsRTOG_Multi.ini.bak +++ /dev/null @@ -1,59 +0,0 @@ -[MODEL] -Activation = 'Sigmoid' -Backbone = 'DenseNet' -Records_Backbone = 'Linear' -batch_size = 10 -Prediction_type = 'Classification' -CT_spatial_dims = 3 -Loss_Function = 'BCEWithLogitsLoss' - -[MODEL_PARAMETERS] -spatial_dims = 3 -in_channels = 10 -out_channels = 1 -block_config = [1,2,4,2] -dropout_prob = 0.3 - -[MODALITY] -CT = '1' -Dose = '1' -Structs = '1' - -[DATA] -Nifty = false -n_classes = 1 -Multichannel = true -dim = [128,128,32] -threshold = 24 -Structs = ['esophagus','lung','trachea','heart_atrium_left', 'heart_atrium_right', 'heart_myocardium', - 'heart_ventricle_left', 'heart_ventricle_right', 'pulmonary_artery','ptv'] -DataFolder = '/home/dgs1/data/OutcomePrediction/' -vis = [0] -train_size = 0.7 -val_size = 0.3 -target = 'survival_months' - -[Records] -category_feat = ['arm', 'gender', 'race', 'ethnicity', 'zubrod', - 'histology','ajcc_stage_grp', 'rt_technique', - 'smoke_hx', 'rx_terminated_ae', 'received_conc_chemo','rt_compliance_ptv90', - 'received_cons_chemo','rt_dose','pet_staging','received_rt'] -numerical_feat = ['age', 'volume_ptv'] - - -[CHECKPOINT] -monitor = "val_loss" #"val_acc_epoch" -mode = "max" -matrix = ['ROC', 'Specificity','Sensitivity','Accuracy','Precision'] - - -[SERVER] -Address = 'http://128.16.11.124:8080/xnat' -Projects = ["RTOG_0617"] -User = "yzhan" -Password = "yzhan" -[CRITERIA] -analysis_inclusion = 1 -[FILTER] -patient_id = ['0617-789552', '0617-607275', '0617-568854', '0617-559978', '0617-805653','0617-693977', '0617-658810', '0617-605230','0617-689511','0617-444138','0617-449451','0617-755214','0617-763022','0617-759543','0617-629653','0617-761615','0617-761493','0617-511240', '0617-608184', '0617-704484', '0617-469897', '0617-664444', '0617-647889', '0617-669208', '0617-619079', '0617-657744', '0617-457079', '0617-592873', '0617-673681', '0617-698612', '0617-529851', '0617-674729', '0617-466966', '0617-747279', '0617-619629', '0617-637689', '0617-682647', '0617-727320', '0617-596122', '0617-714162', '0617-381906', '0617-575441', '0617-540476', '0617-647078', '0617-654574', '0617-457446', '0617-752117', '0617-635346', '0617-714849', '0617-712281', '0617-438343', '0617-617572', '0617-536867', '0617-579384', '0617-656006', '0617-716741', '0617-553859', '0617-730769', '0617-446756', '0617-445118', '0617-621065', '0617-602544', '0617-483282', '0617-548132', '0617-739625'] - diff --git a/Configs/Classification/tSettingsRTOG_All_PTV.ini~ b/Configs/Classification/tSettingsRTOG_All_PTV.ini~ deleted file mode 100644 index 44e8209..0000000 --- a/Configs/Classification/tSettingsRTOG_All_PTV.ini~ +++ /dev/null @@ -1,71 +0,0 @@ -[MODEL] -Activation = 'Sigmoid' -Backbone = 'DenseNet' -Records_Backbone = 'Linear' -batch_size = 10 -Prediction_type = 'Classification' -CT_spatial_dims = 3 -Loss_Function = 'BCEWithLogitsLoss' - -[MODEL_PARAMETERS] -spatial_dims = 3 -in_channels = 3 -out_channels = 512 -block_config = [1,2,4,2] -dropout_prob = 0.15 - -[MODALITY] -CT = '1' -Dose = '1' -Structs = '1' - -[DATA] -Nifty = true -n_classes = 1 -Multichannel = true -dim = [128,128,32] -threshold = 24 -Structs = 'AI_target.nii.gz' -DataFolder = '/home/dgs1/data/Segmentation/Seg/RTOG0617/' -LogFolder = 'Classification/random_seed_75_all_ptv' -vis = [0] -train_size = 0.7 -val_size = 0.3 -target = 'survival_months' -censor_label = 'lost_to_followup' - -[Records] -category_feat = ['arm', 'gender', 'race', 'ethnicity', 'zubrod', - 'histology', 'nonsquam_squam', 'ajcc_stage_grp', 'rt_technique', - 'smoke_hx', 'rx_terminated_ae', 'received_conc_chemo','rt_compliance_ptv90', - 'received_cons_chemo','rt_dose','pet_staging','received_rt'] -numerical_feat = ['age', 'volume_ptv','v5_lung','v20_lung','dmean_lung', - 'v5_heart','v30_heart','v20_esophagus','v60_esophagus'] - - -[CHECKPOINT] -monitor = "val_loss" #"val_acc_epoch" -mode = "max" -matrix = ['ROC', 'Specificity','Sensitivity','Accuracy','Precision','WorstCase'] - - -[SERVER] -Address = 'http://128.16.11.124:8080/xnat' -Projects = ["RTOG_0617"] -User = "yzhan" -Password = "yzhang" -[CRITERIA] -analysis_inclusion = 1 -[FILTER] -patient_id = ['0617-529851', '0617-446756', '0617-647889', '0617-608184', '0617-483479', '0617-381906', '0617-637689', '0617-568854', '0617-444138', - '0617-619629', '0617-698612', '0617-647078', '0617-704484', '0617-482383', '0617-704274', '0617-727320', '0617-674729', '0617-583972', - '0617-309598', '0617-511240', '0617-656006', '0617-658810', '0617-712281', '0617-559978', '0617-761615', '0617-673098', '0617-673681', - '0617-605230', '0617-669208', '0617-607275', '0617-457079', '0617-730769', '0617-449451', '0617-752117', '0617-635346', '0617-657744', - '0617-739625', '0617-736925', '0617-620721', '0617-617572', '0617-438343', '0617-553859', '0617-483282', '0617-690485', '0617-689511', - '0617-575441', '0617-789552', '0617-682647', '0617-445118', '0617-536867', '0617-457446', '0617-592873', '0617-540476', '0617-619079', - '0617-548132', '0617-535250', '0617-654574', '0617-621065', '0617-714849', '0617-613919', '0617-714162', '0617-540662', '0617-711646', - '0617-602544', '0617-693977', '0617-596122', '0617-716741', '0617-466966', '0617-579384', '0617-664444', '0617-469897', '0617-761493', - '0617-747279', '0617-763022', '0617-805653', '0617-640690'] - - - diff --git a/Configs/Classification/tSettingsRTOG_Dose_Nii.ini~ b/Configs/Classification/tSettingsRTOG_Dose_Nii.ini~ deleted file mode 100644 index b56fef9..0000000 --- a/Configs/Classification/tSettingsRTOG_Dose_Nii.ini~ +++ /dev/null @@ -1,61 +0,0 @@ -[MODEL] -Activation = 'Sigmoid' -Backbone = 'DenseNet' -Records_Backbone = 'Linear' -batch_size = 10 -Prediction_type = 'Classification' -CT_spatial_dims = 3 -Loss_Function = 'BCEWithLogitsLoss' - -[MODEL_PARAMETERS] -spatial_dims = 3 -in_channels = 2 -out_channels = 512 -block_config = [1,2,4,2] -dropout_prob = 0.3 - -[MODALITY] -Dose = '1' -Structs = '1' - -[DATA] -Nifty = true -n_classes = 1 -Multichannel = true -dim = [128,128,32] -threshold = 24 -Structs = 'masks.nii.gz' -DataFolder = '/home/dgs1/data/Segmentation/Seg/RTOG0617/' -LogFolder = 'Classification/random_seed_75_dose_nii' -vis = [0] -train_size = 0.7 -val_size = 0.3 -target = 'survival_months' -censor_label = 'lost_to_followup' - -[CHECKPOINT] -monitor = "val_loss" #"val_acc_epoch" -mode = "max" -matrix = ['ROC', 'Specificity','Sensitivity','Accuracy','Precision','WorstCase'] - - -[SERVER] -Address = 'http://128.16.11.124:8080/xnat' -Projects = ["RTOG_0617"] -User = "yzhan" -Password = "yzhang" -[CRITERIA] -analysis_inclusion = 1 -[FILTER] -patient_id = ['0617-529851', '0617-446756', '0617-647889', '0617-608184', '0617-483479', '0617-381906', '0617-637689', '0617-568854', '0617-444138', - '0617-619629', '0617-698612', '0617-647078', '0617-704484', '0617-482383', '0617-704274', '0617-727320', '0617-674729', '0617-583972', - '0617-309598', '0617-511240', '0617-656006', '0617-658810', '0617-712281', '0617-559978', '0617-761615', '0617-673098', '0617-673681', - '0617-605230', '0617-669208', '0617-607275', '0617-457079', '0617-730769', '0617-449451', '0617-752117', '0617-635346', '0617-657744', - '0617-739625', '0617-736925', '0617-620721', '0617-617572', '0617-438343', '0617-553859', '0617-483282', '0617-690485', '0617-689511', - '0617-575441', '0617-789552', '0617-682647', '0617-445118', '0617-536867', '0617-457446', '0617-592873', '0617-540476', '0617-619079', - '0617-548132', '0617-535250', '0617-654574', '0617-621065', '0617-714849', '0617-613919', '0617-714162', '0617-540662', '0617-711646', - '0617-602544', '0617-693977', '0617-596122', '0617-716741', '0617-466966', '0617-579384', '0617-664444', '0617-469897', '0617-761493', - '0617-747279', '0617-763022', '0617-805653', '0617-640690'] - - - diff --git a/Configs/Regression/SettingsRTOG_Multi.ini~ b/Configs/Regression/SettingsRTOG_Multi.ini~ deleted file mode 100644 index 91f4779..0000000 --- a/Configs/Regression/SettingsRTOG_Multi.ini~ +++ /dev/null @@ -1,61 +0,0 @@ -[MODEL] -Activation = 'ReLU' -Backbone = 'DenseNet' -Records_Backbone = 'Linear' -batch_size = 10 -Prediction_type = 'Regression' -CT_spatial_dims = 3 -Loss_Function = 'MSELoss' - -[MODEL_PARAMETERS] -spatial_dims = 3 -in_channels = 2 -out_channels = 512 -block_config = [1,2,4,2] -dropout_prob = 0.3 - -[MODALITY] -CT = '1' -Structs = '1' - -[DATA] -Nifty = true -n_classes = 1 -Multichannel = true -dim = [128,128,32] -Structs = ['esophagus','pulmonary_artery', 'lung', 'lung_vessels', 'heart_atrium_left', 'heart_atrium_right', - 'heart_ventricle_left', 'heart_ventricle_right', 'heart_myocardium', 'coronary_arteries' ,'AI_Target'] -DataFolder = '/home/dgs1/data/Nifty_Data/RTOG0617/' -LogFolder = 'Regression/random_seed_75_CT' -vis = [0] -train_size = 0.7 -val_size = 0.3 -target = 'survival_months' -censor_label = 'lost_to_followup' - -[CHECKPOINT] -monitor = "val_loss" #"val_acc_epoch" -mode = "min" -matrix = ['r2','cindex'] - - -[SERVER] -Address = 'http://128.16.11.124:8080/xnat' -Projects = ["RTOG_0617"] -User = "yzhan" -Password = "yzhan" -[CRITERIA] -analysis_inclusion = 1 -[FILTER] -patient_id = ['0617-529851', '0617-446756', '0617-647889', '0617-608184', '0617-483479', '0617-381906', '0617-637689', '0617-568854', '0617-444138', - '0617-619629', '0617-698612', '0617-647078', '0617-704484', '0617-482383', '0617-704274', '0617-727320', '0617-674729', '0617-583972', - '0617-309598', '0617-511240', '0617-656006', '0617-658810', '0617-712281', '0617-559978', '0617-761615', '0617-673098', '0617-673681', - '0617-605230', '0617-669208', '0617-607275', '0617-457079', '0617-730769', '0617-449451', '0617-752117', '0617-635346', '0617-657744', - '0617-739625', '0617-736925', '0617-620721', '0617-617572', '0617-438343', '0617-553859', '0617-483282', '0617-690485', '0617-689511', - '0617-575441', '0617-789552', '0617-682647', '0617-445118', '0617-536867', '0617-457446', '0617-592873', '0617-540476', '0617-619079', - '0617-548132', '0617-535250', '0617-654574', '0617-621065', '0617-714849', '0617-613919', '0617-714162', '0617-540662', '0617-711646', - '0617-602544', '0617-693977', '0617-596122', '0617-716741', '0617-466966', '0617-579384', '0617-664444', '0617-469897', '0617-761493', - '0617-747279', '0617-763022', '0617-805653', '0617-640690'] - - - diff --git a/DataGenerator/DataGenerator.py b/DataGenerator/DataGenerator.py index 489017d..1f8e159 100644 --- a/DataGenerator/DataGenerator.py +++ b/DataGenerator/DataGenerator.py @@ -93,7 +93,6 @@ def __getitem__(self, i): if 'threshold' in self.config['DATA'].keys(): ## Classification label = torch.where(label > self.config['DATA']['threshold'], 1, 0) label = torch.as_tensor(label, dtype=torch.float32) - #label = (censored_label, label) return data, censor_status, label @@ -106,7 +105,7 @@ def __init__(self, SubjectList, config=None, train_transform=None, val_transform self.num_workers = num_workers data_trans = class_stratify(SubjectList, config) ## Split Test with fixed seed - train_val_list, test_list = train_test_split(SubjectList, train_size=train_size, random_state=75, + train_val_list, test_list = train_test_split(SubjectList, train_size=train_size, random_state=42, stratify=data_trans) data_trans = class_stratify(train_val_list, config) diff --git a/Models/MixModel.py b/Models/MixModel.py index bceb51d..d185d30 100644 --- a/Models/MixModel.py +++ b/Models/MixModel.py @@ -51,17 +51,6 @@ def training_epoch_end(self, step_outputs): censor_status = torch.cat([out['censor_status'] for i, out in enumerate(step_outputs)], dim=0) prediction = torch.cat([out['prediction'] for i, out in enumerate(step_outputs)], dim=0) self.logger.report_epoch(prediction, censor_status, labels, step_outputs,self.current_epoch, 'train_epoch_') - # with open(self.logger.log_dir + "/train_record.ini", "a") as toml_file: - # toml_file.write('\n') - # toml_file.write('label_epoch_' + str(self.current_epoch) + ':\n') - # toml_file.write(str(labels[1])) - # toml_file.write('\n') - # toml_file.write('censor_epoch_' + str(self.current_epoch) + ':\n') - # toml_file.write(str(labels[0])) - # toml_file.write('\n') - # toml_file.write('prediction_epoch_' + str(self.current_epoch) + ':\n') - # toml_file.write(str(prediction)) - # toml_file.write('\n') def validation_step(self, batch, batch_idx): data_dict, censor_status, label = batch @@ -82,17 +71,6 @@ def validation_epoch_end(self, step_outputs): censor_status = torch.cat([out['censor_status'] for i, out in enumerate(step_outputs)], dim=0) prediction = torch.cat([out['prediction'] for i, out in enumerate(step_outputs)], dim=0) self.logger.report_epoch(prediction, censor_status, labels, step_outputs, self.current_epoch, 'val_epoch_') - # with open(self.logger.log_dir + "/val_record.ini", "a") as toml_file: - # toml_file.write('\n') - # toml_file.write('label_epoch_' + str(self.current_epoch) + ':\n') - # toml_file.write(str(labels[1])) - # toml_file.write('\n') - # toml_file.write('censor_epoch_' + str(self.current_epoch) + ':\n') - # toml_file.write(str(labels[0])) - # toml_file.write('\n') - # toml_file.write('prediction_epoch_' + str(self.current_epoch) + ':\n') - # toml_file.write(str(prediction)) - # toml_file.write('\n') def test_step(self, batch, batch_idx): data_dict, censor_status, label = batch diff --git a/Utils/DicomTools.py b/Utils/DicomTools.py index 8b67365..8cb984b 100644 --- a/Utils/DicomTools.py +++ b/Utils/DicomTools.py @@ -102,76 +102,6 @@ def get_masked_img_voxel(ImageVoxel, mask_voxel): img_masked = ImageVoxel[:, bbox[0]:bbox[1], bbox[2]:bbox[3], bbox[4]:bbox[5]] return img_masked - -# def img_train_transform(img_dim): -# transform = tio.Compose([ -# tio.transforms.ZNormalization(), -# tio.RandomAffine(), -# tio.RandomFlip(), -# tio.RandomNoise(), -# tio.RandomMotion(), -# tio.transforms.Resize(img_dim), -# tio.RescaleIntensity(out_min_max=(0, 1)) -# ]) -# return transform -# -# -# def img_val_transform(img_dim): -# transform = tio.Compose([ -# tio.transforms.ZNormalization(), -# tio.transforms.Resize(img_dim), -# tio.RescaleIntensity(out_min_max=(0, 1)) -# ]) -# return transform - - -def get_RS_masks(slabel, CTPath, mask_imgs, RSfile, mask_names): - #RS = RTStructBuilder.create_from(dicom_series_path=CTPath, rt_struct_path=RSfile) - #roi_names = RS.get_roi_names() - #strList = [x.lower() for x in roi_names] - #for idx, roi in enumerate(mask_names): - # if roi.lower() in strList: - # roi_s = roi_names[strList.index(roi.lower())] - # mask_img = RS.get_roi_mask_by_name(roi_s) - # # mask_img = distance_transform_edt(mask_img) - # mask_imgs = BitSet(mask_imgs, idx * np.ones_like(mask_imgs), mask_img) - # else: - # raise ValueError(slabel + " has no ROI of name " + roi + " found in RTStruct") - # - #return mask_imgs - - RS = RTStructBuilder.create_from(dicom_series_path=CTPath, rt_struct_path=RSfile) - roi_names = RS.get_roi_names() - strList = [x.lower() for x in roi_names] - for idx, roi in enumerate(mask_names): - if roi.lower() in strList: - roi_s = roi_names[strList.index(roi.lower())] - mask_img = RS.get_roi_mask_by_name(roi_s) - mask_img = distance_transform_edt(mask_img) - mask_imgs = mask_imgs + mask_img - else: - raise ValueError(slabel + " has no ROI of name " + roi + " found in RTStruct") - - return mask_imgs - -def get_nii_masks(slabel, mask_imgs, MPath, mask_names): - for idx, roi in enumerate(mask_names): - try: - data, meta = LoadImage()(Path(MPath, roi + '.nii.gz')) - except: - raise ValueError(slabel + " has no ROI of name " + roi + " found in RTStruct") - mask_imgs = BitSet(mask_imgs, idx * np.ones_like(mask_imgs), data) - return mask_imgs - # for roi in mask_names: - # try: - # data, meta = LoadImage()(Path(MPath, roi + '.nii.gz')) - # except: - # raise ValueError(slabel + " has no ROI of name " + roi + " found in RTStruct") - # mask_img = distance_transform_edt(data) - # mask_imgs = mask_imgs + mask_img - # return mask_imgs - - def BitSet(n, p, b): p = p.astype(int) n = n.astype(int) From 87e0595bf53fec9b4cf365f59e2928e06e1f9bb6 Mon Sep 17 00:00:00 2001 From: Clara Date: Fri, 21 Apr 2023 15:13:32 +0100 Subject: [PATCH 11/12] remove --- Utils/GenerateSmoothLabel.py | 93 ------------------------------------ 1 file changed, 93 deletions(-) delete mode 100644 Utils/GenerateSmoothLabel.py diff --git a/Utils/GenerateSmoothLabel.py b/Utils/GenerateSmoothLabel.py deleted file mode 100644 index 0c6b8b4..0000000 --- a/Utils/GenerateSmoothLabel.py +++ /dev/null @@ -1,93 +0,0 @@ -import numpy as np -import matplotlib.pyplot as plt -from scipy.ndimage import convolve1d -from scipy.ndimage import gaussian_filter1d -from scipy.signal.windows import triang -from sksurv.metrics import cumulative_dynamic_auc -from torch import nn -from Models.Classifier import Classifier -from Models.Linear import Linear -from pathlib import Path - -def get_lds_kernel_window(kernel, ks, sigma): - assert kernel in ['gaussian', 'triang', 'laplace'] - half_ks = (ks - 1) // 2 - if kernel == 'gaussian': - base_kernel = [0.] * half_ks + [1.] + [0.] * half_ks - kernel_window = gaussian_filter1d(base_kernel, sigma=sigma) / max(gaussian_filter1d(base_kernel, sigma=sigma)) - elif kernel == 'triang': - kernel_window = triang(ks) - else: - laplace = lambda x: np.exp(-abs(x) / sigma) / (2. * sigma) - kernel_window = list(map(laplace, np.arange(-half_ks, half_ks + 1))) / max( - map(laplace, np.arange(-half_ks, half_ks + 1))) - - return kernel_window - - -def get_smoothed_label_distribution(SubjectList, config): - label_all = get_train_label(SubjectList, config) - range_max = np.max(label_all).astype(int) + 1 - range_min = np.min(label_all).astype(int) - - label_range = np.arange(range_min, range_max, 1) - - bin_index_per_label = np.histogram(label_all, bins=label_range) - lds_kernel_window = get_lds_kernel_window(kernel='gaussian', ks=7, sigma=3) - eff_label_dist = convolve1d(np.array(bin_index_per_label[0]), weights=lds_kernel_window, mode='constant') - - eff_num_per_label = [eff_label_dist[bin_idx] for bin_idx in np.arange(eff_label_dist.shape[0])] - weights = [np.float32(1 / x) for x in eff_num_per_label] - - label_mean = np.mean(label_all) - mse = ((label_all - label_mean) ** 2).mean() - return weights, bin_index_per_label[1] - - -def get_train_label(SubjectList, config): - train_label = [] - for patient in SubjectList: - label = patient.fields[config['DATA']['target']] - train_label.append(label) - return train_label - - -def get_module(config): - s_module = config['DATA']['module'] - module_dict = nn.ModuleDict() - if config['MODEL']['Clinical_Backbone']: - Clinical_backbone = Linear() - for i, module in enumerate(s_module): - if module == 'CT' or module == 'Dose' or module == 'PET': - Backbone = Classifier(config) - module_dict[module] = Backbone - else: - module_dict[module] = Clinical_backbone - - return module_dict - - -def generate_cumulative_dynamic_auc(prediction, label, path) -> None: - # this function has issues - risk_score = 1 / prediction - va_times = np.arange(int(label.cpu().min()) + 1, label.cpu().max(), 1) - - dtypes = np.dtype([('event', np.bool_), ('time', np.float)]) - construct_test = np.ndarray(shape=(len(label),), dtype=dtypes) - for i in range(len(label)): - construct_test[i] = (True, label[i].cpu().numpy()) - - cph_auc, cph_mean_auc = cumulative_dynamic_auc( - construct_test, construct_test, risk_score.cpu().squeeze(), va_times - ) - - fig = plt.figure() - plt.plot(va_times, cph_auc, marker="o") - plt.axhline(cph_mean_auc, linestyle="--") - plt.ylim([0, 1]) - plt.xlabel("survival months") - plt.ylabel("time-dependent AUC") - plt.grid(True) - #plt.show() - plt.savefig(Path(path, 't_auroc.jpg')) - From f16ff54da0f79ed770a37b3af6162c5314dacd0f Mon Sep 17 00:00:00 2001 From: Clara Date: Fri, 21 Apr 2023 15:23:28 +0100 Subject: [PATCH 12/12] final check --- SubmitScript/submitjob.pbs | 39 --------------------- SubmitScript/submitjob2.pbs | 39 --------------------- SubmitScript/submitjob3.pbs | 39 --------------------- SubmitScript/submitjob4.pbs | 40 ---------------------- SubmitScript/submitjob4.pbs.save | 39 --------------------- SubmitScript/submitjob5.pbs | 39 --------------------- Utils/DicomTools.py | 3 -- Utils/PredictionReports.py | 58 -------------------------------- 8 files changed, 296 deletions(-) delete mode 100644 SubmitScript/submitjob.pbs delete mode 100644 SubmitScript/submitjob2.pbs delete mode 100644 SubmitScript/submitjob3.pbs delete mode 100644 SubmitScript/submitjob4.pbs delete mode 100644 SubmitScript/submitjob4.pbs.save delete mode 100644 SubmitScript/submitjob5.pbs diff --git a/SubmitScript/submitjob.pbs b/SubmitScript/submitjob.pbs deleted file mode 100644 index fb89aa9..0000000 --- a/SubmitScript/submitjob.pbs +++ /dev/null @@ -1,39 +0,0 @@ -#!/bin/bash -### Job Name -#PBS -N OP -### Project code -#PBS -A AI_OutcomePrediction -### Maximum time this job can run before being killed (here, 1 day) -#PBS -l walltime=15:00:00:00 -### Resource Request (must contain cpucore, memory, and gpu (even if requested amount is zero) -#PBS -l cpucore=30:memory=200gb:gpu=4 -### Output Options (default is stdout_and_stderr) -#PBS -l outputMode=stdout_and_stderr -##PBS -l outputMode=no_output -##PBS -l outputMode=stdout_only -##PBS -l outputMode=stderr_only - -#### ENVIRONMENT VARIABLES BELOW AUTOMATICALLY ADDED FROM QSUB #### -# >>> conda initialize >>> -# !! Contents within this block are managed by 'conda init' !! -__conda_setup="$('/home/dgs1/anaconda3/bin/conda' 'shell.bash' 'hook' 2> /dev/null)" -if [ $? -eq 0 ]; then - eval "$__conda_setup" -else - if [ -f "/home/dgs1/anaconda3/etc/profile.d/conda.sh" ]; then - . "/home/dgs1/anaconda3/etc/profile.d/conda.sh" - else - export PATH="/home/dgs1/anaconda3/bin:$PATH" - fi -fi -unset __conda_setup -# <<< conda initialize <<< -conda activate UCL_OP -export PYTHONPATH=$PYTHONPATH:/home/dgs1/Software/OutcomePrediction -export LS_COLORS="$LS_COLORS:ow=1;34:tw=1;34:" -#### ENVIRONMENT VARIABLES ABOVE AUTOMATICALLY ADDED FROM QSUB #### -#python Training/Regression.py Configs/Regression/SettingsRTOG_Multi_Apple.ini -python Training/Regression.py Configs/Regression/SettingsRTOG_Multi2.ini -### Run your job here -#printenv -#python Training/Preprocess.py ConfigDefault.ini diff --git a/SubmitScript/submitjob2.pbs b/SubmitScript/submitjob2.pbs deleted file mode 100644 index ffff6e4..0000000 --- a/SubmitScript/submitjob2.pbs +++ /dev/null @@ -1,39 +0,0 @@ -#!/bin/bash -### Job Name -#PBS -N OP -### Project code -#PBS -A AI_OutcomePrediction -### Maximum time this job can run before being killed (here, 1 day) -#PBS -l walltime=15:00:00:00 -### Resource Request (must contain cpucore, memory, and gpu (even if requested amount is zero) -#PBS -l cpucore=30:memory=200gb:gpu=4 -### Output Options (default is stdout_and_stderr) -#PBS -l outputMode=stdout_and_stderr -##PBS -l outputMode=no_output -##PBS -l outputMode=stdout_only -##PBS -l outputMode=stderr_only - -#### ENVIRONMENT VARIABLES BELOW AUTOMATICALLY ADDED FROM QSUB #### -# >>> conda initialize >>> -# !! Contents within this block are managed by 'conda init' !! -__conda_setup="$('/home/dgs1/anaconda3/bin/conda' 'shell.bash' 'hook' 2> /dev/null)" -if [ $? -eq 0 ]; then - eval "$__conda_setup" -else - if [ -f "/home/dgs1/anaconda3/etc/profile.d/conda.sh" ]; then - . "/home/dgs1/anaconda3/etc/profile.d/conda.sh" - else - export PATH="/home/dgs1/anaconda3/bin:$PATH" - fi -fi -unset __conda_setup -# <<< conda initialize <<< -conda activate UCL_OP -export PYTHONPATH=$PYTHONPATH:/home/dgs1/Software/OutcomePrediction -export LS_COLORS="$LS_COLORS:ow=1;34:tw=1;34:" -#### ENVIRONMENT VARIABLES ABOVE AUTOMATICALLY ADDED FROM QSUB #### -python Training/Classification.py Configs/Classification/SettingsRTOG_All_PTV2.ini - -### Run your job here -#printenv -#python Training/Preprocess.py ConfigDefault.ini diff --git a/SubmitScript/submitjob3.pbs b/SubmitScript/submitjob3.pbs deleted file mode 100644 index 001bada..0000000 --- a/SubmitScript/submitjob3.pbs +++ /dev/null @@ -1,39 +0,0 @@ -#!/bin/bash -### Job Name -#PBS -N OP -### Project code -#PBS -A AI_OutcomePrediction -### Maximum time this job can run before being killed (here, 1 day) -#PBS -l walltime=15:00:00:00 -### Resource Request (must contain cpucore, memory, and gpu (even if requested amount is zero) -#PBS -l cpucore=30:memory=200gb:gpu=4 -### Output Options (default is stdout_and_stderr) -#PBS -l outputMode=stdout_and_stderr -##PBS -l outputMode=no_output -##PBS -l outputMode=stdout_only -##PBS -l outputMode=stderr_only - -#### ENVIRONMENT VARIABLES BELOW AUTOMATICALLY ADDED FROM QSUB #### -# >>> conda initialize >>> -# !! Contents within this block are managed by 'conda init' !! -__conda_setup="$('/home/dgs1/anaconda3/bin/conda' 'shell.bash' 'hook' 2> /dev/null)" -if [ $? -eq 0 ]; then - eval "$__conda_setup" -else - if [ -f "/home/dgs1/anaconda3/etc/profile.d/conda.sh" ]; then - . "/home/dgs1/anaconda3/etc/profile.d/conda.sh" - else - export PATH="/home/dgs1/anaconda3/bin:$PATH" - fi -fi -unset __conda_setup -# <<< conda initialize <<< -conda activate UCL_OP -export PYTHONPATH=$PYTHONPATH:/home/dgs1/Software/OutcomePrediction -export LS_COLORS="$LS_COLORS:ow=1;34:tw=1;34:" -#### ENVIRONMENT VARIABLES ABOVE AUTOMATICALLY ADDED FROM QSUB #### -python Training/Classification.py Configs/Classification/SettingsRTOG_All_PTV3.ini - -### Run your job here -#printenv -#python Training/Preprocess.py ConfigDefault.ini diff --git a/SubmitScript/submitjob4.pbs b/SubmitScript/submitjob4.pbs deleted file mode 100644 index eb3bf9c..0000000 --- a/SubmitScript/submitjob4.pbs +++ /dev/null @@ -1,40 +0,0 @@ -#!/bin/bash -### Job Name -#PBS -N OP -### Project code -#PBS -A AI_OutcomePrediction -### Maximum time this job can run before being killed (here, 1 day) -#PBS -l walltime=15:00:00:00 -### Resource Request (must contain cpucore, memory, and gpu (even if requested amount is zero) -#PBS -l cpucore=30:memory=200gb:gpu=4 -### Output Options (default is stdout_and_stderr) -#PBS -l outputMode=stdout_and_stderr -##PBS -l outputMode=no_output -##PBS -l outputMode=stdout_only -##PBS -l outputMode=stderr_only - -#### ENVIRONMENT VARIABLES BELOW AUTOMATICALLY ADDED FROM QSUB #### -# >>> conda initialize >>> -# !! Contents within this block are managed by 'conda init' !! -__conda_setup="$('/home/dgs1/anaconda3/bin/conda' 'shell.bash' 'hook' 2> /dev/null)" -if [ $? -eq 0 ]; then - eval "$__conda_setup" -else - if [ -f "/home/dgs1/anaconda3/etc/profile.d/conda.sh" ]; then - . "/home/dgs1/anaconda3/etc/profile.d/conda.sh" - else - export PATH="/home/dgs1/anaconda3/bin:$PATH" - fi -fi -unset __conda_setup -# <<< conda initialize <<< -conda activate UCL_OP -export PYTHONPATH=$PYTHONPATH:/home/dgs1/Software/OutcomePrediction -export LS_COLORS="$LS_COLORS:ow=1;34:tw=1;34:" -#### ENVIRONMENT VARIABLES ABOVE AUTOMATICALLY ADDED FROM QSUB #### -python Training/Classification.py Configs/Classification/SettingsRTOG_All_Nii_Resnet.ini - -### Run your job here -#printenv -#python Training/Preprocess.py ConfigDefault.ini - diff --git a/SubmitScript/submitjob4.pbs.save b/SubmitScript/submitjob4.pbs.save deleted file mode 100644 index 72b47d1..0000000 --- a/SubmitScript/submitjob4.pbs.save +++ /dev/null @@ -1,39 +0,0 @@ -#!/bin/bash -### Job Name -#PBS -N OP -### Project code -#PBS -A AI_OutcomePrediction -### Maximum time this job can run before being killed (here, 1 day) -#PBS -l walltime=15:00:00:00 -### Resource Request (must contain cpucore, memory, and gpu (even if requested amount is zero) -#PBS -l cpucore=30:memory=200gb:gpu=4 -### Output Options (default is stdout_and_stderr) -#PBS -l outputMode=stdout_and_stderr -##PBS -l outputMode=no_output -##PBS -l outputMode=stdout_only -##PBS -l outputMode=stderr_only - -#### ENVIRONMENT VARIABLES BELOW AUTOMATICALLY ADDED FROM QSUB #### -# >>> conda initialize >>> -# !! Contents within this block are managed by 'conda init' !! -__conda_setup="$('/home/dgs1/anaconda3/bin/conda' 'shell.bash' 'hook' 2> /dev/null)" -if [ $? -eq 0 ]; then - eval "$__conda_setup" -else - if [ -f "/home/dgs1/anaconda3/etc/profile.d/conda.sh" ]; then - . "/home/dgs1/anaconda3/etc/profile.d/conda.sh" - else - export PATH="/home/dgs1/anaconda3/bin:$PATH" - fi -fi -unset __conda_setup -# <<< conda initialize <<< -conda activate UCL_OP -export PYTHONPATH=$PYTHONPATH:/home/dgs1/Software/OutcomePrediction -export LS_COLORS="$LS_COLORS:ow=1;34:tw=1;34:" -#### ENVIRONMENT VARIABLES ABOVE AUTOMATICALLY ADDED FROM QSUB #### -python Training/Classification.py Configs/Classification/SettingsRTOG_CT.ini - -### Run your job here -#printenv -#python Training/Preprocess.py ConfigDefault.ini diff --git a/SubmitScript/submitjob5.pbs b/SubmitScript/submitjob5.pbs deleted file mode 100644 index dc366e2..0000000 --- a/SubmitScript/submitjob5.pbs +++ /dev/null @@ -1,39 +0,0 @@ -#!/bin/bash -### Job Name -#PBS -N OP -### Project code -#PBS -A AI_OutcomePrediction -### Maximum time this job can run before being killed (here, 1 day) -#PBS -l walltime=15:00:00:00 -### Resource Request (must contain cpucore, memory, and gpu (even if requested amount is zero) -#PBS -l cpucore=30:memory=200gb:gpu=4 -### Output Options (default is stdout_and_stderr) -#PBS -l outputMode=stdout_and_stderr -##PBS -l outputMode=no_output -##PBS -l outputMode=stdout_only -##PBS -l outputMode=stderr_only - -#### ENVIRONMENT VARIABLES BELOW AUTOMATICALLY ADDED FROM QSUB #### -# >>> conda initialize >>> -# !! Contents within this block are managed by 'conda init' !! -__conda_setup="$('/home/dgs1/anaconda3/bin/conda' 'shell.bash' 'hook' 2> /dev/null)" -if [ $? -eq 0 ]; then - eval "$__conda_setup" -else - if [ -f "/home/dgs1/anaconda3/etc/profile.d/conda.sh" ]; then - . "/home/dgs1/anaconda3/etc/profile.d/conda.sh" - else - export PATH="/home/dgs1/anaconda3/bin:$PATH" - fi -fi -unset __conda_setup -# <<< conda initialize <<< -conda activate UCL_OP -export PYTHONPATH=$PYTHONPATH:/home/dgs1/Software/OutcomePrediction -export LS_COLORS="$LS_COLORS:ow=1;34:tw=1;34:" -#### ENVIRONMENT VARIABLES ABOVE AUTOMATICALLY ADDED FROM QSUB #### -python Training/Classification.py Configs/Classification/SettingsRTOG_All_PTV.ini - -### Run your job here -#printenv -#python Training/Preprocess.py ConfigDefault.ini diff --git a/Utils/DicomTools.py b/Utils/DicomTools.py index 8cb984b..3f62440 100644 --- a/Utils/DicomTools.py +++ b/Utils/DicomTools.py @@ -1,14 +1,11 @@ import os import glob -#import cv2 import SimpleITK as sitk import pydicom as dicom from pydicom import dcmread import numpy as np import matplotlib import matplotlib.pyplot as plt -#from monai.data import ITKReader, PILReader -#import torchio as tio from pathlib import Path from sklearn.preprocessing import KBinsDiscretizer from monai.transforms import LoadImage diff --git a/Utils/PredictionReports.py b/Utils/PredictionReports.py index 17daac7..8ee7807 100644 --- a/Utils/PredictionReports.py +++ b/Utils/PredictionReports.py @@ -39,35 +39,6 @@ def __init__(self, config, def log_hyperparams(self, params: argparse.Namespace, *args, **kwargs): pass - # @property - # def version(self): - # description = '' - # for i, param in enumerate(self.config['CRITERIA'].keys()): - # clinical_criteria = str(self.config['CRITERIA'][param]) - # if i > 0: - # description = description + '_' - # description = description + param + '+' + '+'.join(clinical_criteria) - # # Return the experiment version, int or str. - # - # sub_str = description + '_' + 'modalities' + '+' + '+'.join(self.config['MODALITY'].keys()) - # description = description + '_' + 'modalities' + '+' + '+'.join(self.config['MODALITY'].keys()) + '_' + str(self._get_next_version(sub_str)) - # return description - # - # def _get_next_version(self, sub_str): - # root_dir = self.root_dir - # listdir_info = self._fs.listdir(root_dir) - # existing_versions = [] - # for listing in listdir_info: - # d = listing["name"] - # bn = os.path.basename(d) - # if self._fs.isdir(d) and bn.startswith(sub_str): - # dir_ver = bn.split("_")[-1] - # existing_versions.append(int(dir_ver)) - # if len(existing_versions) == 0: - # return 0 - # - # return max(existing_versions) + 1 - def log_metrics(self, metrics: Dict[str, float], step: Optional[int] = None): for k, v in metrics.items(): if isinstance(v, torch.Tensor): @@ -83,8 +54,6 @@ def log_image(self, img, text, current_epoch=None): def log_text(self) -> None: configurations = 'The modules included are ' + str(self.config['MODALITY'].keys()) - # configurations = 'The img_dim is ' + str(self.config['DATA']['dim']) + ' and the modules included are ' + - # str(self.config['MODALITY'].keys()) self.experiment.add_text('configurations:', configurations) def regression_matrix(self, prediction, censor_status, label, prefix): ## matrix should be metrics @@ -127,37 +96,10 @@ def classification_matrix(self, prediction, label, prefix): c_out[prefix + 'precision'] = precision return c_out - # def generate_cumulative_dynamic_auc(self, prediction, label, current_epoch, prefix) -> None: - # # this function has issues - # risk_score = 1 / prediction - # # risk_score = prediction - # # va_times = np.arange(int(label.cpu().min()) + 1, label.cpu().max(), 1) - # va_times = np.percentile(label.cpu(), np.linspace(5, 81, 20)) - # dtypes = np.dtype([('event', np.bool_), ('time', np.float)]) - # construct_test = np.ndarray(shape=(len(label),), dtype=dtypes) - # for i in range(len(label)): - # construct_test[i] = (True, label[i].cpu().numpy()) - # - # cph_auc, cph_mean_auc = cumulative_dynamic_auc( - # construct_test, construct_test, risk_score.cpu().squeeze(), va_times - # ) - # - # fig = plt.figure() - # plt.plot(va_times, cph_auc, marker="o") - # plt.axhline(cph_mean_auc, linestyle="--") - # plt.ylim([0, 1]) - # plt.xlabel("survival months") - # plt.ylabel("time-dependent AUC") - # plt.grid(True) - # self.experiment.add_figure(prefix + "AUC", fig, current_epoch) - # plt.close(fig) - def plot_AUROC(self, prediction, label, prefix, current_epoch=None) -> None: roc = torchmetrics.ROC() fpr, tpr, _ = roc(prediction, label) fig = plt.figure() - # lw = 2 - # plt.plot(fpr.cpu(), tpr.cpu(), color='darkorange', lw=lw) plt.plot(fpr.cpu(), tpr.cpu(), color='darkorange') plt.title(prefix + '_roc_curve') plt.xlabel('False Positive Rate')