├── src ├── __init__.py ├── metrics.py ├── losses.py ├── data │ ├── splits │ │ ├── train_val_split_0.pkl │ │ ├── train_val_split_1.pkl │ │ ├── train_val_split_2.pkl │ │ ├── train_val_split_3.pkl │ │ ├── train_val_new_split_0.pkl │ │ ├── train_val_new_split_1.pkl │ │ ├── train_val_new_split_2.pkl │ │ └── train_val_new_split_3.pkl │ ├── make_dataset.py │ └── utils.py ├── dataset.py ├── layers.py ├── predictor.py ├── models.py ├── transforms.py └── trainer.py ├── .gitignore ├── config ├── make_dataset.yaml ├── model_train.yaml └── model_predict.yaml ├── LICENSE ├── model ├── predict.py └── train.py ├── README.md └── notebooks └── make_dataset.ipynb /src/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | temp.ipynb 2 | .ipynb_checkpoints -------------------------------------------------------------------------------- /config/make_dataset.yaml: -------------------------------------------------------------------------------- 1 | path_to_input: '/home/iantsen/hecktor/data/hecktor_test/hecktor_nii_test/' # directory with images 2 | path_to_bb: '/home/iantsen/hecktor/data/hecktor_test/bbox_test.csv' # file with bounding box coordinates 3 | path_to_output: '/home/iantsen/hecktor/data/hecktor_test/hecktor_nii_resampled/' # directory to save resampled images 4 | is_mask_available: false # if `true`, masks will be preprocessed. Use `false`, if masks are unavailable 5 | verbose: false # if `true`, the progress bar will be shown -------------------------------------------------------------------------------- /src/metrics.py: -------------------------------------------------------------------------------- 1 | def dice(input, target): 2 | axes = tuple(range(1, input.dim())) 3 | bin_input = (input > 0.5).float() 4 | 5 | intersect = (bin_input * target).sum(dim=axes) 6 | union = bin_input.sum(dim=axes) + target.sum(dim=axes) 7 | score = 2 * intersect / (union + 1e-3) 8 | 9 | return score.mean() 10 | 11 | 12 | def recall(input, target): 13 | axes = tuple(range(1, input.dim())) 14 | binary_input = (input > 0.5).float() 15 | 16 | true_positives = (binary_input * target).sum(dim=axes) 17 | all_positives = target.sum(dim=axes) 18 | recall = true_positives / all_positives 19 | 20 | return recall.mean() 21 | 22 | 23 | def precision(input, target): 24 | axes = tuple(range(1, input.dim())) 25 | binary_input = (input > 0.5).float() 26 | 27 | true_positives = (binary_input * target).sum(dim=axes) 28 | all_positive_calls = binary_input.sum(dim=axes) 29 | precision = true_positives / all_positive_calls 30 | 31 | return precision.mean() 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 iantsen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /config/model_train.yaml: -------------------------------------------------------------------------------- 1 | # paths: 2 | path_to_data: 'C:/inserm/hecktor/hecktor_train/hecktor_nii_resampled/' # directory with images 3 | path_to_pkl: 'C:/inserm/hecktor/splits/train_val_split_0.pkl' # pkl file with train / val splits 4 | path_to_save_dir: 'C:/inserm/hecktor/results/' # all results (weights, learning curves, etc) will be saved here 5 | 6 | # train settings: 7 | train_batch_size: 1 8 | val_batch_size: 1 9 | num_workers: 2 # for example, use a number of CPU cores 10 | 11 | lr: 1e-3 # initial learning rate 12 | n_epochs: 2 # number of training epochs (300 was used in the paper) 13 | n_cls: 2 # number of classes to predict (background and tumor) 14 | in_channels: 2 # number of input modalities 15 | n_filters: 4 # number of filters after the input (24 was used in the paper) 16 | reduction: 2 # parameter controls the size of the bottleneck in SENorm layers 17 | 18 | T_0: 25 # parameter for 'torch.optim.lr_scheduler.CosineAnnealingWarmRestarts' 19 | eta_min: 1e-5 # parameter for 'torch.optim.lr_scheduler.CosineAnnealingWarmRestarts' 20 | 21 | # model: 22 | baseline: false # if `true`, U-Net will be used. Otherwise, the model described in the paper will be trained. 23 | -------------------------------------------------------------------------------- /src/losses.py: -------------------------------------------------------------------------------- 1 | import torch 2 | from torch import nn 3 | 4 | 5 | class DiceLoss(nn.Module): 6 | def __init__(self): 7 | super(DiceLoss, self).__init__() 8 | self.smooth = 1 9 | 10 | def forward(self, input, target): 11 | axes = tuple(range(1, input.dim())) 12 | intersect = (input * target).sum(dim=axes) 13 | union = torch.pow(input, 2).sum(dim=axes) + torch.pow(target, 2).sum(dim=axes) 14 | loss = 1 - (2 * intersect + self.smooth) / (union + self.smooth) 15 | return loss.mean() 16 | 17 | 18 | class FocalLoss(nn.Module): 19 | def __init__(self, gamma=2): 20 | super(FocalLoss, self).__init__() 21 | self.gamma = gamma 22 | self.eps = 1e-3 23 | 24 | def forward(self, input, target): 25 | input = input.clamp(self.eps, 1 - self.eps) 26 | loss = - (target * torch.pow((1 - input), self.gamma) * torch.log(input) + 27 | (1 - target) * torch.pow(input, self.gamma) * torch.log(1 - input)) 28 | return loss.mean() 29 | 30 | 31 | class Dice_and_FocalLoss(nn.Module): 32 | def __init__(self, gamma=2): 33 | super(Dice_and_FocalLoss, self).__init__() 34 | self.dice_loss = DiceLoss() 35 | self.focal_loss = FocalLoss(gamma) 36 | 37 | def forward(self, input, target): 38 | loss = self.dice_loss(input, target) + self.focal_loss(input, target) 39 | return loss 40 | -------------------------------------------------------------------------------- /config/model_predict.yaml: -------------------------------------------------------------------------------- 1 | # paths: 2 | path_to_data: '/home/iantsen/hecktor/data/hecktor_test/hecktor_nii_resampled/' # directory with test images 3 | path_to_save_dir: '/home/iantsen/hecktor/data/hecktor_test/preds/' # predictions will be saved here 4 | 5 | path_to_weights: # path or paths to weights. If multiple paths provided, an ensemble of models will be used 6 | - '/home/iantsen/hecktor/model/weights/s0_best_model_weights.pt' 7 | - '/home/iantsen/hecktor/model/weights/s1_best_model_weights.pt' 8 | - '/home/iantsen/hecktor/model/weights/s2_best_model_weights.pt' 9 | - '/home/iantsen/hecktor/model/weights/s3_best_model_weights.pt' 10 | - '/home/iantsen/hecktor/model/weights/ns0_best_model_weights.pt' 11 | - '/home/iantsen/hecktor/model/weights/ns1_best_model_weights.pt' 12 | - '/home/iantsen/hecktor/model/weights/ns2_best_model_weights.pt' 13 | - '/home/iantsen/hecktor/model/weights/ns3_best_model_weights.pt' 14 | 15 | # output: 16 | probs: false # if `true`, the sigmoid output will be saved. Otherwise, 0.5-threshold will be applied to get binary labels 17 | 18 | # train settings: 19 | num_workers: 2 # for example, use a number of CPU cores 20 | 21 | n_cls: 2 # number of classes to predict (background and tumor) 22 | in_channels: 2 # number of input modalities 23 | n_filters: 24 # number of filters after the input (24 was used in the paper) 24 | reduction: 2 # parameter controls the size of the bottleneck in SENorm layers 25 | -------------------------------------------------------------------------------- /src/data/splits/train_val_split_0.pkl: -------------------------------------------------------------------------------- 1 | {"train": ["CHUS003", "CHUS004", "CHUS005", "CHUS006", "CHUS007", "CHUS008", "CHUS009", "CHUS010", "CHUS013", "CHUS015", "CHUS016", "CHUS019", "CHUS020", "CHUS021", "CHUS022", "CHUS026", "CHUS027", "CHUS028", "CHUS030", "CHUS031", "CHUS033", "CHUS035", "CHUS036", "CHUS038", "CHUS039", "CHUS040", "CHUS041", "CHUS042", "CHUS043", "CHUS045", "CHUS046", "CHUS047", "CHUS048", "CHUS049", "CHUS050", "CHUS051", "CHUS052", "CHUS053", "CHUS055", "CHUS056", "CHUS057", "CHUS058", "CHUS060", "CHUS061", "CHUS064", "CHUS065", "CHUS066", "CHUS067", "CHUS068", "CHUS069", "CHUS073", "CHUS074", "CHUS076", "CHUS077", "CHUS078", "CHUS080", "CHUS081", "CHUS083", "CHUS085", "CHUS086", "CHUS087", "CHUS088", "CHUS089", "CHUS090", "CHUS091", "CHUS094", "CHUS095", "CHUS096", "CHUS097", "CHUS098", "CHUS100", "CHUS101", "CHUM001", "CHUM002", "CHUM006", "CHUM007", "CHUM008", "CHUM010", "CHUM011", "CHUM012", "CHUM013", "CHUM014", "CHUM015", "CHUM016", "CHUM017", "CHUM018", "CHUM019", "CHUM021", "CHUM022", "CHUM023", "CHUM024", "CHUM026", "CHUM027", "CHUM029", "CHUM030", "CHUM032", "CHUM033", "CHUM034", "CHUM035", "CHUM036", "CHUM037", "CHUM038", "CHUM039", "CHUM040", "CHUM041", "CHUM042", "CHUM043", "CHUM044", "CHUM045", "CHUM046", "CHUM047", "CHUM048", "CHUM049", "CHUM050", "CHUM051", "CHUM053", "CHUM054", "CHUM055", "CHUM056", "CHUM057", "CHUM058", "CHUM059", "CHUM060", "CHUM061", "CHUM062", "CHUM063", "CHUM064", "CHUM065", "CHMR001", "CHMR004", "CHMR005", "CHMR011", "CHMR012", "CHMR013", "CHMR014", "CHMR016", "CHMR020", "CHMR021", "CHMR023", "CHMR024", "CHMR025", "CHMR028", "CHMR029", "CHMR030", "CHMR034", "CHMR040"], "val": ["CHGJ007", "CHGJ008", "CHGJ010", "CHGJ013", "CHGJ015", "CHGJ016", "CHGJ017", "CHGJ018", "CHGJ025", "CHGJ026", "CHGJ028", "CHGJ029", "CHGJ030", "CHGJ031", "CHGJ032", "CHGJ034", "CHGJ035", "CHGJ036", "CHGJ037", "CHGJ038", "CHGJ039", "CHGJ043", "CHGJ046", "CHGJ048", "CHGJ050", "CHGJ052", "CHGJ053", "CHGJ055", "CHGJ057", "CHGJ058", "CHGJ062", "CHGJ065", "CHGJ066", "CHGJ067", "CHGJ069", "CHGJ070", "CHGJ071", "CHGJ072", "CHGJ073", "CHGJ074", "CHGJ076", "CHGJ077", "CHGJ078", "CHGJ080", "CHGJ081", "CHGJ082", "CHGJ083", "CHGJ085", "CHGJ086", "CHGJ087", "CHGJ088", "CHGJ089", "CHGJ090", "CHGJ091", "CHGJ092"]} -------------------------------------------------------------------------------- /src/data/splits/train_val_split_1.pkl: -------------------------------------------------------------------------------- 1 | {"train": ["CHGJ007", "CHGJ008", "CHGJ010", "CHGJ013", "CHGJ015", "CHGJ016", "CHGJ017", "CHGJ018", "CHGJ025", "CHGJ026", "CHGJ028", "CHGJ029", "CHGJ030", "CHGJ031", "CHGJ032", "CHGJ034", "CHGJ035", "CHGJ036", "CHGJ037", "CHGJ038", "CHGJ039", "CHGJ043", "CHGJ046", "CHGJ048", "CHGJ050", "CHGJ052", "CHGJ053", "CHGJ055", "CHGJ057", "CHGJ058", "CHGJ062", "CHGJ065", "CHGJ066", "CHGJ067", "CHGJ069", "CHGJ070", "CHGJ071", "CHGJ072", "CHGJ073", "CHGJ074", "CHGJ076", "CHGJ077", "CHGJ078", "CHGJ080", "CHGJ081", "CHGJ082", "CHGJ083", "CHGJ085", "CHGJ086", "CHGJ087", "CHGJ088", "CHGJ089", "CHGJ090", "CHGJ091", "CHGJ092", "CHUM001", "CHUM002", "CHUM006", "CHUM007", "CHUM008", "CHUM010", "CHUM011", "CHUM012", "CHUM013", "CHUM014", "CHUM015", "CHUM016", "CHUM017", "CHUM018", "CHUM019", "CHUM021", "CHUM022", "CHUM023", "CHUM024", "CHUM026", "CHUM027", "CHUM029", "CHUM030", "CHUM032", "CHUM033", "CHUM034", "CHUM035", "CHUM036", "CHUM037", "CHUM038", "CHUM039", "CHUM040", "CHUM041", "CHUM042", "CHUM043", "CHUM044", "CHUM045", "CHUM046", "CHUM047", "CHUM048", "CHUM049", "CHUM050", "CHUM051", "CHUM053", "CHUM054", "CHUM055", "CHUM056", "CHUM057", "CHUM058", "CHUM059", "CHUM060", "CHUM061", "CHUM062", "CHUM063", "CHUM064", "CHUM065", "CHMR001", "CHMR004", "CHMR005", "CHMR011", "CHMR012", "CHMR013", "CHMR014", "CHMR016", "CHMR020", "CHMR021", "CHMR023", "CHMR024", "CHMR025", "CHMR028", "CHMR029", "CHMR030", "CHMR034", "CHMR040"], "val": ["CHUS003", "CHUS004", "CHUS005", "CHUS006", "CHUS007", "CHUS008", "CHUS009", "CHUS010", "CHUS013", "CHUS015", "CHUS016", "CHUS019", "CHUS020", "CHUS021", "CHUS022", "CHUS026", "CHUS027", "CHUS028", "CHUS030", "CHUS031", "CHUS033", "CHUS035", "CHUS036", "CHUS038", "CHUS039", "CHUS040", "CHUS041", "CHUS042", "CHUS043", "CHUS045", "CHUS046", "CHUS047", "CHUS048", "CHUS049", "CHUS050", "CHUS051", "CHUS052", "CHUS053", "CHUS055", "CHUS056", "CHUS057", "CHUS058", "CHUS060", "CHUS061", "CHUS064", "CHUS065", "CHUS066", "CHUS067", "CHUS068", "CHUS069", "CHUS073", "CHUS074", "CHUS076", "CHUS077", "CHUS078", "CHUS080", "CHUS081", "CHUS083", "CHUS085", "CHUS086", "CHUS087", "CHUS088", "CHUS089", "CHUS090", "CHUS091", "CHUS094", "CHUS095", "CHUS096", "CHUS097", "CHUS098", "CHUS100", "CHUS101"]} -------------------------------------------------------------------------------- /src/data/splits/train_val_split_2.pkl: -------------------------------------------------------------------------------- 1 | {"train": ["CHGJ007", "CHGJ008", "CHGJ010", "CHGJ013", "CHGJ015", "CHGJ016", "CHGJ017", "CHGJ018", "CHGJ025", "CHGJ026", "CHGJ028", "CHGJ029", "CHGJ030", "CHGJ031", "CHGJ032", "CHGJ034", "CHGJ035", "CHGJ036", "CHGJ037", "CHGJ038", "CHGJ039", "CHGJ043", "CHGJ046", "CHGJ048", "CHGJ050", "CHGJ052", "CHGJ053", "CHGJ055", "CHGJ057", "CHGJ058", "CHGJ062", "CHGJ065", "CHGJ066", "CHGJ067", "CHGJ069", "CHGJ070", "CHGJ071", "CHGJ072", "CHGJ073", "CHGJ074", "CHGJ076", "CHGJ077", "CHGJ078", "CHGJ080", "CHGJ081", "CHGJ082", "CHGJ083", "CHGJ085", "CHGJ086", "CHGJ087", "CHGJ088", "CHGJ089", "CHGJ090", "CHGJ091", "CHGJ092", "CHUS003", "CHUS004", "CHUS005", "CHUS006", "CHUS007", "CHUS008", "CHUS009", "CHUS010", "CHUS013", "CHUS015", "CHUS016", "CHUS019", "CHUS020", "CHUS021", "CHUS022", "CHUS026", "CHUS027", "CHUS028", "CHUS030", "CHUS031", "CHUS033", "CHUS035", "CHUS036", "CHUS038", "CHUS039", "CHUS040", "CHUS041", "CHUS042", "CHUS043", "CHUS045", "CHUS046", "CHUS047", "CHUS048", "CHUS049", "CHUS050", "CHUS051", "CHUS052", "CHUS053", "CHUS055", "CHUS056", "CHUS057", "CHUS058", "CHUS060", "CHUS061", "CHUS064", "CHUS065", "CHUS066", "CHUS067", "CHUS068", "CHUS069", "CHUS073", "CHUS074", "CHUS076", "CHUS077", "CHUS078", "CHUS080", "CHUS081", "CHUS083", "CHUS085", "CHUS086", "CHUS087", "CHUS088", "CHUS089", "CHUS090", "CHUS091", "CHUS094", "CHUS095", "CHUS096", "CHUS097", "CHUS098", "CHUS100", "CHUS101", "CHMR001", "CHMR004", "CHMR005", "CHMR011", "CHMR012", "CHMR013", "CHMR014", "CHMR016", "CHMR020", "CHMR021", "CHMR023", "CHMR024", "CHMR025", "CHMR028", "CHMR029", "CHMR030", "CHMR034", "CHMR040"], "val": ["CHUM001", "CHUM002", "CHUM006", "CHUM007", "CHUM008", "CHUM010", "CHUM011", "CHUM012", "CHUM013", "CHUM014", "CHUM015", "CHUM016", "CHUM017", "CHUM018", "CHUM019", "CHUM021", "CHUM022", "CHUM023", "CHUM024", "CHUM026", "CHUM027", "CHUM029", "CHUM030", "CHUM032", "CHUM033", "CHUM034", "CHUM035", "CHUM036", "CHUM037", "CHUM038", "CHUM039", "CHUM040", "CHUM041", "CHUM042", "CHUM043", "CHUM044", "CHUM045", "CHUM046", "CHUM047", "CHUM048", "CHUM049", "CHUM050", "CHUM051", "CHUM053", "CHUM054", "CHUM055", "CHUM056", "CHUM057", "CHUM058", "CHUM059", "CHUM060", "CHUM061", "CHUM062", "CHUM063", "CHUM064", "CHUM065"]} -------------------------------------------------------------------------------- /src/data/splits/train_val_split_3.pkl: -------------------------------------------------------------------------------- 1 | {"train": ["CHGJ007", "CHGJ008", "CHGJ010", "CHGJ013", "CHGJ015", "CHGJ016", "CHGJ017", "CHGJ018", "CHGJ025", "CHGJ026", "CHGJ028", "CHGJ029", "CHGJ030", "CHGJ031", "CHGJ032", "CHGJ034", "CHGJ035", "CHGJ036", "CHGJ037", "CHGJ038", "CHGJ039", "CHGJ043", "CHGJ046", "CHGJ048", "CHGJ050", "CHGJ052", "CHGJ053", "CHGJ055", "CHGJ057", "CHGJ058", "CHGJ062", "CHGJ065", "CHGJ066", "CHGJ067", "CHGJ069", "CHGJ070", "CHGJ071", "CHGJ072", "CHGJ073", "CHGJ074", "CHGJ076", "CHGJ077", "CHGJ078", "CHGJ080", "CHGJ081", "CHGJ082", "CHGJ083", "CHGJ085", "CHGJ086", "CHGJ087", "CHGJ088", "CHGJ089", "CHGJ090", "CHGJ091", "CHGJ092", "CHUS003", "CHUS004", "CHUS005", "CHUS006", "CHUS007", "CHUS008", "CHUS009", "CHUS010", "CHUS013", "CHUS015", "CHUS016", "CHUS019", "CHUS020", "CHUS021", "CHUS022", "CHUS026", "CHUS027", "CHUS028", "CHUS030", "CHUS031", "CHUS033", "CHUS035", "CHUS036", "CHUS038", "CHUS039", "CHUS040", "CHUS041", "CHUS042", "CHUS043", "CHUS045", "CHUS046", "CHUS047", "CHUS048", "CHUS049", "CHUS050", "CHUS051", "CHUS052", "CHUS053", "CHUS055", "CHUS056", "CHUS057", "CHUS058", "CHUS060", "CHUS061", "CHUS064", "CHUS065", "CHUS066", "CHUS067", "CHUS068", "CHUS069", "CHUS073", "CHUS074", "CHUS076", "CHUS077", "CHUS078", "CHUS080", "CHUS081", "CHUS083", "CHUS085", "CHUS086", "CHUS087", "CHUS088", "CHUS089", "CHUS090", "CHUS091", "CHUS094", "CHUS095", "CHUS096", "CHUS097", "CHUS098", "CHUS100", "CHUS101", "CHUM001", "CHUM002", "CHUM006", "CHUM007", "CHUM008", "CHUM010", "CHUM011", "CHUM012", "CHUM013", "CHUM014", "CHUM015", "CHUM016", "CHUM017", "CHUM018", "CHUM019", "CHUM021", "CHUM022", "CHUM023", "CHUM024", "CHUM026", "CHUM027", "CHUM029", "CHUM030", "CHUM032", "CHUM033", "CHUM034", "CHUM035", "CHUM036", "CHUM037", "CHUM038", "CHUM039", "CHUM040", "CHUM041", "CHUM042", "CHUM043", "CHUM044", "CHUM045", "CHUM046", "CHUM047", "CHUM048", "CHUM049", "CHUM050", "CHUM051", "CHUM053", "CHUM054", "CHUM055", "CHUM056", "CHUM057", "CHUM058", "CHUM059", "CHUM060", "CHUM061", "CHUM062", "CHUM063", "CHUM064", "CHUM065"], "val": ["CHMR001", "CHMR004", "CHMR005", "CHMR011", "CHMR012", "CHMR013", "CHMR014", "CHMR016", "CHMR020", "CHMR021", "CHMR023", "CHMR024", "CHMR025", "CHMR028", "CHMR029", "CHMR030", "CHMR034", "CHMR040"]} -------------------------------------------------------------------------------- /src/data/splits/train_val_new_split_0.pkl: -------------------------------------------------------------------------------- 1 | {"train": ["CHUS052", "CHUS053", "CHMR024", "CHUS050", "CHUM038", "CHGJ030", "CHMR020", "CHMR021", "CHUS065", "CHGJ052", "CHMR014", "CHUS036", "CHMR040", "CHUM032", "CHUS033", "CHUS069", "CHUM044", "CHUS021", "CHUS013", "CHUS094", "CHGJ035", "CHUM006", "CHMR011", "CHUM002", "CHUM046", "CHGJ038", "CHUM043", "CHGJ073", "CHGJ085", "CHMR030", "CHUM065", "CHUM007", "CHUM061", "CHUS003", "CHUM037", "CHUS027", "CHGJ062", "CHMR013", "CHUM016", "CHUS030", "CHGJ086", "CHUS043", "CHGJ025", "CHUS091", "CHUM029", "CHGJ091", "CHGJ048", "CHUS051", "CHUS090", "CHUM026", "CHGJ046", "CHUM036", "CHUS009", "CHUM023", "CHGJ008", "CHUM012", "CHMR016", "CHUS097", "CHUM060", "CHUS042", "CHGJ078", "CHUM054", "CHMR005", "CHGJ026", "CHUS007", "CHUM014", "CHUS098", "CHUS004", "CHUS066", "CHUS035", "CHGJ018", "CHGJ077", "CHUS049", "CHUM048", "CHUM040", "CHUS078", "CHMR012", "CHUM015", "CHUS074", "CHUS045", "CHUS048", "CHGJ088", "CHGJ083", "CHUS055", "CHGJ050", "CHUM050", "CHGJ065", "CHUS068", "CHGJ072", "CHUS089", "CHGJ071", "CHUS077", "CHGJ037", "CHUS016", "CHUS083", "CHUS005", "CHUS067", "CHGJ028", "CHUS064", "CHUS056", "CHUM034", "CHUS010", "CHMR025", "CHUM001", "CHGJ013", "CHUM035", "CHGJ070", "CHUS095", "CHGJ057", "CHUS046", "CHUM059", "CHUM013", "CHUM064", "CHUM022", "CHMR001", "CHUM011", "CHMR034", "CHUM030", "CHUS031", "CHGJ031", "CHUS100", "CHGJ087", "CHUM062", "CHMR029", "CHUS096", "CHUM021", "CHUS038", "CHUM010", "CHUS088", "CHGJ067", "CHUS060", "CHUS076", "CHGJ034", "CHUM017", "CHUM063", "CHGJ058", "CHUS019", "CHUM058", "CHUM033", "CHUM045", "CHUM053", "CHUM041", "CHUS101", "CHUM039", "CHUM008", "CHGJ036", "CHUS041", "CHUS039", "CHGJ032", "CHUM056"], "val": ["CHUM051", "CHUM055", "CHUS073", "CHGJ010", "CHUS026", "CHUS022", "CHGJ039", "CHUS015", "CHUS061", "CHUS008", "CHGJ076", "CHGJ007", "CHUS087", "CHGJ029", "CHGJ017", "CHUM042", "CHGJ080", "CHUM019", "CHMR028", "CHGJ015", "CHGJ081", "CHGJ043", "CHGJ069", "CHGJ016", "CHUS020", "CHUS058", "CHGJ082", "CHUS047", "CHUM018", "CHUS080", "CHGJ055", "CHGJ066", "CHGJ089", "CHUM057", "CHUS081", "CHGJ090", "CHUS085", "CHUM049", "CHGJ074", "CHUS040", "CHUS086", "CHUS006", "CHUM027", "CHUS028", "CHUM024", "CHGJ053", "CHMR004", "CHGJ092", "CHUS057", "CHMR023", "CHUM047"]} -------------------------------------------------------------------------------- /src/data/splits/train_val_new_split_1.pkl: -------------------------------------------------------------------------------- 1 | {"train": ["CHUM051", "CHUM055", "CHUS073", "CHGJ010", "CHUS026", "CHUS022", "CHGJ039", "CHUS015", "CHUS061", "CHUS008", "CHGJ076", "CHGJ007", "CHUS087", "CHGJ029", "CHGJ017", "CHUM042", "CHGJ080", "CHUM019", "CHMR028", "CHGJ015", "CHGJ081", "CHGJ043", "CHGJ069", "CHGJ016", "CHUS020", "CHUS058", "CHGJ082", "CHUS047", "CHUM018", "CHUS080", "CHGJ055", "CHGJ066", "CHGJ089", "CHUM057", "CHUS081", "CHGJ090", "CHUS085", "CHUM049", "CHGJ074", "CHUS040", "CHUS086", "CHUS006", "CHUM027", "CHUS028", "CHUM024", "CHGJ053", "CHMR004", "CHGJ092", "CHUS057", "CHMR023", "CHUM047", "CHUM002", "CHGJ038", "CHUM029", "CHGJ091", "CHUS051", "CHUS090", "CHGJ046", "CHUS009", "CHUM023", "CHGJ008", "CHUM012", "CHMR016", "CHUM060", "CHUS042", "CHGJ078", "CHMR005", "CHGJ026", "CHUS007", "CHUM014", "CHUS004", "CHUS066", "CHUS035", "CHGJ018", "CHGJ077", "CHUS049", "CHUM048", "CHUM040", "CHUS078", "CHMR012", "CHUM015", "CHUS074", "CHUS045", "CHUS048", "CHGJ088", "CHGJ083", "CHUM050", "CHGJ065", "CHUS068", "CHGJ072", "CHUS089", "CHGJ071", "CHUS077", "CHGJ037", "CHUS016", "CHUS083", "CHUS005", "CHUS067", "CHGJ028", "CHUS064", "CHUS056", "CHUM034", "CHUS010", "CHMR025", "CHUM001", "CHGJ013", "CHUM035", "CHGJ070", "CHUS095", "CHGJ057", "CHUS046", "CHUM059", "CHUM013", "CHUM064", "CHUM022", "CHMR001", "CHUM011", "CHMR034", "CHUM030", "CHUS031", "CHGJ031", "CHUS100", "CHGJ087", "CHUM062", "CHMR029", "CHUS096", "CHUM021", "CHUS038", "CHUM010", "CHUS088", "CHGJ067", "CHUS060", "CHUS076", "CHGJ034", "CHUM017", "CHUM063", "CHGJ058", "CHUS019", "CHUM058", "CHUM033", "CHUM045", "CHUM053", "CHUM041", "CHUS101", "CHUM039", "CHUM008", "CHGJ036", "CHUS041", "CHUS039", "CHGJ032", "CHUM056"], "val": ["CHUS052", "CHUS053", "CHMR024", "CHUS050", "CHUM038", "CHGJ030", "CHMR020", "CHMR021", "CHUS065", "CHGJ052", "CHMR014", "CHUS036", "CHMR040", "CHUM032", "CHUS033", "CHUS069", "CHUM044", "CHUS021", "CHUS013", "CHUS094", "CHGJ035", "CHUM006", "CHMR011", "CHUM046", "CHUM043", "CHGJ073", "CHGJ085", "CHMR030", "CHUM065", "CHUM007", "CHUM061", "CHUS003", "CHUM037", "CHUS027", "CHGJ062", "CHMR013", "CHUM016", "CHUS030", "CHGJ086", "CHUS043", "CHGJ025", "CHUS091", "CHGJ048", "CHUM026", "CHUM036", "CHUS097", "CHUM054", "CHUS098", "CHUS055", "CHGJ050"]} -------------------------------------------------------------------------------- /src/data/splits/train_val_new_split_2.pkl: -------------------------------------------------------------------------------- 1 | {"train": ["CHUM051", "CHUM055", "CHUS073", "CHGJ010", "CHUS026", "CHUS022", "CHGJ039", "CHUS015", "CHUS061", "CHUS008", "CHGJ076", "CHGJ007", "CHUS087", "CHGJ029", "CHGJ017", "CHUM042", "CHGJ080", "CHUM019", "CHMR028", "CHGJ015", "CHGJ081", "CHGJ043", "CHGJ069", "CHGJ016", "CHUS020", "CHUS058", "CHGJ082", "CHUS047", "CHUM018", "CHUS080", "CHGJ055", "CHUS052", "CHGJ066", "CHGJ089", "CHUM057", "CHUS081", "CHGJ090", "CHUS085", "CHUM049", "CHGJ074", "CHUS040", "CHUS053", "CHUS086", "CHUS006", "CHUM027", "CHUS028", "CHMR024", "CHUM024", "CHGJ053", "CHUS050", "CHMR004", "CHGJ092", "CHUM038", "CHGJ030", "CHMR020", "CHMR021", "CHUS065", "CHUS057", "CHGJ052", "CHMR014", "CHUS036", "CHMR023", "CHMR040", "CHUM032", "CHUS033", "CHUS069", "CHUM044", "CHUM047", "CHUS021", "CHUS013", "CHUS094", "CHGJ035", "CHUM006", "CHMR011", "CHUM046", "CHUM043", "CHGJ073", "CHGJ085", "CHMR030", "CHUM065", "CHUM007", "CHUM061", "CHUS003", "CHUM037", "CHUS027", "CHGJ062", "CHMR013", "CHUM016", "CHUS030", "CHGJ086", "CHUS043", "CHGJ025", "CHUS091", "CHGJ048", "CHUM026", "CHUM036", "CHUS097", "CHUM054", "CHUS098", "CHGJ083", "CHUS055", "CHGJ050", "CHUM050", "CHGJ071", "CHUS005", "CHUS064", "CHUM034", "CHGJ013", "CHUM035", "CHUS095", "CHGJ057", "CHUS046", "CHUM059", "CHUM013", "CHUM022", "CHMR001", "CHUM011", "CHMR034", "CHUM030", "CHUS031", "CHGJ031", "CHUS100", "CHGJ087", "CHUM062", "CHMR029", "CHUS096", "CHUM021", "CHUS038", "CHUM010", "CHUS088", "CHGJ067", "CHUS060", "CHUS076", "CHGJ034", "CHUM017", "CHUM063", "CHGJ058", "CHUS019", "CHUM058", "CHUM033", "CHUM045", "CHUM053", "CHUM041", "CHUS101", "CHUM039", "CHUM008", "CHGJ036", "CHUS041", "CHUS039", "CHGJ032", "CHUM056"], "val": ["CHUM002", "CHGJ038", "CHUM029", "CHGJ091", "CHUS051", "CHUS090", "CHGJ046", "CHUS009", "CHUM023", "CHGJ008", "CHUM012", "CHMR016", "CHUM060", "CHUS042", "CHGJ078", "CHMR005", "CHGJ026", "CHUS007", "CHUM014", "CHUS004", "CHUS066", "CHUS035", "CHGJ018", "CHGJ077", "CHUS049", "CHUM048", "CHUM040", "CHUS078", "CHMR012", "CHUM015", "CHUS074", "CHUS045", "CHUS048", "CHGJ088", "CHGJ065", "CHUS068", "CHGJ072", "CHUS089", "CHUS077", "CHGJ037", "CHUS016", "CHUS083", "CHUS067", "CHGJ028", "CHUS056", "CHUS010", "CHMR025", "CHUM001", "CHGJ070", "CHUM064"]} -------------------------------------------------------------------------------- /src/data/splits/train_val_new_split_3.pkl: -------------------------------------------------------------------------------- 1 | {"train": ["CHUM051", "CHUM055", "CHUS073", "CHGJ010", "CHUS026", "CHUS022", "CHGJ039", "CHUS015", "CHUS061", "CHUS008", "CHGJ076", "CHGJ007", "CHUS087", "CHGJ029", "CHGJ017", "CHUM042", "CHGJ080", "CHUM019", "CHMR028", "CHGJ015", "CHGJ081", "CHGJ043", "CHGJ069", "CHGJ016", "CHUS020", "CHUS058", "CHGJ082", "CHUS047", "CHUM018", "CHUS080", "CHGJ055", "CHUS052", "CHGJ066", "CHGJ089", "CHUM057", "CHUS081", "CHGJ090", "CHUS085", "CHUM049", "CHGJ074", "CHUS040", "CHUS053", "CHUS086", "CHUS006", "CHUM027", "CHUS028", "CHMR024", "CHUM024", "CHGJ053", "CHUS050", "CHMR004", "CHGJ092", "CHUM038", "CHGJ030", "CHMR020", "CHMR021", "CHUS065", "CHUS057", "CHGJ052", "CHMR014", "CHUS036", "CHMR023", "CHMR040", "CHUM032", "CHUS033", "CHUS069", "CHUM044", "CHUM047", "CHUS021", "CHUS013", "CHUS094", "CHGJ035", "CHUM006", "CHMR011", "CHUM002", "CHUM046", "CHGJ038", "CHUM043", "CHGJ073", "CHGJ085", "CHMR030", "CHUM065", "CHUM007", "CHUM061", "CHUS003", "CHUM037", "CHUS027", "CHGJ062", "CHMR013", "CHUM016", "CHUS030", "CHGJ086", "CHUS043", "CHGJ025", "CHUS091", "CHUM029", "CHGJ091", "CHGJ048", "CHUS051", "CHUS090", "CHUM026", "CHGJ046", "CHUM036", "CHUS009", "CHUM023", "CHGJ008", "CHUM012", "CHMR016", "CHUS097", "CHUM060", "CHUS042", "CHGJ078", "CHUM054", "CHMR005", "CHGJ026", "CHUS007", "CHUM014", "CHUS098", "CHUS004", "CHUS066", "CHUS035", "CHGJ018", "CHGJ077", "CHUS049", "CHUM048", "CHUM040", "CHUS078", "CHMR012", "CHUM015", "CHUS074", "CHUS045", "CHUS048", "CHGJ088", "CHUS055", "CHGJ050", "CHGJ065", "CHUS068", "CHGJ072", "CHUS089", "CHUS077", "CHGJ037", "CHUS016", "CHUS083", "CHUS067", "CHGJ028", "CHUS056", "CHUS010", "CHMR025", "CHUM001", "CHGJ070", "CHUM064"], "val": ["CHGJ083", "CHUM050", "CHGJ071", "CHUS005", "CHUS064", "CHUM034", "CHGJ013", "CHUM035", "CHUS095", "CHGJ057", "CHUS046", "CHUM059", "CHUM013", "CHUM022", "CHMR001", "CHUM011", "CHMR034", "CHUM030", "CHUS031", "CHGJ031", "CHUS100", "CHGJ087", "CHUM062", "CHMR029", "CHUS096", "CHUM021", "CHUS038", "CHUM010", "CHUS088", "CHGJ067", "CHUS060", "CHUS076", "CHGJ034", "CHUM017", "CHUM063", "CHGJ058", "CHUS019", "CHUM058", "CHUM033", "CHUM045", "CHUM053", "CHUM041", "CHUS101", "CHUM039", "CHUM008", "CHGJ036", "CHUS041", "CHUS039", "CHGJ032", "CHUM056"]} -------------------------------------------------------------------------------- /model/predict.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import argparse 3 | import yaml 4 | import pathlib 5 | 6 | import torch 7 | from torch.utils.data import DataLoader 8 | torch.backends.cudnn.benchmark = True 9 | 10 | sys.path.append('../src/') 11 | sys.path.append('../src/data/') 12 | import dataset 13 | import transforms 14 | import utils 15 | import models 16 | import predictor 17 | 18 | 19 | def main(args): 20 | path_to_config = pathlib.Path(args.path) 21 | with open(path_to_config) as f: 22 | config = yaml.safe_load(f) 23 | 24 | # read config: 25 | path_to_data = pathlib.Path(config['path_to_data']) 26 | path_to_save_dir = pathlib.Path(config['path_to_save_dir']) 27 | path_to_weights = config['path_to_weights'] 28 | probs = config['probs'] 29 | num_workers = int(config['num_workers']) 30 | n_cls = int(config['n_cls']) 31 | in_channels = int(config['in_channels']) 32 | n_filters = int(config['n_filters']) 33 | reduction = int(config['reduction']) 34 | 35 | # test data paths: 36 | all_paths = utils.get_paths_to_patient_files(path_to_imgs=path_to_data, append_mask=False) 37 | 38 | # input transforms: 39 | input_transforms = transforms.Compose([ 40 | transforms.NormalizeIntensity(), 41 | transforms.ToTensor(mode='test') 42 | ]) 43 | 44 | # ensemble output transforms: 45 | output_transforms = [ 46 | transforms.InverseToTensor(), 47 | transforms.CheckOutputShape(shape=(144, 144, 144)) 48 | ] 49 | if not probs: 50 | output_transforms.append(transforms.ProbsToLabels()) 51 | 52 | output_transforms = transforms.Compose(output_transforms) 53 | 54 | # dataset and dataloader: 55 | data_set = dataset.HecktorDataset(all_paths, transforms=input_transforms, mode='test') 56 | data_loader = DataLoader(data_set, batch_size=1, shuffle=False, num_workers=num_workers) 57 | 58 | # model: 59 | model = models.FastSmoothSENormDeepUNet_supervision_skip_no_drop(in_channels, n_cls, n_filters, reduction) 60 | 61 | # init predictor: 62 | predictor_ = predictor.Predictor( 63 | model=model, 64 | path_to_model_weights=path_to_weights, 65 | dataloader=data_loader, 66 | output_transforms=output_transforms, 67 | path_to_save_dir=path_to_save_dir 68 | ) 69 | 70 | # check if multiple paths were provided to run an ensemble: 71 | if isinstance(path_to_weights, list): 72 | predictor_.ensemble_predict() 73 | 74 | elif isinstance(path_to_weights, str): 75 | predictor_.predict() 76 | 77 | else: 78 | raise ValueError(f"Argument 'path_to_weights' must be str or list of str, provided {type(path_to_weights)}") 79 | 80 | 81 | if __name__ == "__main__": 82 | parser = argparse.ArgumentParser(description='Model Inference Script') 83 | parser.add_argument("-p", "--path", type=str, required=True, help="path to the config file") 84 | args = parser.parse_args() 85 | main(args) 86 | -------------------------------------------------------------------------------- /src/dataset.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import nibabel as nib 3 | from torch.utils.data import Dataset 4 | 5 | 6 | class HecktorDataset(Dataset): 7 | """A class for fetching data samples. 8 | 9 | Parameters 10 | ---------- 11 | paths_to_samples : list 12 | A list wherein each element is a tuple with two (three) `pathlib.Path` objects for a single patient. 13 | The first one is the path to the CT image, the second one - to the PET image. If `mode == 'train'`, a path to 14 | a ground truth mask must be provided for each patient. 15 | transforms 16 | Transformations applied to each data sample. 17 | mode : str 18 | Must be `train` or `test`. If `train`, a ground truth mask is loaded using a path from `paths_to_samples` and 19 | added to a sample. 20 | If `test`, an additional information (an affine array), that describes the position of the image data 21 | in a reference space, is added to each data sample. Ground truth masks are not loaded in this mode. 22 | 23 | Returns 24 | ------- 25 | dict 26 | A dictionary corresponding to a data sample. 27 | Keys: 28 | id : A patient's ID. 29 | input : A numpy array containing CT & PET images stacked along the last (4th) dimension. 30 | target : A numpy array containing a ground truth mask. 31 | affine : A numpy array with the position of the image data in a reference space (needed for resampling). 32 | """ 33 | 34 | def __init__(self, paths_to_samples, transforms=None, mode='train'): 35 | self.paths_to_samples = paths_to_samples 36 | self.transforms = transforms 37 | if mode not in ['train', 'test']: 38 | raise ValueError(f"Argument 'mode' must be 'train' or 'test'. Received {mode}") 39 | self.mode = mode 40 | if mode == 'train': 41 | self.num_of_seqs = len(paths_to_samples[0]) - 1 42 | else: 43 | self.num_of_seqs = len(paths_to_samples[0]) 44 | 45 | def __len__(self): 46 | return len(self.paths_to_samples) 47 | 48 | def __getitem__(self, index): 49 | sample = dict() 50 | 51 | id_ = self.paths_to_samples[index][0].parent.stem 52 | sample['id'] = id_ 53 | 54 | img = [self.read_data(self.paths_to_samples[index][i]) for i in range(self.num_of_seqs)] 55 | img = np.stack(img, axis=-1) 56 | sample['input'] = img 57 | 58 | if self.mode == 'train': 59 | mask = self.read_data(self.paths_to_samples[index][-1]) 60 | mask = np.expand_dims(mask, axis=3) 61 | 62 | assert img.shape[:-1] == mask.shape[:-1], \ 63 | f"Shape mismatch for the image with the shape {img.shape} and the mask with the shape {mask.shape}." 64 | 65 | sample['target'] = mask 66 | 67 | else: 68 | sample['affine'] = self.read_data(self.paths_to_samples[index][0], False).affine 69 | if self.transforms: 70 | sample = self.transforms(sample) 71 | 72 | return sample 73 | 74 | @staticmethod 75 | def read_data(path_to_nifti, return_numpy=True): 76 | """Read a NIfTI image. Return a numpy array (default) or `nibabel.nifti1.Nifti1Image` object""" 77 | if return_numpy: 78 | return nib.load(str(path_to_nifti)).get_fdata() 79 | return nib.load(str(path_to_nifti)) 80 | -------------------------------------------------------------------------------- /src/layers.py: -------------------------------------------------------------------------------- 1 | import torch 2 | from torch import nn 3 | from torch.nn import functional as F 4 | 5 | 6 | class BasicConv3d(nn.Module): 7 | def __init__(self, in_channels, out_channels, **kwargs): 8 | super(BasicConv3d, self).__init__() 9 | self.conv = nn.Conv3d(in_channels, out_channels, bias=False, **kwargs) 10 | self.norm = nn.InstanceNorm3d(out_channels, affine=True) 11 | 12 | def forward(self, x): 13 | x = self.conv(x) 14 | x = self.norm(x) 15 | x = F.relu(x, inplace=True) 16 | return x 17 | 18 | 19 | class FastSmoothSENorm(nn.Module): 20 | class SEWeights(nn.Module): 21 | def __init__(self, in_channels, reduction=2): 22 | super().__init__() 23 | self.conv1 = nn.Conv3d(in_channels, in_channels // reduction, kernel_size=1, stride=1, padding=0, bias=True) 24 | self.conv2 = nn.Conv3d(in_channels // reduction, in_channels, kernel_size=1, stride=1, padding=0, bias=True) 25 | 26 | def forward(self, x): 27 | b, c, d, h, w = x.size() 28 | out = torch.mean(x.view(b, c, -1), dim=-1).view(b, c, 1, 1, 1) # output_shape: in_channels x (1, 1, 1) 29 | out = F.relu(self.conv1(out)) 30 | out = self.conv2(out) 31 | return out 32 | 33 | def __init__(self, in_channels, reduction=2): 34 | super(FastSmoothSENorm, self).__init__() 35 | self.norm = nn.InstanceNorm3d(in_channels, affine=False) 36 | self.gamma = self.SEWeights(in_channels, reduction) 37 | self.beta = self.SEWeights(in_channels, reduction) 38 | 39 | def forward(self, x): 40 | gamma = torch.sigmoid(self.gamma(x)) 41 | beta = torch.tanh(self.beta(x)) 42 | x = self.norm(x) 43 | return gamma * x + beta 44 | 45 | 46 | class FastSmoothSeNormConv3d(nn.Module): 47 | def __init__(self, in_channels, out_channels, reduction=2, **kwargs): 48 | super(FastSmoothSeNormConv3d, self).__init__() 49 | self.conv = nn.Conv3d(in_channels, out_channels, bias=True, **kwargs) 50 | self.norm = FastSmoothSENorm(out_channels, reduction) 51 | 52 | def forward(self, x): 53 | x = self.conv(x) 54 | x = F.relu(x, inplace=True) 55 | x = self.norm(x) 56 | return x 57 | 58 | 59 | class RESseNormConv3d(nn.Module): 60 | def __init__(self, in_channels, out_channels, reduction=2, **kwargs): 61 | super().__init__() 62 | self.conv1 = FastSmoothSeNormConv3d(in_channels, out_channels, reduction, **kwargs) 63 | 64 | if in_channels != out_channels: 65 | self.res_conv = FastSmoothSeNormConv3d(in_channels, out_channels, reduction, kernel_size=1, stride=1, padding=0) 66 | else: 67 | self.res_conv = None 68 | 69 | def forward(self, x): 70 | residual = self.res_conv(x) if self.res_conv else x 71 | x = self.conv1(x) 72 | x += residual 73 | return x 74 | 75 | 76 | class UpConv(nn.Module): 77 | def __init__(self, in_channels, out_channels, reduction=2, scale=2): 78 | super().__init__() 79 | self.scale = scale 80 | self.conv = FastSmoothSeNormConv3d(in_channels, out_channels, reduction, kernel_size=1, stride=1, padding=0) 81 | 82 | def forward(self, x): 83 | x = self.conv(x) 84 | x = F.interpolate(x, scale_factor=self.scale, mode='trilinear', align_corners=False) 85 | return x 86 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 1st Place Solution for the [HECKTOR](https://www.aicrowd.com/challenges/miccai-2020-hecktor) challenge 2 | 3 | > The official implementation of the winning solution for the MICCAI 2020 HEad and neCK TumOR segmentation challenge (HECKTOR). 4 | 5 | ### Main requirements 6 | - PyTorch 1.6.0 (cuda 10.2) 7 | - SimpleITK 1.2.4 (ITK 4.13) 8 | - nibabel 3.1.1 9 | - skimage 0.17.2 10 | 11 | ### Dataset 12 | Train and test images are available through the competition [website](https://www.aicrowd.com/challenges/miccai-2020-hecktor). The concise description of the dataset is present in `notebooks/make_dataset.ipynb`. 13 | 14 | ### Data preprocessing 15 | The data preprocessing consists of: 16 | - Resampling the pair of PET & CT images for each patient to a common reference space. 17 | - Extracting the region of interest (bounding box) of the size of 144x144x144 voxels. 18 | - Saving the transformed images in NIfTI format. 19 | 20 | To prepare the dataset in _an interactive manner_, one can use `notebooks/make_dataset.ipynb`, that gives an explanation about each step. 21 | Alternatively, _the fully automated data preprocessing_ can be performed by running `src/data/make_dataset.py`. All required parameters must be provided as _a single config file_ in the YAML data format: 22 | ```sh 23 | cd hecktor/src/data/ 24 | python make_dataset.py -p hecktor/config/make_dataset.yaml 25 | ``` 26 | Use `/config/make_dataset.yaml` to specify all required parameters. 27 | 28 | ### Training 29 | For training the model from scratch, one can use `notebooks/model_train.ipynb` setting all parameters right in the notebook. Otherwise, with all parameters written in the config file, one needs to run `hecktor/model/train.py` from its current directory: 30 | ```sh 31 | cd hecktor/model/ 32 | python train.py -p hecktor/config/model_train.yaml 33 | ``` 34 | All parameters are described in `hecktor/config/model_train.yaml` that should be used as a template to build your own config file. 35 | 36 | ### Inference 37 | For inference, run the script `hecktor/model/predict.py` with parameters defined in the config file `hecktor/config/model_predict.yaml`: 38 | ```sh 39 | cd hecktor/model/ 40 | python predict.py -p hecktor/config/model_predict.yaml 41 | ``` 42 | 43 | ### Model weights 44 | To reproduce results presented in the paper on different train / validation folds, one must download and save pretrained weights in the folder `hecktor/model/weights/`. Weights of a single model are stored in files named `{split}_best_model_weights.pt`. IDs of the patients in the train / validation folds for each data split are stored in `train_val_{split}.pkl` files located in the folder `hecktor/src/data/splits/`. 45 | 46 | In order to download weights of all pretrained model (eight models in total) built on the different train / validation, use the following [link](https://www.dropbox.com/sh/kkvqwn0bpnt1ynk/AABGNdpzTSiIKjiGV5K2ta0Na?dl=0). 47 | 48 | 49 | ### Example 50 | ![img](https://drive.google.com/uc?export=view&id=1U5ifCqqWMKV65wvv1x2BWAKMvMZS9Ywt) 51 | 52 | 53 | ### Paper 54 | If you use this code in you research, please cite the following paper ([arXiv](https://arxiv.org/abs/2102.10446)): 55 | > Iantsen A., Visvikis D., Hatt M. (2021) Squeeze-and-Excitation Normalization for Automated Delineation of Head and Neck Primary Tumors in Combined PET and CT Images. In: Andrearczyk V., Oreiller V., Depeursinge A. (eds) Head and Neck Tumor Segmentation. HECKTOR 2020. Lecture Notes in Computer Science, vol 12603. Springer, Cham. https://doi.org/10.1007/978-3-030-67194-5_4 56 | -------------------------------------------------------------------------------- /model/train.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import argparse 3 | import yaml 4 | import pathlib 5 | 6 | import torch 7 | from torch.utils.data import DataLoader 8 | torch.backends.cudnn.benchmark = True 9 | 10 | sys.path.append('../src/') 11 | sys.path.append('../src/data/') 12 | import dataset 13 | import transforms 14 | import losses 15 | import metrics 16 | import trainer 17 | import models 18 | 19 | import utils 20 | 21 | 22 | def main(args): 23 | path_to_config = pathlib.Path(args.path) 24 | with open(path_to_config) as f: 25 | config = yaml.safe_load(f) 26 | 27 | # read config: 28 | path_to_data = pathlib.Path(config['path_to_data']) 29 | path_to_pkl = pathlib.Path(config['path_to_pkl']) 30 | path_to_save_dir = pathlib.Path(config['path_to_save_dir']) 31 | 32 | train_batch_size = int(config['train_batch_size']) 33 | val_batch_size = int(config['val_batch_size']) 34 | num_workers = int(config['num_workers']) 35 | lr = float(config['lr']) 36 | n_epochs = int(config['n_epochs']) 37 | n_cls = int(config['n_cls']) 38 | in_channels = int(config['in_channels']) 39 | n_filters = int(config['n_filters']) 40 | reduction = int(config['reduction']) 41 | T_0 = int(config['T_0']) 42 | eta_min = float(config['eta_min']) 43 | baseline = config['baseline'] 44 | 45 | # train and val data paths: 46 | all_paths = utils.get_paths_to_patient_files(path_to_imgs=path_to_data, append_mask=True) 47 | train_paths, val_paths = utils.get_train_val_paths(all_paths=all_paths, path_to_train_val_pkl=path_to_pkl) 48 | train_paths = train_paths[:2] 49 | val_paths = val_paths[:2] 50 | 51 | # train and val data transforms: 52 | train_transforms = transforms.Compose([ 53 | transforms.RandomRotation(p=0.5, angle_range=[0, 45]), 54 | transforms.Mirroring(p=0.5), 55 | transforms.NormalizeIntensity(), 56 | transforms.ToTensor() 57 | ]) 58 | 59 | val_transforms = transforms.Compose([ 60 | transforms.NormalizeIntensity(), 61 | transforms.ToTensor() 62 | ]) 63 | 64 | # datasets: 65 | train_set = dataset.HecktorDataset(train_paths, transforms=train_transforms) 66 | val_set = dataset.HecktorDataset(val_paths, transforms=val_transforms) 67 | 68 | # dataloaders: 69 | train_loader = DataLoader(train_set, batch_size=train_batch_size, shuffle=True, num_workers=num_workers) 70 | val_loader = DataLoader(val_set, batch_size=val_batch_size, shuffle=False, num_workers=num_workers) 71 | 72 | dataloaders = { 73 | 'train': train_loader, 74 | 'val': val_loader 75 | } 76 | 77 | if baseline: 78 | model = models.BaselineUNet(in_channels, n_cls, n_filters) 79 | else: 80 | model = models.FastSmoothSENormDeepUNet_supervision_skip_no_drop(in_channels, n_cls, n_filters, reduction) 81 | 82 | criterion = losses.Dice_and_FocalLoss() 83 | optimizer = torch.optim.Adam(model.parameters(), lr=lr, betas=(0.9, 0.99)) 84 | metric = metrics.dice 85 | scheduler = torch.optim.lr_scheduler.CosineAnnealingWarmRestarts(optimizer, T_0=T_0, eta_min=eta_min) 86 | 87 | trainer_ = trainer.ModelTrainer( 88 | model=model, 89 | dataloaders=dataloaders, 90 | criterion=criterion, 91 | optimizer=optimizer, 92 | metric=metric, 93 | scheduler=scheduler, 94 | num_epochs=n_epochs, 95 | parallel=True 96 | ) 97 | 98 | trainer_.train_model() 99 | trainer_.save_results(path_to_dir=path_to_save_dir) 100 | 101 | 102 | if __name__ == "__main__": 103 | parser = argparse.ArgumentParser(description='Model Training Script') 104 | parser.add_argument("-p", "--path", type=str, required=True, help="path to the config file") 105 | args = parser.parse_args() 106 | main(args) 107 | -------------------------------------------------------------------------------- /src/data/make_dataset.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import yaml 3 | import os 4 | import pathlib 5 | 6 | import pandas as pd 7 | import SimpleITK as sitk 8 | from tqdm import tqdm 9 | 10 | from utils import read_nifti, write_nifti, get_attributes, resample_sitk_image 11 | 12 | 13 | def main(args): 14 | path_to_config = pathlib.Path(args.path) 15 | with open(path_to_config) as f: 16 | config = yaml.safe_load(f) 17 | 18 | path_to_input = pathlib.Path(config['path_to_input']) 19 | path_to_bb = pathlib.Path(config['path_to_bb']) 20 | path_to_output = pathlib.Path(config['path_to_output']) 21 | is_mask_available = config['is_mask_available'] 22 | verbose = config['verbose'] 23 | 24 | bb = pd.read_csv(path_to_bb) 25 | patients = list(bb.PatientID) 26 | print(f"Total number of patients: {len(patients)}") 27 | 28 | print(f"Resampled images will be saved in {path_to_output}") 29 | if not os.path.exists(path_to_output): 30 | os.makedirs(path_to_output, exist_ok=True) 31 | 32 | for p in tqdm(patients) if verbose else patients: 33 | # Read images: 34 | img_ct = read_nifti(path_to_input / p / (p + '_ct.nii.gz')) 35 | img_pt = read_nifti(path_to_input / p / (p + '_pt.nii.gz')) 36 | if is_mask_available: 37 | mask = read_nifti(path_to_input / p / (p + '_ct_gtvt.nii.gz')) 38 | 39 | # Get bounding boxes: 40 | pt1 = bb.loc[bb.PatientID == p, ['x1', 'y1', 'z1']] 41 | pt2 = bb.loc[bb.PatientID == p, ['x2', 'y2', 'z2']] 42 | pt1, pt2 = tuple(*pt1.values), tuple(*pt2.values) 43 | 44 | # Convert physcial points into array indexes: 45 | pt1_ct = img_ct.TransformPhysicalPointToIndex(pt1) 46 | pt1_pt = img_pt.TransformPhysicalPointToIndex(pt1) 47 | if is_mask_available: 48 | pt1_mask = mask.TransformPhysicalPointToIndex(pt1) 49 | 50 | pt2_ct = img_ct.TransformPhysicalPointToIndex(pt2) 51 | pt2_pt = img_pt.TransformPhysicalPointToIndex(pt2) 52 | if is_mask_available: 53 | pt2_mask = mask.TransformPhysicalPointToIndex(pt2) 54 | 55 | # Exctract the patch: 56 | cr_img_ct = img_ct[pt1_ct[0]: pt2_ct[0], pt1_ct[1]: pt2_ct[1], pt1_ct[2]: pt2_ct[2]] 57 | cr_img_pt = img_pt[pt1_pt[0]: pt2_pt[0], pt1_pt[1]: pt2_pt[1], pt1_pt[2]: pt2_pt[2]] 58 | if is_mask_available: 59 | cr_mask = mask[pt1_mask[0]: pt2_mask[0], pt1_mask[1]: pt2_mask[1], pt1_mask[2]: pt2_mask[2]] 60 | 61 | # Resample all images using CT attributes: 62 | # CT: 63 | cr_img_ct = resample_sitk_image( 64 | cr_img_ct, 65 | new_spacing=[1, 1, 1], 66 | new_size=[144, 144, 144], 67 | interpolator=sitk.sitkLinear) 68 | target_size = list(cr_img_ct.GetSize()) 69 | attributes = get_attributes(cr_img_ct) 70 | 71 | # PT: 72 | cr_img_pt = resample_sitk_image( 73 | cr_img_pt, 74 | new_spacing=[1, 1, 1], 75 | new_size=target_size, 76 | attributes=attributes, 77 | interpolator=sitk.sitkLinear 78 | ) 79 | 80 | # Mask: 81 | if is_mask_available: 82 | cr_mask = resample_sitk_image( 83 | cr_mask, 84 | new_spacing=[1, 1, 1], 85 | new_size=target_size, 86 | attributes=attributes, 87 | interpolator=sitk.sitkNearestNeighbor 88 | ) 89 | 90 | # Save resampled images: 91 | if not os.path.exists(path_to_output / p): 92 | os.makedirs(path_to_output / p, exist_ok=True) 93 | 94 | write_nifti(cr_img_ct, path_to_output / p / (p + '_ct.nii.gz')) 95 | write_nifti(cr_img_pt, path_to_output / p / (p + '_pt.nii.gz')) 96 | if is_mask_available: 97 | write_nifti(cr_mask, path_to_output / p / (p + '_ct_gtvt.nii.gz')) 98 | 99 | 100 | if __name__ == "__main__": 101 | parser = argparse.ArgumentParser(description='Data Preprocessing Script') 102 | parser.add_argument("-p", "--path", type=str, required=True, help="path to the config file") 103 | args = parser.parse_args() 104 | main(args) 105 | -------------------------------------------------------------------------------- /src/predictor.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | import os 3 | import nibabel as nib 4 | import torch 5 | 6 | 7 | class Predictor: 8 | """ 9 | A class for building a model predictions. 10 | 11 | Parameters 12 | ---------- 13 | model : a subclass of `torch.nn.Module` 14 | A model used for prediction. 15 | path_to_model_weights : list of (`pathlib.Path` or str) or (`pathlib.Path` or str) 16 | A path to model weights. Provide a path and use `self.predict` to build predictions using a single model. 17 | Use a list of paths and `self.ensemble_predict` to get predictions for an ensemble (the same architecture but 18 | different weights). 19 | dataloaders : `torch.utils.data.DataLoader` 20 | A dataloader fetching test samples. 21 | output_transforms 22 | Transforms applied to outputs. 23 | path_to_save_dir : `pathlib.Path` or str 24 | A path to a directory to save predictions 25 | """ 26 | 27 | def __init__(self, 28 | model, 29 | path_to_model_weights, # list of paths or path 30 | dataloader, 31 | output_transforms=None, 32 | path_to_save_dir='.', 33 | device="cuda:0"): 34 | 35 | self.model = model 36 | self.path_to_model_weights = [pathlib.Path(p) for p in path_to_model_weights] \ 37 | if isinstance(path_to_model_weights, list) else pathlib.Path(path_to_model_weights) 38 | 39 | self.dataloader = dataloader 40 | self.output_transforms = output_transforms 41 | self.path_to_save_dir = pathlib.Path(path_to_save_dir) 42 | self.device = torch.device(device if torch.cuda.is_available() else "cpu") 43 | 44 | def predict(self): 45 | """Run inference for an single model""" 46 | 47 | if self.device.type == 'cpu': 48 | print(f'Run inference for a model on CPU') 49 | else: 50 | print(f'Run inference for a model' 51 | f' on {torch.cuda.get_device_name(torch.cuda.current_device())}') 52 | 53 | # Check if the directory exists: 54 | if not os.path.exists(self.path_to_save_dir): 55 | os.makedirs(self.path_to_save_dir, exist_ok=True) 56 | 57 | # Send model to device: 58 | self.model = self.model.to(self.device) 59 | self.model.eval() 60 | 61 | # Load model weights: 62 | self.model = self._load_model_weights(self.model, self.path_to_model_weights) 63 | 64 | # Inference: 65 | with torch.no_grad(): 66 | for sample in self.dataloader: 67 | input = sample['input'] 68 | input = input.to(self.device) 69 | 70 | output = self.model(input) 71 | output = output.cpu() 72 | 73 | sample['output'] = output 74 | 75 | # apply output transforms, if any: 76 | if self.output_transforms: 77 | sample = self.output_transforms(sample) 78 | 79 | # Save prediction: 80 | self._save_preds(sample, self.path_to_save_dir) 81 | 82 | print(f'Predictions have been saved in {self.path_to_save_dir}') 83 | 84 | def ensemble_predict(self): 85 | """Run inference for an ensemble of models""" 86 | 87 | if self.device.type == 'cpu': 88 | print(f'Run inference for an ensemble of {len(self.path_to_model_weights)} models on CPU') 89 | else: 90 | print(f'Run inference for an ensemble of {len(self.path_to_model_weights)} models' 91 | f' on {torch.cuda.get_device_name(torch.cuda.current_device())}') 92 | 93 | # Check if the directory exists: 94 | if not os.path.exists(self.path_to_save_dir): 95 | os.makedirs(self.path_to_save_dir, exist_ok=True) 96 | 97 | # Send model to device: 98 | self.model = self.model.to(self.device) 99 | self.model.eval() 100 | 101 | # Inference: 102 | with torch.no_grad(): 103 | for sample in self.dataloader: 104 | input = sample['input'] 105 | input = input.to(self.device) 106 | 107 | ensemble_output = 0 108 | for path in self.path_to_model_weights: 109 | self.model = self._load_model_weights(self.model, path) 110 | output = self.model(input) 111 | output = output.cpu() 112 | 113 | ensemble_output += output 114 | 115 | ensemble_output /= len(self.path_to_model_weights) 116 | sample['output'] = ensemble_output 117 | 118 | # apply (ensemble) output transforms, if any: 119 | if self.output_transforms: 120 | sample = self.output_transforms(sample) 121 | 122 | # Save prediction: 123 | self._save_preds(sample, self.path_to_save_dir) 124 | 125 | print(f'Predictions have been saved in {self.path_to_save_dir}') 126 | 127 | @staticmethod 128 | def _save_preds(sample, path_to_dir): 129 | preds = sample['output'] 130 | sample_id = sample['id'][0] 131 | affine = sample['affine'][0].numpy() 132 | preds = nib.Nifti1Image(preds, affine=affine) 133 | nib.save(preds, str(path_to_dir / (sample_id + '.nii.gz'))) 134 | 135 | @staticmethod 136 | def _load_model_weights(model, path_to_model_weights): 137 | model_state_dict = torch.load(path_to_model_weights, map_location=lambda storage, loc: storage) 138 | try: 139 | model.load_state_dict(model_state_dict, strict=True) 140 | except RuntimeError: 141 | # if model was trained in parallel 142 | from collections import OrderedDict 143 | new_model_state_dict = OrderedDict() 144 | for k, v in model_state_dict.items(): 145 | k = k.replace('module.', '') 146 | new_model_state_dict[k] = v 147 | model.load_state_dict(new_model_state_dict, strict=True) 148 | return model 149 | -------------------------------------------------------------------------------- /src/data/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pathlib 3 | import json 4 | import numpy as np 5 | import SimpleITK as sitk 6 | import torch 7 | from torch.nn import functional as F 8 | 9 | 10 | def get_paths_to_patient_files(path_to_imgs, append_mask=True): 11 | """ 12 | Get paths to all data samples, i.e., CT & PET images (and a mask) for each patient. 13 | 14 | Parameters 15 | ---------- 16 | path_to_imgs : str 17 | A path to a directory with patients' data. Each folder in the directory must corresponds to a single patient. 18 | append_mask : bool 19 | Used to append a path to a ground truth mask. 20 | 21 | Returns 22 | ------- 23 | list of tuple 24 | A list wherein each element is a tuple with two (three) `pathlib.Path` objects for a single patient. 25 | The first one is the path to the CT image, the second one - to the PET image. If `append_mask` is True, 26 | the path to the ground truth mask is added. 27 | """ 28 | path_to_imgs = pathlib.Path(path_to_imgs) 29 | 30 | patients = [p for p in os.listdir(path_to_imgs) if os.path.isdir(path_to_imgs / p)] 31 | paths = [] 32 | for p in patients: 33 | path_to_ct = path_to_imgs / p / (p + '_ct.nii.gz') 34 | path_to_pt = path_to_imgs / p / (p + '_pt.nii.gz') 35 | 36 | if append_mask: 37 | path_to_mask = path_to_imgs / p / (p + '_ct_gtvt.nii.gz') 38 | paths.append((path_to_ct, path_to_pt, path_to_mask)) 39 | else: 40 | paths.append((path_to_ct, path_to_pt)) 41 | return paths 42 | 43 | 44 | def get_train_val_paths(all_paths, path_to_train_val_pkl): 45 | """" 46 | Split a list of all paths to patients' data into train & validation parts using patients' IDs. 47 | 48 | Parameters 49 | ---------- 50 | all_paths: list 51 | An output of `get_paths_to_patient_files`. 52 | path_to_train_val_pkl: str 53 | A path to a pkl file storing train & validation IDs. 54 | 55 | Returns 56 | ------- 57 | (list, list) 58 | Two lists of paths to train & validation data samples. 59 | """ 60 | path_to_train_val_pkl = pathlib.Path(path_to_train_val_pkl) 61 | with open(path_to_train_val_pkl) as f: 62 | train_val_split = json.load(f) 63 | 64 | train_paths = [path for path in all_paths 65 | if any(patient_id + '_ct.nii.gz' in str(path[0]) for patient_id in train_val_split['train'])] 66 | 67 | val_paths = [path for path in all_paths 68 | if any(patient_id + '_ct.nii.gz' in str(path[0]) for patient_id in train_val_split['val'])] 69 | 70 | return train_paths, val_paths 71 | 72 | 73 | def read_nifti(path): 74 | """Read a NIfTI image. Return a SimpleITK Image.""" 75 | nifti = sitk.ReadImage(str(path)) 76 | return nifti 77 | 78 | 79 | def write_nifti(sitk_img, path): 80 | """Save a SimpleITK Image to disk in NIfTI format.""" 81 | writer = sitk.ImageFileWriter() 82 | writer.SetImageIO("NiftiImageIO") 83 | writer.SetFileName(str(path)) 84 | writer.Execute(sitk_img) 85 | 86 | 87 | def get_attributes(sitk_image): 88 | """Get physical space attributes (meta-data) of the image.""" 89 | attributes = {} 90 | attributes['orig_pixelid'] = sitk_image.GetPixelIDValue() 91 | attributes['orig_origin'] = sitk_image.GetOrigin() 92 | attributes['orig_direction'] = sitk_image.GetDirection() 93 | attributes['orig_spacing'] = np.array(sitk_image.GetSpacing()) 94 | attributes['orig_size'] = np.array(sitk_image.GetSize(), dtype=np.int) 95 | return attributes 96 | 97 | 98 | def resample_sitk_image(sitk_image, 99 | new_spacing=[1, 1, 1], 100 | new_size=None, 101 | attributes=None, 102 | interpolator=sitk.sitkLinear, 103 | fill_value=0): 104 | """ 105 | Resample a SimpleITK Image. 106 | 107 | Parameters 108 | ---------- 109 | sitk_image : sitk.Image 110 | An input image. 111 | new_spacing : list of int 112 | A distance between adjacent voxels in each dimension given in physical units (mm) for the output image. 113 | new_size : list of int or None 114 | A number of pixels per dimension of the output image. If None, `new_size` is computed based on the original 115 | input size, original spacing and new spacing. 116 | attributes : dict or None 117 | The desired output image's spatial domain (its meta-data). If None, the original image's meta-data is used. 118 | interpolator 119 | Available interpolators: 120 | - sitk.sitkNearestNeighbor : nearest 121 | - sitk.sitkLinear : linear 122 | - sitk.sitkGaussian : gaussian 123 | - sitk.sitkLabelGaussian : label_gaussian 124 | - sitk.sitkBSpline : bspline 125 | - sitk.sitkHammingWindowedSinc : hamming_sinc 126 | - sitk.sitkCosineWindowedSinc : cosine_windowed_sinc 127 | - sitk.sitkWelchWindowedSinc : welch_windowed_sinc 128 | - sitk.sitkLanczosWindowedSinc : lanczos_windowed_sinc 129 | fill_value : int or float 130 | A value used for padding, if the output image size is less than `new_size`. 131 | 132 | Returns 133 | ------- 134 | sitk.Image 135 | The resampled image. 136 | 137 | Notes 138 | ----- 139 | This implementation is based on https://github.com/deepmedic/SimpleITK-examples/blob/master/examples/resample_isotropically.py 140 | """ 141 | sitk_interpolator = interpolator 142 | 143 | # provided attributes: 144 | if attributes: 145 | orig_pixelid = attributes['orig_pixelid'] 146 | orig_origin = attributes['orig_origin'] 147 | orig_direction = attributes['orig_direction'] 148 | orig_spacing = attributes['orig_spacing'] 149 | orig_size = attributes['orig_size'] 150 | 151 | else: 152 | # use original attributes: 153 | orig_pixelid = sitk_image.GetPixelIDValue() 154 | orig_origin = sitk_image.GetOrigin() 155 | orig_direction = sitk_image.GetDirection() 156 | orig_spacing = np.array(sitk_image.GetSpacing()) 157 | orig_size = np.array(sitk_image.GetSize(), dtype=np.int) 158 | 159 | # new image size: 160 | if not new_size: 161 | new_size = orig_size * (orig_spacing / new_spacing) 162 | new_size = np.ceil(new_size).astype(np.int) # Image dimensions are in integers 163 | new_size = [int(s) for s in new_size] # SimpleITK expects lists, not ndarrays 164 | 165 | resample_filter = sitk.ResampleImageFilter() 166 | resampled_sitk_image = resample_filter.Execute(sitk_image, 167 | new_size, 168 | sitk.Transform(), 169 | sitk_interpolator, 170 | orig_origin, 171 | new_spacing, 172 | orig_direction, 173 | fill_value, 174 | orig_pixelid) 175 | 176 | return resampled_sitk_image 177 | -------------------------------------------------------------------------------- /src/models.py: -------------------------------------------------------------------------------- 1 | import torch 2 | from torch import nn 3 | from torch.nn import functional as F 4 | 5 | from layers import BasicConv3d, FastSmoothSeNormConv3d, RESseNormConv3d, UpConv 6 | 7 | 8 | class BaselineUNet(nn.Module): 9 | def __init__(self, in_channels, n_cls, n_filters): 10 | super(BaselineUNet, self).__init__() 11 | self.in_channels = in_channels 12 | self.n_cls = 1 if n_cls == 2 else n_cls 13 | self.n_filters = n_filters 14 | 15 | self.block_1_1_left = BasicConv3d(in_channels, n_filters, kernel_size=3, stride=1, padding=1) 16 | self.block_1_2_left = BasicConv3d(n_filters, n_filters, kernel_size=3, stride=1, padding=1) 17 | 18 | self.pool_1 = nn.MaxPool3d(kernel_size=2, stride=2) # 64, 1/2 19 | self.block_2_1_left = BasicConv3d(n_filters, 2 * n_filters, kernel_size=3, stride=1, padding=1) 20 | self.block_2_2_left = BasicConv3d(2 * n_filters, 2 * n_filters, kernel_size=3, stride=1, padding=1) 21 | 22 | self.pool_2 = nn.MaxPool3d(kernel_size=2, stride=2) # 128, 1/4 23 | self.block_3_1_left = BasicConv3d(2 * n_filters, 4 * n_filters, kernel_size=3, stride=1, padding=1) 24 | self.block_3_2_left = BasicConv3d(4 * n_filters, 4 * n_filters, kernel_size=3, stride=1, padding=1) 25 | 26 | self.pool_3 = nn.MaxPool3d(kernel_size=2, stride=2) # 256, 1/8 27 | self.block_4_1_left = BasicConv3d(4 * n_filters, 8 * n_filters, kernel_size=3, stride=1, padding=1) 28 | self.block_4_2_left = BasicConv3d(8 * n_filters, 8 * n_filters, kernel_size=3, stride=1, padding=1) 29 | 30 | self.upconv_3 = nn.ConvTranspose3d(8 * n_filters, 4 * n_filters, kernel_size=3, stride=2, padding=1, output_padding=1) 31 | self.block_3_1_right = BasicConv3d((4 + 4) * n_filters, 4 * n_filters, kernel_size=3, stride=1, padding=1) 32 | self.block_3_2_right = BasicConv3d(4 * n_filters, 4 * n_filters, kernel_size=3, stride=1, padding=1) 33 | 34 | self.upconv_2 = nn.ConvTranspose3d(4 * n_filters, 2 * n_filters, kernel_size=3, stride=2, padding=1, output_padding=1) 35 | self.block_2_1_right = BasicConv3d((2 + 2) * n_filters, 2 * n_filters, kernel_size=3, stride=1, padding=1) 36 | self.block_2_2_right = BasicConv3d(2 * n_filters, 2 * n_filters, kernel_size=3, stride=1, padding=1) 37 | 38 | self.upconv_1 = nn.ConvTranspose3d(2 * n_filters, n_filters, kernel_size=3, stride=2, padding=1, output_padding=1) 39 | self.block_1_1_right = BasicConv3d((1 + 1) * n_filters, n_filters, kernel_size=3, stride=1, padding=1) 40 | self.block_1_2_right = BasicConv3d(n_filters, n_filters, kernel_size=3, stride=1, padding=1) 41 | 42 | self.conv1x1 = nn.Conv3d(n_filters, self.n_cls, kernel_size=1, stride=1, padding=0) 43 | 44 | def forward(self, x): 45 | 46 | ds0 = self.block_1_2_left(self.block_1_1_left(x)) 47 | ds1 = self.block_2_2_left(self.block_2_1_left(self.pool_1(ds0))) 48 | ds2 = self.block_3_2_left(self.block_3_1_left(self.pool_2(ds1))) 49 | x = self.block_4_2_left(self.block_4_1_left(self.pool_3(ds2))) 50 | 51 | x = self.block_3_2_right(self.block_3_1_right(torch.cat([self.upconv_3(x), ds2], 1))) 52 | x = self.block_2_2_right(self.block_2_1_right(torch.cat([self.upconv_2(x), ds1], 1))) 53 | x = self.block_1_2_right(self.block_1_1_right(torch.cat([self.upconv_1(x), ds0], 1))) 54 | 55 | x = self.conv1x1(x) 56 | 57 | if self.n_cls == 1: 58 | return torch.sigmoid(x) 59 | else: 60 | return F.softmax(x, dim=1) 61 | 62 | 63 | class FastSmoothSENormDeepUNet_supervision_skip_no_drop(nn.Module): 64 | """The model presented in the paper. This model is one of the multiple models that we tried in our experiments 65 | that it why it has such an awkward name.""" 66 | 67 | def __init__(self, in_channels, n_cls, n_filters, reduction=2, return_logits=False): 68 | super(FastSmoothSENormDeepUNet_supervision_skip_no_drop, self).__init__() 69 | self.in_channels = in_channels 70 | self.n_cls = 1 if n_cls == 2 else n_cls 71 | self.n_filters = n_filters 72 | self.return_logits = return_logits 73 | 74 | self.block_1_1_left = RESseNormConv3d(in_channels, n_filters, reduction, kernel_size=7, stride=1, padding=3) 75 | self.block_1_2_left = RESseNormConv3d(n_filters, n_filters, reduction, kernel_size=3, stride=1, padding=1) 76 | 77 | self.pool_1 = nn.MaxPool3d(kernel_size=2, stride=2) 78 | self.block_2_1_left = RESseNormConv3d(n_filters, 2 * n_filters, reduction, kernel_size=3, stride=1, padding=1) 79 | self.block_2_2_left = RESseNormConv3d(2 * n_filters, 2 * n_filters, reduction, kernel_size=3, stride=1, padding=1) 80 | self.block_2_3_left = RESseNormConv3d(2 * n_filters, 2 * n_filters, reduction, kernel_size=3, stride=1, padding=1) 81 | 82 | self.pool_2 = nn.MaxPool3d(kernel_size=2, stride=2) 83 | self.block_3_1_left = RESseNormConv3d(2 * n_filters, 4 * n_filters, reduction, kernel_size=3, stride=1, padding=1) 84 | self.block_3_2_left = RESseNormConv3d(4 * n_filters, 4 * n_filters, reduction, kernel_size=3, stride=1, padding=1) 85 | self.block_3_3_left = RESseNormConv3d(4 * n_filters, 4 * n_filters, reduction, kernel_size=3, stride=1, padding=1) 86 | 87 | self.pool_3 = nn.MaxPool3d(kernel_size=2, stride=2) 88 | self.block_4_1_left = RESseNormConv3d(4 * n_filters, 8 * n_filters, reduction, kernel_size=3, stride=1, padding=1) 89 | self.block_4_2_left = RESseNormConv3d(8 * n_filters, 8 * n_filters, reduction, kernel_size=3, stride=1, padding=1) 90 | self.block_4_3_left = RESseNormConv3d(8 * n_filters, 8 * n_filters, reduction, kernel_size=3, stride=1, padding=1) 91 | 92 | self.pool_4 = nn.MaxPool3d(kernel_size=2, stride=2) 93 | self.block_5_1_left = RESseNormConv3d(8 * n_filters, 16 * n_filters, reduction, kernel_size=3, stride=1, padding=1) 94 | self.block_5_2_left = RESseNormConv3d(16 * n_filters, 16 * n_filters, reduction, kernel_size=3, stride=1, padding=1) 95 | self.block_5_3_left = RESseNormConv3d(16 * n_filters, 16 * n_filters, reduction, kernel_size=3, stride=1, padding=1) 96 | 97 | self.upconv_4 = nn.ConvTranspose3d(16 * n_filters, 8 * n_filters, kernel_size=3, stride=2, padding=1, output_padding=1) 98 | self.block_4_1_right = FastSmoothSeNormConv3d((8 + 8) * n_filters, 8 * n_filters, reduction, kernel_size=3, stride=1, padding=1) 99 | self.block_4_2_right = FastSmoothSeNormConv3d(8 * n_filters, 8 * n_filters, reduction, kernel_size=3, stride=1, padding=1) 100 | self.vision_4 = UpConv(8 * n_filters, n_filters, reduction, scale=8) 101 | 102 | self.upconv_3 = nn.ConvTranspose3d(8 * n_filters, 4 * n_filters, kernel_size=3, stride=2, padding=1, output_padding=1) 103 | self.block_3_1_right = FastSmoothSeNormConv3d((4 + 4) * n_filters, 4 * n_filters, reduction, kernel_size=3, stride=1, padding=1) 104 | self.block_3_2_right = FastSmoothSeNormConv3d(4 * n_filters, 4 * n_filters, reduction, kernel_size=3, stride=1, padding=1) 105 | self.vision_3 = UpConv(4 * n_filters, n_filters, reduction, scale=4) 106 | 107 | self.upconv_2 = nn.ConvTranspose3d(4 * n_filters, 2 * n_filters, kernel_size=3, stride=2, padding=1, output_padding=1) 108 | self.block_2_1_right = FastSmoothSeNormConv3d((2 + 2) * n_filters, 2 * n_filters, reduction, kernel_size=3, stride=1, padding=1) 109 | self.block_2_2_right = FastSmoothSeNormConv3d(2 * n_filters, 2 * n_filters, reduction, kernel_size=3, stride=1, padding=1) 110 | self.vision_2 = UpConv(2 * n_filters, n_filters, reduction, scale=2) 111 | 112 | self.upconv_1 = nn.ConvTranspose3d(2 * n_filters, 1 * n_filters, kernel_size=3, stride=2, padding=1, output_padding=1) 113 | self.block_1_1_right = FastSmoothSeNormConv3d((1 + 1) * n_filters, n_filters, reduction, kernel_size=3, stride=1, padding=1) 114 | self.block_1_2_right = FastSmoothSeNormConv3d(n_filters, n_filters, reduction, kernel_size=3, stride=1, padding=1) 115 | 116 | self.conv1x1 = nn.Conv3d(1 * n_filters, self.n_cls, kernel_size=1, stride=1, padding=0) 117 | 118 | def forward(self, x): 119 | 120 | ds0 = self.block_1_2_left(self.block_1_1_left(x)) 121 | ds1 = self.block_2_3_left(self.block_2_2_left(self.block_2_1_left(self.pool_1(ds0)))) 122 | ds2 = self.block_3_3_left(self.block_3_2_left(self.block_3_1_left(self.pool_2(ds1)))) 123 | ds3 = self.block_4_3_left(self.block_4_2_left(self.block_4_1_left(self.pool_3(ds2)))) 124 | x = self.block_5_3_left(self.block_5_2_left(self.block_5_1_left(self.pool_4(ds3)))) 125 | 126 | x = self.block_4_2_right(self.block_4_1_right(torch.cat([self.upconv_4(x), ds3], 1))) 127 | sv4 = self.vision_4(x) 128 | 129 | x = self.block_3_2_right(self.block_3_1_right(torch.cat([self.upconv_3(x), ds2], 1))) 130 | sv3 = self.vision_3(x) 131 | 132 | x = self.block_2_2_right(self.block_2_1_right(torch.cat([self.upconv_2(x), ds1], 1))) 133 | sv2 = self.vision_2(x) 134 | 135 | x = self.block_1_1_right(torch.cat([self.upconv_1(x), ds0], 1)) 136 | x = x + sv4 + sv3 + sv2 137 | x = self.block_1_2_right(x) 138 | 139 | x = self.conv1x1(x) 140 | 141 | if self.return_logits: 142 | return x 143 | else: 144 | if self.n_cls == 1: 145 | return torch.sigmoid(x) 146 | else: 147 | return F.softmax(x, dim=1) 148 | -------------------------------------------------------------------------------- /src/transforms.py: -------------------------------------------------------------------------------- 1 | import random 2 | import numpy as np 3 | import torch 4 | 5 | from skimage.transform import rotate 6 | 7 | 8 | class Compose: 9 | def __init__(self, transforms=None): 10 | self.transforms = transforms 11 | 12 | def __call__(self, sample): 13 | for transform in self.transforms: 14 | sample = transform(sample) 15 | 16 | return sample 17 | 18 | 19 | class ToTensor: 20 | def __init__(self, mode='train'): 21 | if mode not in ['train', 'test']: 22 | raise ValueError(f"Argument 'mode' must be 'train' or 'test'. Received {mode}") 23 | self.mode = mode 24 | 25 | def __call__(self, sample): 26 | if self.mode == 'train': 27 | img, mask = sample['input'], sample['target'] 28 | img = np.transpose(img, axes=[3, 0, 1, 2]) 29 | mask = np.transpose(mask, axes=[3, 0, 1, 2]) 30 | img = torch.from_numpy(img).float() 31 | mask = torch.from_numpy(mask).float() 32 | sample['input'], sample['target'] = img, mask 33 | 34 | else: # if self.mode == 'test' 35 | img = sample['input'] 36 | img = np.transpose(img, axes=[3, 0, 1, 2]) 37 | img = torch.from_numpy(img).float() 38 | sample['input'] = img 39 | 40 | return sample 41 | 42 | 43 | class Mirroring: 44 | def __init__(self, p=0.5): 45 | self.p = p 46 | 47 | def __call__(self, sample): 48 | if random.random() < self.p: 49 | img, mask = sample['input'], sample['target'] 50 | 51 | n_axes = random.randint(0, 3) 52 | random_axes = random.sample(range(3), n_axes) 53 | 54 | img = np.flip(img, axis=tuple(random_axes)) 55 | mask = np.flip(mask, axis=tuple(random_axes)) 56 | 57 | sample['input'], sample['target'] = img.copy(), mask.copy() 58 | 59 | return sample 60 | 61 | 62 | class NormalizeIntensity: 63 | 64 | def __call__(self, sample): 65 | img = sample['input'] 66 | img[:, :, :, 0] = self.normalize_ct(img[:, :, :, 0]) 67 | img[:, :, :, 1] = self.normalize_pt(img[:, :, :, 1]) 68 | 69 | sample['input'] = img 70 | return sample 71 | 72 | @staticmethod 73 | def normalize_ct(img): 74 | norm_img = np.clip(img, -1024, 1024) / 1024 75 | return norm_img 76 | 77 | @staticmethod 78 | def normalize_pt(img): 79 | mean = np.mean(img) 80 | std = np.std(img) 81 | return (img - mean) / (std + 1e-3) 82 | 83 | 84 | class RandomRotation: 85 | def __init__(self, p=0.5, angle_range=[5, 15]): 86 | self.p = p 87 | self.angle_range = angle_range 88 | 89 | def __call__(self, sample): 90 | if random.random() < self.p: 91 | img, mask = sample['input'], sample['target'] 92 | 93 | num_of_seqs = img.shape[-1] 94 | n_axes = random.randint(1, 3) 95 | random_axes = random.sample([0, 1, 2], n_axes) 96 | 97 | for axis in random_axes: 98 | 99 | angle = random.randrange(*self.angle_range) 100 | angle = -angle if random.random() < 0.5 else angle 101 | 102 | for i in range(num_of_seqs): 103 | img[:, :, :, i] = RandomRotation.rotate_3d_along_axis(img[:, :, :, i], angle, axis, 1) 104 | 105 | mask[:, :, :, 0] = RandomRotation.rotate_3d_along_axis(mask[:, :, :, 0], angle, axis, 0) 106 | 107 | sample['input'], sample['target'] = img, mask 108 | return sample 109 | 110 | @staticmethod 111 | def rotate_3d_along_axis(img, angle, axis, order): 112 | 113 | if axis == 0: 114 | rot_img = rotate(img, angle, order=order, preserve_range=True) 115 | 116 | if axis == 1: 117 | rot_img = np.transpose(img, axes=(1, 2, 0)) 118 | rot_img = rotate(rot_img, angle, order=order, preserve_range=True) 119 | rot_img = np.transpose(rot_img, axes=(2, 0, 1)) 120 | 121 | if axis == 2: 122 | rot_img = np.transpose(img, axes=(2, 0, 1)) 123 | rot_img = rotate(rot_img, angle, order=order, preserve_range=True) 124 | rot_img = np.transpose(rot_img, axes=(1, 2, 0)) 125 | 126 | return rot_img 127 | 128 | 129 | class ZeroPadding: 130 | 131 | def __init__(self, target_shape, mode='train'): 132 | self.target_shape = np.array(target_shape) # without channel dimension 133 | if mode not in ['train', 'test']: 134 | raise ValueError(f"Argument 'mode' must be 'train' or 'test'. Received {mode}") 135 | self.mode = mode 136 | 137 | def __call__(self, sample): 138 | if self.mode == 'train': 139 | img, mask = sample['input'], sample['target'] 140 | 141 | input_shape = np.array(img.shape[:-1]) # last (channel) dimension is ignored 142 | d_x, d_y, d_z = self.target_shape - input_shape 143 | d_x, d_y, d_z = int(d_x), int(d_y), int(d_z) 144 | 145 | if not all(i == 0 for i in (d_x, d_y, d_z)): 146 | positive = [i if i > 0 else 0 for i in (d_x, d_y, d_z)] 147 | negative = [i if i < 0 else None for i in (d_x, d_y, d_z)] 148 | 149 | # padding for positive values: 150 | img = np.pad(img, ((0, positive[0]), (0, positive[1]), (0, positive[2]), (0, 0)), 'constant', constant_values=(0, 0)) 151 | mask = np.pad(mask, ((0, positive[0]), (0, positive[1]), (0, positive[2]), (0, 0)), 'constant', constant_values=(0, 0)) 152 | 153 | # cropping for negative values: 154 | img = img[: negative[0], : negative[1], : negative[2], :].copy() 155 | mask = mask[: negative[0], : negative[1], : negative[2], :].copy() 156 | 157 | assert img.shape[:-1] == mask.shape[:-1], f'Shape mismatch for the image {img.shape[:-1]} and mask {mask.shape[:-1]}' 158 | 159 | sample['input'], sample['target'] = img, mask 160 | 161 | return sample 162 | 163 | else: # if self.mode == 'test' 164 | img = sample['input'] 165 | 166 | input_shape = np.array(img.shape[:-1]) # last (channel) dimension is ignored 167 | d_x, d_y, d_z = self.target_shape - input_shape 168 | d_x, d_y, d_z = int(d_x), int(d_y), int(d_z) 169 | 170 | if not all(i == 0 for i in (d_x, d_y, d_z)): 171 | positive = [i if i > 0 else 0 for i in (d_x, d_y, d_z)] 172 | negative = [i if i < 0 else None for i in (d_x, d_y, d_z)] 173 | 174 | # padding for positive values: 175 | img = np.pad(img, ((0, positive[0]), (0, positive[1]), (0, positive[2]), (0, 0)), 'constant', constant_values=(0, 0)) 176 | 177 | # cropping for negative values: 178 | img = img[: negative[0], : negative[1], : negative[2], :].copy() 179 | 180 | sample['input'] = img 181 | 182 | return sample 183 | 184 | 185 | class ExtractPatch: 186 | """Extracts a patch of a given size from an image (4D numpy array).""" 187 | 188 | def __init__(self, patch_size, p_tumor=0.5): 189 | self.patch_size = patch_size # without channel dimension! 190 | self.p_tumor = p_tumor # probs to extract a patch with a tumor 191 | 192 | def __call__(self, sample): 193 | img = sample['input'] 194 | mask = sample['target'] 195 | 196 | assert all(x <= y for x, y in zip(self.patch_size, img.shape[:-1])), \ 197 | f"Cannot extract the patch with the shape {self.patch_size} from " \ 198 | f"the image with the shape {img.shape}." 199 | 200 | # patch_size components: 201 | ps_x, ps_y, ps_z = self.patch_size 202 | 203 | if random.random() < self.p_tumor: 204 | # coordinates of the tumor's center: 205 | xs, ys, zs, _ = np.where(mask != 0) 206 | tumor_center_x = np.min(xs) + (np.max(xs) - np.min(xs)) // 2 207 | tumor_center_y = np.min(ys) + (np.max(ys) - np.min(ys)) // 2 208 | tumor_center_z = np.min(zs) + (np.max(zs) - np.min(zs)) // 2 209 | 210 | # compute the origin of the patch: 211 | patch_org_x = random.randint(tumor_center_x - ps_x, tumor_center_x) 212 | patch_org_x = np.clip(patch_org_x, 0, img.shape[0] - ps_x) 213 | 214 | patch_org_y = random.randint(tumor_center_y - ps_y, tumor_center_y) 215 | patch_org_y = np.clip(patch_org_y, 0, img.shape[1] - ps_y) 216 | 217 | patch_org_z = random.randint(tumor_center_z - ps_z, tumor_center_z) 218 | patch_org_z = np.clip(patch_org_z, 0, img.shape[2] - ps_z) 219 | else: 220 | patch_org_x = random.randint(0, img.shape[0] - ps_x) 221 | patch_org_y = random.randint(0, img.shape[1] - ps_y) 222 | patch_org_z = random.randint(0, img.shape[2] - ps_z) 223 | 224 | # extract the patch: 225 | patch_img = img[patch_org_x: patch_org_x + ps_x, 226 | patch_org_y: patch_org_y + ps_y, 227 | patch_org_z: patch_org_z + ps_z, 228 | :].copy() 229 | 230 | patch_mask = mask[patch_org_x: patch_org_x + ps_x, 231 | patch_org_y: patch_org_y + ps_y, 232 | patch_org_z: patch_org_z + ps_z, 233 | :].copy() 234 | 235 | assert patch_img.shape[:-1] == self.patch_size, \ 236 | f"Shape mismatch for the patch with the shape {patch_img.shape[:-1]}, " \ 237 | f"whereas the required shape is {self.patch_size}." 238 | 239 | sample['input'] = patch_img 240 | sample['target'] = patch_mask 241 | 242 | return sample 243 | 244 | 245 | class InverseToTensor: 246 | def __call__(self, sample): 247 | output = sample['output'] 248 | 249 | output = torch.squeeze(output) # squeeze the batch and channel dimensions 250 | output = output.numpy() 251 | 252 | sample['output'] = output 253 | return sample 254 | 255 | 256 | class CheckOutputShape: 257 | def __init__(self, shape=(144, 144, 144)): 258 | self.shape = shape 259 | 260 | def __call__(self, sample): 261 | output = sample['output'] 262 | assert output.shape == self.shape, \ 263 | f'Received wrong output shape. Must be {self.shape}, but received {output.shape}.' 264 | return sample 265 | 266 | 267 | class ProbsToLabels: 268 | def __call__(self, sample): 269 | output = sample['output'] 270 | output = (output > 0.5).astype(int) # get binary label 271 | sample['output'] = output 272 | return sample 273 | -------------------------------------------------------------------------------- /src/trainer.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pathlib 3 | import copy 4 | import numpy as np 5 | import pandas as pd 6 | import matplotlib.pyplot as plt 7 | import torch 8 | 9 | 10 | class ModelTrainer: 11 | """ 12 | A class for fitting a model. 13 | 14 | Parameters 15 | ---------- 16 | model : a subclass of `torch.nn.Module` 17 | A model to fit. 18 | dataloaders : dict of `torch.utils.data.DataLoader` 19 | A dictionary with 'train' and 'val' keys specifying dataloaders for model training and validation. 20 | criterion : a subclass of `torch.nn.Module` 21 | A loss function used for model training. 22 | optimizer : a subclass of `torch.optim.Optimizer` 23 | An optimizer for training a model. 24 | metric : function or a subclass of `torch.nn.Module` 25 | A metric used for evaluation. 26 | mode : str 27 | Must be 'min' or 'max'. If 'max', a model with the highest metric will be treated as the best one. 28 | scheduler : a class from `torch.optim.lr_scheduler` (a subclass of _LRScheduler) 29 | A method to adjust a learning rate during training. 30 | num_epochs : int 31 | A number of epochs for training. 32 | parallel : bool 33 | Train a model on multiple GPUs using `torch.nn.DataParallel`. 34 | cuda_device : str 35 | A CUDA device used for training and validation. If the device is unavailable, 36 | all computation are performed on a CPU. 37 | save_last_model : bool 38 | If 'true', a checkpoint of the last epoch will be saved (for inference and/or resuming training). 39 | scheduler_step_per_epoch : bool 40 | If 'true', a learning rate adjustment is performed after each epoch. Otherwise, after each training batch. 41 | 42 | Attributes 43 | ---------- 44 | learning_curves : dict of dict 45 | A dictionary containing train & validation learning curves (loss and metric). 46 | best_val_epoch : int 47 | Indicates an epoch with the best metric on a validation set. 48 | best_model_wts 49 | Weights of the best model. 50 | checkpoint 51 | The model weights and optimizer state after the last epoch. 52 | """ 53 | 54 | def __init__(self, model, dataloaders, criterion, optimizer, 55 | metric=None, mode='max', scheduler=None, num_epochs=25, 56 | parallel=False, cuda_device="cuda:0", 57 | save_last_model=True, scheduler_step_per_epoch=True): 58 | 59 | self.model = model 60 | self.dataloaders = dataloaders 61 | self.criterion = criterion 62 | self.metric = metric 63 | self.mode = mode 64 | self.optimizer = optimizer 65 | self.scheduler = scheduler 66 | self.num_epochs = num_epochs 67 | self.parallel = parallel 68 | self.device = torch.device(cuda_device if torch.cuda.is_available() else "cpu") 69 | self.save_last_model = save_last_model 70 | self.scheduler_step_per_epoch = scheduler_step_per_epoch 71 | 72 | # Dicts for saving train and val losses: 73 | self.learning_curves = dict() 74 | self.learning_curves['loss'], self.learning_curves['metric'] = dict(), dict() 75 | self.learning_curves['loss']['train'], self.learning_curves['loss']['val'] = [], [] 76 | self.learning_curves['metric']['train'], self.learning_curves['metric']['val'] = [], [] 77 | 78 | # Summary: Best epoch, loss, metric and best model weights: 79 | self.best_val_epoch = 0 80 | self.best_val_loss = float('inf') 81 | if self.mode == 'max': 82 | self.best_val_avg_metric = -float('inf') 83 | else: 84 | self.best_val_avg_metric = float('inf') 85 | self.best_val_metric = 0.0 86 | self.best_model_wts = None 87 | self.checkpoint = None # last model and optimizer weights 88 | 89 | def train_model(self): 90 | """Fit a model.""" 91 | 92 | if self.device.type == 'cpu': 93 | print('Start training the model on CPU') 94 | elif self.parallel and torch.cuda.device_count() > 1: 95 | print(f'Start training the model on {torch.cuda.device_count()} ' 96 | f'{torch.cuda.get_device_name(torch.cuda.current_device())} in parallel') 97 | self.model = torch.nn.DataParallel(self.model) 98 | else: 99 | print(f'Start training the model on {torch.cuda.get_device_name(torch.cuda.current_device())}') 100 | 101 | self.model = self.model.to(self.device) 102 | 103 | for epoch in range(self.num_epochs): 104 | print(f'Epoch {epoch} / {self.num_epochs - 1}') 105 | print('-' * 20) 106 | 107 | # Each epoch has a training and validation phase: 108 | for phase in ['train', 'val']: 109 | if phase == 'train': 110 | self.model.train() # Set model to training mode 111 | else: 112 | self.model.eval() # Set model to evaluate mode 113 | 114 | phase_loss = 0.0 # Train or val loss 115 | phase_metric = 0.0 116 | 117 | # Track history only if in train phase: 118 | with torch.set_grad_enabled(phase == 'train'): 119 | # Iterate over data batches: 120 | batch = 0 121 | for sample in self.dataloaders[phase]: 122 | input, target = sample['input'], sample['target'] 123 | input, target = input.to(self.device), target.to(self.device) 124 | 125 | # Forward pass: 126 | output = self.model(input) 127 | 128 | loss = self.criterion(output, target) 129 | metric = self.metric(output.detach(), target.detach()) 130 | 131 | # Losses and metric: 132 | phase_loss += loss.item() 133 | phase_metric += metric.item() 134 | 135 | with np.printoptions(precision=3, suppress=True): 136 | print(f'batch: {batch} batch loss: {loss:.3f} \tmetric: {metric:.3f}') 137 | 138 | del input, target, output, metric 139 | 140 | # Backward pass + optimize only if in training phase: 141 | if phase == 'train': 142 | loss.backward() 143 | self.optimizer.step() 144 | 145 | # zero the parameter gradients: 146 | self.optimizer.zero_grad() 147 | 148 | if self.scheduler and not self.scheduler_step_per_epoch: 149 | self.scheduler.step() 150 | 151 | del loss 152 | batch += 1 153 | 154 | phase_loss /= len(self.dataloaders[phase]) 155 | phase_metric /= len(self.dataloaders[phase]) 156 | self.learning_curves['loss'][phase].append(phase_loss) 157 | self.learning_curves['metric'][phase].append(phase_metric) 158 | 159 | print(f'{phase.upper()} loss: {phase_loss:.3f} \tavg_metric: {np.mean(phase_metric):.3f}') 160 | 161 | # Save summary if it is the best val results so far: 162 | if phase == 'val': 163 | if self.mode == 'max' and np.mean(phase_metric) > self.best_val_avg_metric: 164 | self.best_val_epoch = epoch 165 | self.best_val_loss = phase_loss 166 | self.best_val_avg_metric = np.mean(phase_metric) 167 | self.best_val_metric = phase_metric 168 | self.best_model_wts = copy.deepcopy(self.model.state_dict()) 169 | 170 | if self.mode == 'min' and np.mean(phase_metric) < self.best_val_avg_metric: 171 | self.best_val_epoch = epoch 172 | self.best_val_loss = phase_loss 173 | self.best_val_avg_metric = np.mean(phase_metric) 174 | self.best_val_metric = phase_metric 175 | self.best_model_wts = copy.deepcopy(self.model.state_dict()) 176 | 177 | # Adjust learning rate after val phase: 178 | if self.scheduler and self.scheduler_step_per_epoch: 179 | if isinstance(self.scheduler, torch.optim.lr_scheduler.ReduceLROnPlateau): 180 | self.scheduler.step(np.mean(phase_metric)) 181 | else: 182 | self.scheduler.step() 183 | 184 | if self.save_last_model: 185 | self.checkpoint = {'model_state_dict': copy.deepcopy(self.model.state_dict()), 186 | 'optimizer_state_dict': copy.deepcopy(self.optimizer.state_dict())} 187 | 188 | def save_results(self, path_to_dir): 189 | """" 190 | Save results in a directory. The method must be used after training. 191 | 192 | A short summary is stored in a csv file ('summary.csv'). Weights of the best model are stored in 193 | 'best_model_weights.pt'. A checkpoint of the last epoch is stored in 'last_model_checkpoint.tar'. Two plots 194 | for the loss function and metric are stored in 'loss_plot.png' and 'metric_plot.png', respectively. 195 | 196 | Parameters 197 | ---------- 198 | path_to_dir : str 199 | A path to the directory for storing all results. 200 | """ 201 | 202 | path_to_dir = pathlib.Path(path_to_dir) 203 | 204 | # Check if the directory exists: 205 | if not os.path.exists(path_to_dir): 206 | os.makedirs(path_to_dir) 207 | 208 | # Write a short summary in a csv file: 209 | with open(path_to_dir / 'summary.csv', 'w', newline='', encoding='utf-8') as summary: 210 | summary.write(f'SUMMARY OF THE EXPERIMENT:\n\n') 211 | summary.write(f'BEST VAL EPOCH: {self.best_val_epoch}\n') 212 | summary.write(f'BEST VAL LOSS: {self.best_val_loss}\n') 213 | summary.write(f'BEST VAL AVG metric: {self.best_val_avg_metric}\n') 214 | summary.write(f'BEST VAL metric: {self.best_val_metric}\n') 215 | 216 | # Save best model weights: 217 | torch.save(self.best_model_wts, path_to_dir / 'best_model_weights.pt') 218 | 219 | # Save last model weights (checkpoint): 220 | if self.save_last_model: 221 | torch.save(self.checkpoint, path_to_dir / 'last_model_checkpoint.tar') 222 | 223 | # Save learning curves as pandas df: 224 | df_learning_curves = pd.DataFrame.from_dict({ 225 | 'loss_train': self.learning_curves['loss']['train'], 226 | 'loss_val': self.learning_curves['loss']['val'], 227 | 'metric_train': self.learning_curves['metric']['train'], 228 | 'metric_val': self.learning_curves['metric']['val'] 229 | }) 230 | df_learning_curves.to_csv(path_to_dir / 'learning_curves.csv', sep=';') 231 | 232 | # Save learning curves' plots in png files: 233 | # Loss figure: 234 | plt.figure(figsize=(17.5, 10)) 235 | plt.plot(range(self.num_epochs), self.learning_curves['loss']['train'], label='train') 236 | plt.plot(range(self.num_epochs), self.learning_curves['loss']['val'], label='val') 237 | plt.xlabel('Epoch', fontsize=20) 238 | plt.ylabel('Loss', fontsize=20) 239 | plt.xticks(fontsize=15) 240 | plt.yticks(fontsize=15) 241 | plt.legend(fontsize=20) 242 | plt.grid() 243 | plt.savefig(path_to_dir / 'loss_plot.png', bbox_inches='tight') 244 | 245 | # metric figure: 246 | train_avg_metric = [np.mean(i) for i in self.learning_curves['metric']['train']] 247 | val_avg_metric = [np.mean(i) for i in self.learning_curves['metric']['val']] 248 | 249 | plt.figure(figsize=(17.5, 10)) 250 | plt.plot(range(self.num_epochs), train_avg_metric, label='train') 251 | plt.plot(range(self.num_epochs), val_avg_metric, label='val') 252 | plt.xlabel('Epoch', fontsize=20) 253 | plt.ylabel('Avg metric', fontsize=20) 254 | plt.xticks(fontsize=15) 255 | plt.yticks(fontsize=15) 256 | plt.legend(fontsize=20) 257 | plt.grid() 258 | plt.savefig(path_to_dir / 'metric_plot.png', bbox_inches='tight') 259 | 260 | print(f'All results have been saved in {path_to_dir}') 261 | -------------------------------------------------------------------------------- /notebooks/make_dataset.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "## Data Preprocessing for the MICCAI 2020 HEad and neCK TumOR segmentation challenge [(HECKTOR)](https://www.aicrowd.com/challenges/miccai-2020-hecktor)" 8 | ] 9 | }, 10 | { 11 | "cell_type": "code", 12 | "execution_count": 1, 13 | "metadata": {}, 14 | "outputs": [], 15 | "source": [ 16 | "import os\n", 17 | "import sys\n", 18 | "import pathlib\n", 19 | "\n", 20 | "import numpy as np\n", 21 | "import pandas as pd\n", 22 | "import SimpleITK as sitk\n", 23 | "from tqdm.notebook import tqdm\n", 24 | "\n", 25 | "import matplotlib.pyplot as plt\n", 26 | "%matplotlib inline\n", 27 | "\n", 28 | "sys.path.append('../')\n", 29 | "from src.data.utils import read_nifti, write_nifti, get_attributes, resample_sitk_image" 30 | ] 31 | }, 32 | { 33 | "cell_type": "markdown", 34 | "metadata": {}, 35 | "source": [ 36 | "### Summary:" 37 | ] 38 | }, 39 | { 40 | "cell_type": "markdown", 41 | "metadata": {}, 42 | "source": [ 43 | "Dataset:\n", 44 | "- Each data sample (patient) consists of PET & CT images and a GTVt (primary Gross Tumor Volume) mask provided in NIfTI format.\n", 45 | "- PET & CT images for a single patient might occupy different regions in physical space, i.e., the images have a different size (number of pixels per dimension), origin, spacing and direction cosine matrix (axis directions in physical space).\n", 46 | "- For each case, a bounding box of the size of 144x144x144 mm is available. Segmentation must be performed within the bounding box. \n", 47 | "\n", 48 | "This notebook shows the way to transform (resample) a pair of PET & CT images for each patient to a common reference space and to extract a region of interest (a bounding box). Transformed images will be saved in NIfTI format.\n", 49 | "\n", 50 | "**From now onward, the train set preprocessing will be demonstrated. For the test set preprocessing, all operations with ground truth labels (segmentation masks) must be omitted.**" 51 | ] 52 | }, 53 | { 54 | "cell_type": "markdown", 55 | "metadata": {}, 56 | "source": [ 57 | "#### Input / Output paths:" 58 | ] 59 | }, 60 | { 61 | "cell_type": "code", 62 | "execution_count": 2, 63 | "metadata": {}, 64 | "outputs": [], 65 | "source": [ 66 | "path_to_input = pathlib.Path('C:/inserm/hecktor/hecktor_train/hecktor_nii/')\n", 67 | "path_to_bb = pathlib.Path('C:/inserm/hecktor/hecktor_train/bbox.csv')\n", 68 | "\n", 69 | "path_to_output = pathlib.Path('C:/inserm/hecktor/hecktor_train/hecktor_nii_resampled/')" 70 | ] 71 | }, 72 | { 73 | "cell_type": "markdown", 74 | "metadata": {}, 75 | "source": [ 76 | "#### Bounding boxes:" 77 | ] 78 | }, 79 | { 80 | "cell_type": "code", 81 | "execution_count": 3, 82 | "metadata": {}, 83 | "outputs": [ 84 | { 85 | "data": { 86 | "text/html": [ 87 | "
\n", 88 | "\n", 101 | "\n", 102 | " \n", 103 | " \n", 104 | " \n", 105 | " \n", 106 | " \n", 107 | " \n", 108 | " \n", 109 | " \n", 110 | " \n", 111 | " \n", 112 | " \n", 113 | " \n", 114 | " \n", 115 | " \n", 116 | " \n", 117 | " \n", 118 | " \n", 119 | " \n", 120 | " \n", 121 | " \n", 122 | " \n", 123 | " \n", 124 | " \n", 125 | " \n", 126 | " \n", 127 | " \n", 128 | " \n", 129 | " \n", 130 | " \n", 131 | " \n", 132 | " \n", 133 | " \n", 134 | " \n", 135 | " \n", 136 | " \n", 137 | " \n", 138 | " \n", 139 | " \n", 140 | " \n", 141 | " \n", 142 | " \n", 143 | " \n", 144 | " \n", 145 | " \n", 146 | " \n", 147 | " \n", 148 | " \n", 149 | " \n", 150 | " \n", 151 | " \n", 152 | " \n", 153 | " \n", 154 | " \n", 155 | " \n", 156 | " \n", 157 | " \n", 158 | " \n", 159 | " \n", 160 | " \n", 161 | " \n", 162 | " \n", 163 | " \n", 164 | " \n", 165 | " \n", 166 | "
PatientIDx1x2y1y2z1z2
0CHGJ007-65.03906275.585938-166.992188-26.367188-204.050262-60.170746
1CHGJ008-65.03906275.585938-166.992188-26.367188-460.319519-316.438660
2CHGJ010-72.07031268.554688-135.3515625.273438-232.130219-88.250702
3CHGJ013-75.58593865.039062-152.929688-12.304688-242.630219-98.750702
4CHGJ015-65.03906275.585938-152.929688-12.304688-243.780273-99.900757
\n", 167 | "
" 168 | ], 169 | "text/plain": [ 170 | " PatientID x1 x2 y1 y2 z1 \\\n", 171 | "0 CHGJ007 -65.039062 75.585938 -166.992188 -26.367188 -204.050262 \n", 172 | "1 CHGJ008 -65.039062 75.585938 -166.992188 -26.367188 -460.319519 \n", 173 | "2 CHGJ010 -72.070312 68.554688 -135.351562 5.273438 -232.130219 \n", 174 | "3 CHGJ013 -75.585938 65.039062 -152.929688 -12.304688 -242.630219 \n", 175 | "4 CHGJ015 -65.039062 75.585938 -152.929688 -12.304688 -243.780273 \n", 176 | "\n", 177 | " z2 \n", 178 | "0 -60.170746 \n", 179 | "1 -316.438660 \n", 180 | "2 -88.250702 \n", 181 | "3 -98.750702 \n", 182 | "4 -99.900757 " 183 | ] 184 | }, 185 | "execution_count": 3, 186 | "metadata": {}, 187 | "output_type": "execute_result" 188 | } 189 | ], 190 | "source": [ 191 | "bb = pd.read_csv(path_to_bb)\n", 192 | "bb.head()" 193 | ] 194 | }, 195 | { 196 | "cell_type": "code", 197 | "execution_count": 4, 198 | "metadata": {}, 199 | "outputs": [ 200 | { 201 | "name": "stdout", 202 | "output_type": "stream", 203 | "text": [ 204 | "Total number of patients: 201\n" 205 | ] 206 | } 207 | ], 208 | "source": [ 209 | "patients = list(bb.PatientID)\n", 210 | "print(f'Total number of patients: {len(patients)}')" 211 | ] 212 | }, 213 | { 214 | "cell_type": "code", 215 | "execution_count": 5, 216 | "metadata": {}, 217 | "outputs": [ 218 | { 219 | "name": "stdout", 220 | "output_type": "stream", 221 | "text": [ 222 | "Available data for the patient CHUS101:\n", 223 | "['CHUS101_ct.nii.gz', 'CHUS101_ct_gtvt.nii.gz', 'CHUS101_pt.nii.gz']\n" 224 | ] 225 | } 226 | ], 227 | "source": [ 228 | "n = -1\n", 229 | "print(f'Available data for the patient {patients[n]}:')\n", 230 | "print(os.listdir(path_to_input / patients[n]))" 231 | ] 232 | }, 233 | { 234 | "cell_type": "markdown", 235 | "metadata": {}, 236 | "source": [ 237 | "#### Extract patches and resample:" 238 | ] 239 | }, 240 | { 241 | "cell_type": "code", 242 | "execution_count": 6, 243 | "metadata": {}, 244 | "outputs": [ 245 | { 246 | "name": "stdout", 247 | "output_type": "stream", 248 | "text": [ 249 | "Resampled images will be saved in C:\\inserm\\hecktor\\hecktor_train\\hecktor_nii_resampled\n" 250 | ] 251 | } 252 | ], 253 | "source": [ 254 | "print(f'Resampled images will be saved in {path_to_output}')\n", 255 | "\n", 256 | "if not os.path.exists(path_to_output):\n", 257 | " os.makedirs(path_to_output, exist_ok=True)" 258 | ] 259 | }, 260 | { 261 | "cell_type": "code", 262 | "execution_count": 7, 263 | "metadata": {}, 264 | "outputs": [ 265 | { 266 | "data": { 267 | "application/vnd.jupyter.widget-view+json": { 268 | "model_id": "96b2f152cc8c47158edda564ce425ee9", 269 | "version_major": 2, 270 | "version_minor": 0 271 | }, 272 | "text/plain": [ 273 | "HBox(children=(FloatProgress(value=0.0, max=201.0), HTML(value='')))" 274 | ] 275 | }, 276 | "metadata": {}, 277 | "output_type": "display_data" 278 | }, 279 | { 280 | "name": "stdout", 281 | "output_type": "stream", 282 | "text": [ 283 | "\n" 284 | ] 285 | } 286 | ], 287 | "source": [ 288 | "for p in tqdm(patients):\n", 289 | " # Read images:\n", 290 | " img_ct = read_nifti(path_to_input / p / (p + '_ct.nii.gz'))\n", 291 | " img_pt = read_nifti(path_to_input / p / (p + '_pt.nii.gz'))\n", 292 | " mask = read_nifti(path_to_input / p / (p + '_ct_gtvt.nii.gz'))\n", 293 | " \n", 294 | " # Get bounding boxes:\n", 295 | " pt1 = bb.loc[bb.PatientID == p, ['x1', 'y1', 'z1']]\n", 296 | " pt2 = bb.loc[bb.PatientID == p, ['x2', 'y2', 'z2']]\n", 297 | " pt1, pt2 = tuple(*pt1.values), tuple(*pt2.values)\n", 298 | " \n", 299 | " # Convert physcial points into array indexes:\n", 300 | " pt1_ct = img_ct.TransformPhysicalPointToIndex(pt1)\n", 301 | " pt1_pt = img_pt.TransformPhysicalPointToIndex(pt1)\n", 302 | " pt1_mask = mask.TransformPhysicalPointToIndex(pt1)\n", 303 | "\n", 304 | " pt2_ct = img_ct.TransformPhysicalPointToIndex(pt2)\n", 305 | " pt2_pt = img_pt.TransformPhysicalPointToIndex(pt2)\n", 306 | " pt2_mask = mask.TransformPhysicalPointToIndex(pt2)\n", 307 | " \n", 308 | " # Exctract the patch:\n", 309 | " cr_img_ct = img_ct[pt1_ct[0]: pt2_ct[0], pt1_ct[1]: pt2_ct[1], pt1_ct[2]: pt2_ct[2]]\n", 310 | " cr_img_pt = img_pt[pt1_pt[0]: pt2_pt[0], pt1_pt[1]: pt2_pt[1], pt1_pt[2]: pt2_pt[2]]\n", 311 | " cr_mask = mask[pt1_mask[0]: pt2_mask[0], pt1_mask[1]: pt2_mask[1], pt1_mask[2]: pt2_mask[2]]\n", 312 | " \n", 313 | " # Resample all images using CT attributes:\n", 314 | " # CT:\n", 315 | " cr_img_ct = resample_sitk_image(cr_img_ct, \n", 316 | " new_spacing=[1, 1, 1],\n", 317 | " new_size=[144, 144, 144],\n", 318 | " interpolator=sitk.sitkLinear)\n", 319 | " target_size = list(cr_img_ct.GetSize())\n", 320 | " attributes = get_attributes(cr_img_ct)\n", 321 | " \n", 322 | " # PT:\n", 323 | " cr_img_pt = resample_sitk_image(cr_img_pt, \n", 324 | " new_spacing=[1, 1, 1], \n", 325 | " new_size=target_size, \n", 326 | " attributes=attributes,\n", 327 | " interpolator=sitk.sitkLinear)\n", 328 | " \n", 329 | " # Mask:\n", 330 | " cr_mask = resample_sitk_image(cr_mask, \n", 331 | " new_spacing=[1, 1, 1], \n", 332 | " new_size=target_size, \n", 333 | " attributes=attributes,\n", 334 | " interpolator=sitk.sitkNearestNeighbor)\n", 335 | " \n", 336 | " # Save resampled images:\n", 337 | " if not os.path.exists(path_to_output / p):\n", 338 | " os.makedirs(path_to_output / p, exist_ok=True)\n", 339 | " \n", 340 | " write_nifti(cr_img_ct, path_to_output / p / (p + '_ct.nii.gz'))\n", 341 | " write_nifti(cr_img_pt, path_to_output / p / (p + '_pt.nii.gz'))\n", 342 | " write_nifti(cr_mask, path_to_output / p / (p + '_ct_gtvt.nii.gz'))" 343 | ] 344 | }, 345 | { 346 | "cell_type": "markdown", 347 | "metadata": {}, 348 | "source": [ 349 | "#### Check resampled images:" 350 | ] 351 | }, 352 | { 353 | "cell_type": "code", 354 | "execution_count": 8, 355 | "metadata": {}, 356 | "outputs": [], 357 | "source": [ 358 | "n = 0\n", 359 | "p = patients[n]\n", 360 | "\n", 361 | "img_ct = read_nifti(path_to_output / p / (p + '_ct.nii.gz'))\n", 362 | "img_pt = read_nifti(path_to_output / p / (p + '_pt.nii.gz'))\n", 363 | "mask = read_nifti(path_to_output / p / (p + '_ct_gtvt.nii.gz'))" 364 | ] 365 | }, 366 | { 367 | "cell_type": "code", 368 | "execution_count": 9, 369 | "metadata": {}, 370 | "outputs": [ 371 | { 372 | "name": "stdout", 373 | "output_type": "stream", 374 | "text": [ 375 | "SIZE:\n", 376 | "CT: \t(144, 144, 144) \n", 377 | "PET: \t(144, 144, 144) \n", 378 | "Mask: \t(144, 144, 144)\n", 379 | "----------------------------------------\n", 380 | "SPACING:\n", 381 | "CT: \t(1.0, 1.0, 1.0) \n", 382 | "PET: \t(1.0, 1.0, 1.0) \n", 383 | "Mask: \t(1.0, 1.0, 1.0)\n", 384 | "----------------------------------------\n", 385 | "ORIGIN:\n", 386 | "CT: \t(-65.42977905273438, -166.9922332763672, -204.05026245117188) \n", 387 | "PET: \t(-65.42977905273438, -166.9922332763672, -204.05026245117188) \n", 388 | "Mask: \t(-65.42977905273438, -166.9922332763672, -204.05026245117188)\n", 389 | "----------------------------------------\n", 390 | "DIRECTION:\n", 391 | "CT: \t(1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0) \n", 392 | "PET: \t(1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0) \n", 393 | "Mask: \t(1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0)\n" 394 | ] 395 | } 396 | ], 397 | "source": [ 398 | "print(f'SIZE:')\n", 399 | "print(f'CT: \\t{img_ct.GetSize()} \\nPET: \\t{img_pt.GetSize()} \\nMask: \\t{mask.GetSize()}')\n", 400 | "print('-' * 40)\n", 401 | "print(f'SPACING:')\n", 402 | "print(f'CT: \\t{img_ct.GetSpacing()} \\nPET: \\t{img_pt.GetSpacing()} \\nMask: \\t{mask.GetSpacing()}') \n", 403 | "print('-' * 40)\n", 404 | "print(f'ORIGIN:')\n", 405 | "print(f'CT: \\t{img_ct.GetOrigin()} \\nPET: \\t{img_pt.GetOrigin()} \\nMask: \\t{mask.GetOrigin()}') \n", 406 | "print('-' * 40)\n", 407 | "print(f'DIRECTION:')\n", 408 | "print(f'CT: \\t{img_ct.GetDirection()} \\nPET: \\t{img_pt.GetDirection()} \\nMask: \\t{mask.GetDirection()}') " 409 | ] 410 | }, 411 | { 412 | "cell_type": "markdown", 413 | "metadata": {}, 414 | "source": [ 415 | "#### Visualization:" 416 | ] 417 | }, 418 | { 419 | "cell_type": "code", 420 | "execution_count": 10, 421 | "metadata": {}, 422 | "outputs": [], 423 | "source": [ 424 | "from copy import copy" 425 | ] 426 | }, 427 | { 428 | "cell_type": "code", 429 | "execution_count": 11, 430 | "metadata": {}, 431 | "outputs": [], 432 | "source": [ 433 | "img_ct = sitk.GetArrayFromImage(img_ct)\n", 434 | "img_pt = sitk.GetArrayFromImage(img_pt)\n", 435 | "mask = sitk.GetArrayFromImage(mask)" 436 | ] 437 | }, 438 | { 439 | "cell_type": "code", 440 | "execution_count": 12, 441 | "metadata": {}, 442 | "outputs": [], 443 | "source": [ 444 | "img_ct_mask = np.ma.masked_where(img_ct * mask != 0, img_ct)\n", 445 | "img_pt_mask = np.ma.masked_where(img_pt * mask != 0, img_pt)" 446 | ] 447 | }, 448 | { 449 | "cell_type": "code", 450 | "execution_count": 13, 451 | "metadata": {}, 452 | "outputs": [ 453 | { 454 | "data": { 455 | "image/png": "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\n", 456 | "text/plain": [ 457 | "
" 458 | ] 459 | }, 460 | "metadata": {}, 461 | "output_type": "display_data" 462 | } 463 | ], 464 | "source": [ 465 | "s = 75\n", 466 | "\n", 467 | "plt.rcdefaults()\n", 468 | "palette = copy(plt.cm.Greys_r)\n", 469 | "palette.set_bad(color='r', alpha=1) # set color for the mask\n", 470 | "\n", 471 | "fig, axes = plt.subplots(1, 4, figsize=(14, 10))\n", 472 | "axes[0].imshow(img_ct[:, :, s], cmap=plt.cm.Greys_r, interpolation='none')\n", 473 | "axes[1].imshow(img_pt[:, :, s], cmap=plt.cm.Greys_r, interpolation='none')\n", 474 | "axes[2].imshow(img_ct_mask[:, :, s], cmap=palette, interpolation='none')\n", 475 | "axes[3].imshow(img_pt_mask[:, :, s], cmap=palette, interpolation='none')\n", 476 | "plt.tight_layout()\n", 477 | "plt.show()" 478 | ] 479 | }, 480 | { 481 | "cell_type": "code", 482 | "execution_count": null, 483 | "metadata": {}, 484 | "outputs": [], 485 | "source": [] 486 | } 487 | ], 488 | "metadata": { 489 | "kernelspec": { 490 | "display_name": "Python 3", 491 | "language": "python", 492 | "name": "python3" 493 | }, 494 | "language_info": { 495 | "codemirror_mode": { 496 | "name": "ipython", 497 | "version": 3 498 | }, 499 | "file_extension": ".py", 500 | "mimetype": "text/x-python", 501 | "name": "python", 502 | "nbconvert_exporter": "python", 503 | "pygments_lexer": "ipython3", 504 | "version": "3.6.5" 505 | } 506 | }, 507 | "nbformat": 4, 508 | "nbformat_minor": 2 509 | } 510 | --------------------------------------------------------------------------------