├── .gitignore
├── .gitmodules
├── README.md
├── __init__.py
├── assets
├── process_sde.png
└── sampling_sdf.gif
├── cfg
├── __init__.py
├── glas.yaml
└── monuseg.yaml
├── datasets
├── __init__.py
├── config_dl.py
├── glas_dataset.py
├── monuseg_dataset.py
└── transform_factory.py
├── env.yml
├── main.py
├── models
├── __init__.py
└── ddpm.py
├── preprocess_data
└── precompute_sdf.ipynb
├── sampler.py
└── trainer.py
/.gitignore:
--------------------------------------------------------------------------------
1 | *.directory
2 | *pycache*
3 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "SimulationHelper"]
2 | path = SimulationHelper
3 | url = https://github.com/f-ilic/SimulationHelper.git
4 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | **Score-Based Generative Models for Medical Image Segmentation using Signed Distance Functions**
3 | GCPR 2023
4 | Lea Bogensperger, Dominik Narnhofer, Filip Ilic, Thomas Pock
5 |
6 | ---
7 |
8 | [[Project Page]](https://github.com/leabogensperger/generative-segmentation-sdf)
9 | [[Paper]](https://arxiv.org/abs/2303.05966)
10 |
11 |
12 | Environment Setup:
13 | ```bash
14 | git clone --recurse-submodules git@github.com:leabogensperger/generative-segmentation-sdf.git
15 | conda env create -f env.yaml
16 | conda activate generative_segmentation_sdf
17 | ```
18 |
19 | # Score-Based Generative Models for Medical Image Segmentation using Signed Distance Functions
20 |
21 | This repository contains the code to train a generative model that learns the conditional distribution of implicit segmentation masks in the form of signed distance function conditioned on a specific input image. The generative model is set up as a score-based diffusion model with a variance-exploding scheme -- however, later experiments have shown that the variance-preserving scheme seems numerically a bit more stable for this case, therefore this option is now also included (set the param *sde* in *SMLD* of the config file to either *ve*/*vp*).
22 |
23 |
24 |
25 | # Instructions
26 |
27 | 1) Run by specifying a config file:
28 | ```python
29 | python main.py --config "cfg/monuseg.yaml"
30 | ```
31 |
32 | 2) Sample (set experiment folder in config file):
33 | ```python
34 | python sample.py --config "cfg/monuseg.yaml"
35 | ```
36 |
37 | Note: the pre-processed data sets will be uploaded later. The data set is specified by the config file. The root directory is set with in the config file, which must contain csv files for train and test mode with columns *filename* and *maskname* of all pre-processed patches. Moreover, it must contain the folders *Trainig_patches* and *Test_patches*, which include for each patch a .png file of the input image and a .npy file of the sdf transformed segmentation mask.
38 |
39 | # Sampling
40 |
41 | The sampling process of the proposed approach is shown using the predictor-corrector sampling algorithm (see Algorithm 1 in the paper).
42 | In the top row there are four different condition images and the center row contains the generated/predicted SDF masks.
43 | Further, the bottom row displays the corresponding binary masks, which are obtained only indirectly from thresholding the predicted SDF masks.
44 |
45 |
46 |
47 | # Cite
48 |
49 | ```bibtex
50 | @misc{
51 | bogensperger2023scorebased,
52 | title={Score-Based Generative Models for Medical Image Segmentation using Signed Distance Functions},
53 | author={Lea Bogensperger and Dominik Narnhofer and Filip Ilic and Thomas Pock},
54 | year={2023},
55 | eprint={2303.05966},
56 | archivePrefix={arXiv},
57 | primaryClass={cs.CV}
58 | }
59 |
--------------------------------------------------------------------------------
/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leabogensperger/generative-segmentation-sdf/3b8037e3e03d347a41f361f6ba844e6928240200/__init__.py
--------------------------------------------------------------------------------
/assets/process_sde.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leabogensperger/generative-segmentation-sdf/3b8037e3e03d347a41f361f6ba844e6928240200/assets/process_sde.png
--------------------------------------------------------------------------------
/assets/sampling_sdf.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leabogensperger/generative-segmentation-sdf/3b8037e3e03d347a41f361f6ba844e6928240200/assets/sampling_sdf.gif
--------------------------------------------------------------------------------
/cfg/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leabogensperger/generative-segmentation-sdf/3b8037e3e03d347a41f361f6ba844e6928240200/cfg/__init__.py
--------------------------------------------------------------------------------
/cfg/glas.yaml:
--------------------------------------------------------------------------------
1 | general:
2 | modality: glas
3 | corr_mode: diffusion_ls # diffusion on level sets
4 | img_cond: 1 # condition on image to obtain segmentation mask
5 | data_path: "/home/lea/Data/GlaS_trunc"
6 | csv_train: "train.csv"
7 | csv_test: "test.csv"
8 | batch_size: 32
9 | sz: 128 #
10 | resume_training: False
11 | load_path: ''
12 | class_label_cond: False
13 | num_classes: 0
14 | with_class_label_emb: False
15 |
16 | inference:
17 | latest: False
18 | load_exp: ''
19 | n_samples: 4
20 |
21 | model:
22 | type: 'unet'
23 | n_cin: 1 # 1, n_classes
24 | n_cin_cond: 1 # 1, 3 for gray/color valued input
25 | n_fm: 10
26 | dim: 128
27 | embedding: 'sinusoidal'
28 | mults:
29 | - 1
30 | - 2
31 | - 4
32 | - 4
33 |
34 | learning:
35 | epochs: 500000
36 | lr: 1.0E-4
37 | loss: 2
38 | n_val: 8
39 | clip: 100000.
40 | gpus:
41 | - 1
42 |
43 | SMLD:
44 | sde: 'vp' # VE used in GCPR paper, VP scheme is like classic DDPM
45 | beta_1: 1.E-4 # default from DDPM
46 | beta_T: 0.02 # default from DDPM
47 | T: 1000 # default from DDPM
48 | n_steps: 100 # VE params
49 | sigma_1_m: 5. # VE params: heuristic
50 | sigma_L_m: 0.001 # VE params: heuristic
51 | objective: 'cont'
52 | sampler: 'pc' # for VE scheme with reverse SDE
53 | eps: 2.0E-5 # annealed Langevin
54 | N: 200 # Predictor steps
55 | M: 1 # Corrector steps
56 | r: 0.15 # "snr" for PC sampling
--------------------------------------------------------------------------------
/cfg/monuseg.yaml:
--------------------------------------------------------------------------------
1 | general:
2 | modality: monuseg
3 | corr_mode: diffusion_ls # diffusion on level sets
4 | img_cond: 1 # condition on image to obtain segmentation mask
5 | data_path: "/home/lea/Data/MonuSeg_spcn_trunc"
6 | csv_train: "train.csv"
7 | csv_test: "test.csv"
8 | batch_size: 32
9 | sz: 128 # 128, 256
10 | resume_training: False
11 | load_path: ''
12 | class_label_cond: False
13 | num_classes: 0
14 | with_class_label_emb: False
15 |
16 | inference:
17 | latest: False
18 | load_exp: ''
19 | n_samples: 4
20 |
21 | model:
22 | type: 'unet'
23 | n_cin: 1 # 1, n_classes
24 | n_cin_cond: 1 # 1, 3 for gray/color valued input
25 | n_fm: 10
26 | dim: 128
27 | embedding: 'sinusoidal'
28 | mults:
29 | - 1
30 | - 2
31 | - 4
32 | - 4
33 |
34 | learning:
35 | epochs: 500000
36 | lr: 1.0E-4
37 | loss: 2
38 | n_val: 8
39 | clip: 40000.
40 | gpus:
41 | - 1
42 |
43 | SMLD:
44 | sde: 'vp' # VE used in GCPR paper, VP scheme is like classic DDPM
45 | beta_1: 1.E-4 # default from DDPM
46 | beta_T: 0.02 # default from DDPM
47 | T: 1000 # default from DDPM
48 | n_steps: 100 # VE params
49 | sigma_1_m: 5. # VE params: heuristic
50 | sigma_L_m: 0.001 # VE params: heuristic
51 | objective: 'cont'
52 | sampler: 'pc' # for VE scheme with reverse SDE
53 | eps: 2.0E-5 # annealed Langevin
54 | N: 200 # Predictor steps
55 | M: 1 # Corrector steps
56 | r: 0.15 # "snr" for PC sampling
57 |
--------------------------------------------------------------------------------
/datasets/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leabogensperger/generative-segmentation-sdf/3b8037e3e03d347a41f361f6ba844e6928240200/datasets/__init__.py
--------------------------------------------------------------------------------
/datasets/config_dl.py:
--------------------------------------------------------------------------------
1 | from types import SimpleNamespace
2 | from torch.utils.data.dataloader import DataLoader
3 | import yaml
4 | import json
5 | from os.path import join, isfile
6 |
7 | from datasets.transform_factory import inv_normalize, transform_factory
8 | from datasets.monuseg_dataset import MoNuSegDataset
9 | from datasets.glas_dataset import GlaSDataset
10 |
11 |
12 | def config_dl(cfg):
13 | if cfg.general.modality == 'monuseg':
14 | DatasetType = MoNuSegDataset
15 | stats = {'mean': 0., 'std': 1.}
16 |
17 | elif cfg.general.modality == 'glas':
18 | DatasetType = GlaSDataset
19 | stats = {'mean': 0., 'std': 1.}
20 |
21 | else:
22 | raise ValueError('Unknown modality %s specified!' %cfg.modality)
23 |
24 | train_dataset = DatasetType(cfg.general.data_path, f'{cfg.general.data_path}/{cfg.general.csv_train}', cfg=cfg.general)
25 | test_dataset = DatasetType(cfg.general.data_path, f'{cfg.general.data_path}/{cfg.general.csv_test}', cfg=cfg.general)
26 |
27 | train_dataloader = DataLoader(train_dataset, batch_size=cfg.general.batch_size, shuffle=True, drop_last=True)
28 | test_dataloader = DataLoader(test_dataset, batch_size=cfg.inference.n_samples, shuffle=False, drop_last=False)
29 |
30 | dbdict = {"train_dl": train_dataloader, "test_dl": test_dataloader}
31 |
32 | train_dl, test_dl = dbdict["train_dl"], dbdict["test_dl"]
33 | tfdict = transform_factory(cfg.general)
34 | T_train, T_test = tfdict["train"](stats["mean"], stats["std"]), tfdict["test"](
35 | stats["mean"], stats["std"]
36 | )
37 | train_dl.dataset.transform, test_dl.dataset.transform = T_train, T_test
38 | train_dl.inv_normalize, test_dl.inv_normalize = inv_normalize(
39 | stats["mean"], stats["std"]
40 | ), inv_normalize(stats["mean"], stats["std"])
41 |
42 | return train_dl, test_dl
43 |
--------------------------------------------------------------------------------
/datasets/glas_dataset.py:
--------------------------------------------------------------------------------
1 | from PIL import Image, ImageOps
2 | import numpy as np
3 | import cv2
4 | import pandas as pd
5 | import matplotlib.pyplot as plt
6 | from types import SimpleNamespace
7 |
8 | import torch
9 | from torch.utils.data import Dataset, DataLoader
10 | from datasets.transform_factory import transform_factory
11 |
12 | class GlaSDataset(Dataset):
13 | def __init__(self, data_path, csv_file, cfg):
14 | self.data_path = data_path
15 | self.csv_file = csv_file
16 | self.data = pd.read_csv(self.csv_file)
17 |
18 | self.transform = None
19 | self.inv_normalize = None
20 |
21 | self.corr_mode = cfg.corr_mode
22 | self.img_cond = cfg.img_cond
23 | self.sz = cfg.sz
24 |
25 | def __len__(self):
26 | return len(self.data)
27 |
28 | def __getitem__(self, idx):
29 | img_path = self.data_path + self.data.loc[idx]['filename']
30 | mask_path = self.data_path + self.data.loc[idx]['maskname']
31 |
32 | # load image and mask
33 | img = cv2.imread(img_path,0).astype(np.float32)/255.
34 |
35 | # load level sets mask
36 | if self.corr_mode == 'diffusion_ls':
37 | mask_ls_path = self.data_path + self.data.loc[idx]['maskdtname']
38 | mask = np.load(mask_ls_path).astype(np.float32)
39 | else:
40 | mask = cv2.imread(mask_path,0).astype(np.float32)
41 | mask = mask/255.
42 |
43 | if self.corr_mode == 'diffusion':
44 | corr_type = 0
45 | else:
46 | corr_type = 1
47 |
48 | transform_cfg = {
49 | 'hflip': np.random.rand(),
50 | 'vflip': np.random.rand(),
51 | 'corr_type': corr_type, # 0 is diffusion
52 | 'img_cond': self.img_cond,
53 | }
54 |
55 | if self.img_cond: # condition on image
56 | ret = {'image': mask, 'mask': img, 'name': str(img_path.split('/')[-1][:-4])}
57 | else:
58 | ret = {'image': img, 'mask': mask, 'name': str(img_path.split('/')[-1][:-4])}
59 |
60 | self.transform(transform_cfg)(ret)
61 |
62 | if self.img_cond and self.corr_mode == 'diffusion_ls':
63 | if 'trunc' in self.data_path:
64 | ret['image'] /= 5.
65 | else:
66 | ret['image'] *= 1.6
67 | return ret
--------------------------------------------------------------------------------
/datasets/monuseg_dataset.py:
--------------------------------------------------------------------------------
1 | from PIL import Image, ImageOps
2 | import numpy as np
3 | import cv2
4 | import pandas as pd
5 | import matplotlib.pyplot as plt
6 | from types import SimpleNamespace
7 |
8 | import torch
9 | from torch.utils.data import Dataset, DataLoader
10 | from datasets.transform_factory import transform_factory
11 |
12 | class MoNuSegDataset(Dataset):
13 | def __init__(self, data_path, csv_file, cfg):
14 | self.data_path = data_path
15 | self.csv_file = csv_file
16 | self.data = pd.read_csv(self.csv_file)
17 |
18 | self.transform = None
19 | self.inv_normalize = None
20 |
21 | self.corr_mode = cfg.corr_mode
22 | self.img_cond = cfg.img_cond
23 | self.sz = cfg.sz
24 |
25 | def __len__(self):
26 | return len(self.data)
27 |
28 | def __getitem__(self, idx):
29 | img_path = self.data_path + self.data.loc[idx]['filename']
30 | mask_path = self.data_path + self.data.loc[idx]['maskname']
31 |
32 | # load image and mask
33 | if 'rgb' in self.data_path:
34 | img = cv2.imread(img_path).astype(np.float32)/255.
35 | else:
36 | img = cv2.imread(img_path,0).astype(np.float32)/255.
37 |
38 | # load level sets mask
39 | if self.corr_mode == 'diffusion_ls':
40 | mask_ls_path = self.data_path + self.data.loc[idx]['maskdtname']
41 | mask = np.load(mask_ls_path)
42 | else:
43 | mask = cv2.imread(mask_path,0).astype(np.float32)
44 | mask[mask > 200] = 255.
45 | mask[mask <= 200] = 0.
46 | # mask to [0,1]
47 | mask = mask/255.
48 |
49 | if self.corr_mode == 'diffusion':
50 | corr_type = 0
51 | else:
52 | corr_type = 1
53 |
54 | transform_cfg = {
55 | 'hflip': np.random.rand(),
56 | 'vflip': np.random.rand(),
57 | 'corr_type': corr_type, # 0 is diffusion
58 | 'img_cond': self.img_cond,
59 | }
60 |
61 | if self.img_cond: # condition on image
62 | ret = {'image': mask, 'mask': img, 'name': str(img_path.split('/')[-1][:-4])}
63 | else:
64 | ret = {'image': img, 'mask': mask, 'name': str(img_path.split('/')[-1][:-4])}
65 |
66 | self.transform(transform_cfg)(ret)
67 |
68 | if self.img_cond and self.corr_mode == 'diffusion_ls':
69 | if 'trunc' in self.data_path:
70 | ret['image'] /= 5.
71 | else:
72 | ret['image'] *= 10./3. # heuristic from intensity distribution of histogram
73 |
74 | return ret
75 |
--------------------------------------------------------------------------------
/datasets/transform_factory.py:
--------------------------------------------------------------------------------
1 | from typing import Callable, Dict, List, Optional, Tuple
2 |
3 | import numpy as np
4 | import torch
5 | import torch.nn as nn
6 | from torchvision.transforms import Compose, Lambda, transforms, InterpolationMode, CenterCrop
7 | from torchvision.transforms.functional import crop, hflip, vflip, equalize
8 |
9 | class ApplyTransformToKey:
10 | def __init__(self, key: str, transform: Callable):
11 | self._key = key
12 | self._transform = transform
13 |
14 | def __call__(self, x: Dict[str, torch.Tensor]) -> Dict[str, torch.Tensor]:
15 | x[self._key] = self._transform(x[self._key])
16 | return x
17 |
18 | # ----------------------------------------------------------------
19 |
20 |
21 | def train_glas_transform(mean, std):
22 | def configured_transform(transform_config):
23 | p_hflip = transform_config['hflip']
24 | p_vflip = transform_config['vflip']
25 | corr_type = transform_config['corr_type']
26 | img_cond = transform_config['img_cond']
27 |
28 | def hflip_closure(img):
29 | return hflip(img) if p_hflip > 0.5 else img
30 |
31 | def vflip_closure(img):
32 | return vflip(img) if p_vflip > 0.5 else img
33 |
34 | def normalization_mask(mask):
35 | return (mask-0.5)*2 # normalize all to be in [-1,1] for guidance image
36 |
37 | return Compose([
38 | ApplyTransformToKey(
39 | key="image",
40 | transform=Compose([
41 | transforms.ToTensor(),
42 | hflip_closure,
43 | vflip_closure,
44 | ]),
45 | ),
46 |
47 | ApplyTransformToKey(
48 | key="mask",
49 | transform=Compose([
50 | transforms.ToTensor(),
51 | normalization_mask,
52 | hflip_closure,
53 | vflip_closure,
54 | ]),
55 | ),
56 | ])
57 | return configured_transform
58 |
59 | def test_glas_transform(mean, std):
60 | def configured_transform(transform_config):
61 | def normalization_mask(mask):
62 | return (mask-0.5)*2 # normalize all to be in [-1,1] for guidance image
63 |
64 | return Compose([
65 | ApplyTransformToKey(
66 | key="image",
67 | transform=Compose([
68 | transforms.ToTensor(),
69 | ]),
70 | ),
71 |
72 | ApplyTransformToKey(
73 | key="mask",
74 | transform=Compose([
75 | transforms.ToTensor(),
76 | normalization_mask,
77 | ]),
78 | ),
79 | ])
80 | return configured_transform
81 |
82 | def train_monuseg_transform(mean, std):
83 | def configured_transform(transform_config):
84 | p_hflip = transform_config['hflip']
85 | p_vflip = transform_config['vflip']
86 | corr_type = transform_config['corr_type']
87 | img_cond = transform_config['img_cond']
88 |
89 | def normalization(img):
90 | return (img-0.5)*2 if corr_type == 0 else img
91 |
92 | def hflip_closure(img):
93 | return hflip(img) if p_hflip > 0.5 else img
94 |
95 | def vflip_closure(img):
96 | return vflip(img) if p_vflip > 0.5 else img
97 |
98 | def normalization(img):
99 | return img # placeholder for different normalization procedure
100 |
101 | def normalization_mask(mask):
102 | return (mask-0.5)*2 # normalize all to be in [-1,1] for guidance image
103 |
104 | interp_mode_img = InterpolationMode.NEAREST if img_cond == 1 else InterpolationMode.BILINEAR
105 | interp_mode_mask = InterpolationMode.BILINEAR if img_cond == 1 else InterpolationMode.NEAREST
106 |
107 | return Compose([
108 | ApplyTransformToKey(
109 | key="image",
110 | transform=Compose([
111 | transforms.ToTensor(),
112 | hflip_closure,
113 | vflip_closure,
114 | ]),
115 | ),
116 |
117 | ApplyTransformToKey(
118 | key="mask",
119 | transform=Compose([
120 | transforms.ToTensor(),
121 | normalization_mask,
122 | hflip_closure,
123 | vflip_closure,
124 | ]),
125 | ),
126 | ])
127 | return configured_transform
128 |
129 | def test_monuseg_transform(mean, std):
130 | def configured_transform(transform_config):
131 | p_hflip = transform_config['hflip']
132 | p_vflip = transform_config['vflip']
133 | corr_type = transform_config['corr_type']
134 | img_cond = transform_config['img_cond']
135 |
136 | def normalization(img):
137 | return (img-0.5)*2 if corr_type == 0 else img
138 |
139 | def normalization(img):
140 | return img # placeholder for different normalization procedure
141 |
142 | def normalization_mask(mask):
143 | return (mask-0.5)*2 # normalize all to be in [-1,1] for guidance image
144 |
145 | interp_mode_img = InterpolationMode.NEAREST if img_cond == 1 else InterpolationMode.BILINEAR
146 | interp_mode_mask = InterpolationMode.BILINEAR if img_cond == 1 else InterpolationMode.NEAREST
147 |
148 | return Compose([
149 | ApplyTransformToKey(
150 | key="image",
151 | transform=Compose([
152 | transforms.ToTensor(),
153 | ]),
154 | ),
155 |
156 | ApplyTransformToKey(
157 | key="mask",
158 | transform=Compose([
159 | transforms.ToTensor(),
160 | normalization_mask,
161 | ]),
162 | ),
163 | ])
164 | return configured_transform
165 |
166 | # ----------------------------------------------------------------
167 | def inv_normalize(mean, std):
168 | return transforms.Normalize(mean=-mean/std, std=1/std)
169 |
170 | def transform_factory(cfg):
171 | if cfg.modality == 'monuseg':
172 | ret = {
173 | 'train': train_monuseg_transform,
174 | 'test' : test_monuseg_transform
175 | }
176 |
177 | elif cfg.modality == 'glas':
178 | ret = {
179 | 'train': train_glas_transform,
180 | 'test': test_glas_transform
181 | }
182 |
183 | else:
184 | raise ValueError('Unknown modality %s specified!' %cfg.modality)
185 |
186 | return ret
187 |
--------------------------------------------------------------------------------
/env.yml:
--------------------------------------------------------------------------------
1 | name: generative_segmentation_sdf
2 | channels:
3 | - anaconda
4 | - pytorch
5 | - nvidia
6 | - conda-forge
7 | - defaults
8 | dependencies:
9 | - _libgcc_mutex=0.1=conda_forge
10 | - _openmp_mutex=4.5=1_gnu
11 | - _tflow_select=2.3.0=mkl
12 | - absl-py=0.13.0=pyhd8ed1ab_0
13 | - aiohttp=3.7.4.post0=py38h497a2fe_0
14 | - appdirs=1.4.4=pyh9f0ad1d_0
15 | - astor=0.8.1=pyh9f0ad1d_0
16 | - astunparse=1.6.3=pyhd8ed1ab_0
17 | - async-timeout=3.0.1=py_1000
18 | - attrs=21.2.0=pyhd8ed1ab_0
19 | - autopep8=1.5.7=pyhd8ed1ab_0
20 | - blas=1.0=mkl
21 | - blinker=1.4=py_1
22 | - brotlipy=0.7.0=py38h497a2fe_1001
23 | - bzip2=1.0.8=h7f98852_4
24 | - c-ares=1.17.1=h7f98852_1
25 | - ca-certificates=2022.9.24=ha878542_0
26 | - cachetools=4.2.2=pyhd8ed1ab_0
27 | - certifi=2022.9.24=pyhd8ed1ab_0
28 | - cffi=1.14.6=py38ha65f79e_0
29 | - chardet=3.0.4=py38h924ce5b_1008
30 | - click=8.0.1=py38h578d9bd_0
31 | - cloudpickle=2.0.0=pyhd8ed1ab_0
32 | - coverage=5.5=py38h497a2fe_0
33 | - cryptography=3.4.7=py38ha5dfef3_0
34 | - cudatoolkit=11.1.74=h6bb024c_0
35 | - cycler=0.10.0=py_2
36 | - cython=0.29.24=py38h709712a_0
37 | - cytoolz=0.11.0=py38h497a2fe_3
38 | - dask-core=2021.8.1=pyhd8ed1ab_0
39 | - dbus=1.13.18=hb2f20db_0
40 | - decorator=4.4.2=py_0
41 | - dominate=2.6.0=pyhd8ed1ab_0
42 | - einops=0.4.1=pyhd8ed1ab_0
43 | - expat=2.4.1=h9c3ff4c_0
44 | - ffmpeg=4.3=hf484d3e_0
45 | - fontconfig=2.13.1=h6c09931_0
46 | - freetype=2.10.4=h0708190_1
47 | - fsspec=2021.8.1=pyhd8ed1ab_0
48 | - gast=0.4.0=pyh9f0ad1d_0
49 | - gettext=0.19.8.1=h0b5b191_1005
50 | - glib=2.69.0=h5202010_0
51 | - gmp=6.2.1=h58526e2_0
52 | - gnutls=3.6.15=he1e5248_0
53 | - google-auth=1.33.0=pyh6c4a22f_0
54 | - google-auth-oauthlib=0.4.4=pyhd8ed1ab_0
55 | - google-pasta=0.2.0=pyh8c360ce_0
56 | - grpcio=1.36.1=py38hdd6454d_0
57 | - gst-plugins-base=1.14.0=h8213a91_2
58 | - gstreamer=1.14.0=h28cd5cc_2
59 | - h5py=2.10.0=nompi_py38h9915d05_106
60 | - hdf5=1.10.6=nompi_h7c3c948_1111
61 | - icu=58.2=hf484d3e_1000
62 | - idna=2.10=pyh9f0ad1d_0
63 | - imageio=2.9.0=py_0
64 | - importlib-metadata=3.10.0=py38h578d9bd_0
65 | - intel-openmp=2021.3.0=h06a4308_3350
66 | - joblib=1.0.1=pyhd8ed1ab_0
67 | - jpeg=9b=h024ee3a_2
68 | - keras-preprocessing=1.1.2=pyhd8ed1ab_0
69 | - kiwisolver=1.3.1=py38h1fd1430_1
70 | - krb5=1.19.2=hcc1bbae_0
71 | - lame=3.100=h7f98852_1001
72 | - lcms2=2.12=h3be6417_0
73 | - ld_impl_linux-64=2.35.1=hea4e1c9_2
74 | - libblas=3.9.0=11_linux64_mkl
75 | - libcblas=3.9.0=11_linux64_mkl
76 | - libcurl=7.78.0=h2574ce0_0
77 | - libedit=3.1.20191231=he28a2e2_2
78 | - libev=4.33=h516909a_1
79 | - libffi=3.3=h58526e2_2
80 | - libgcc-ng=9.3.0=h2828fa1_19
81 | - libgfortran-ng=7.5.0=h14aa051_19
82 | - libgfortran4=7.5.0=h14aa051_19
83 | - libgomp=9.3.0=h2828fa1_19
84 | - libiconv=1.15=h516909a_1006
85 | - libidn2=2.3.2=h7f98852_0
86 | - libllvm10=10.0.1=he513fc3_3
87 | - libnghttp2=1.43.0=h812cca2_0
88 | - libpng=1.6.37=h21135ba_2
89 | - libprotobuf=3.17.2=h780b84a_1
90 | - libssh2=1.9.0=ha56f1ee_6
91 | - libstdcxx-ng=9.3.0=h6de172a_19
92 | - libtasn1=4.16.0=h27cfd23_0
93 | - libtiff=4.2.0=h85742a9_0
94 | - libunistring=0.9.10=h7f98852_0
95 | - libuuid=1.0.3=h7f8727e_2
96 | - libuv=1.40.0=h7f98852_0
97 | - libwebp-base=1.2.0=h7f98852_2
98 | - libxcb=1.14=h7b6447c_0
99 | - libxml2=2.9.12=h03d6c58_0
100 | - libxslt=1.1.34=hc22bd24_0
101 | - libzlib=1.2.11=h36c2ea0_1013
102 | - llvmlite=0.36.0=py38h4630a5e_0
103 | - locket=0.2.1=py38h06a4308_1
104 | - lxml=4.8.0=py38h1f438cf_0
105 | - lz4-c=1.9.3=h9c3ff4c_1
106 | - markdown=3.3.4=pyhd8ed1ab_0
107 | - matplotlib=3.3.4=py38h578d9bd_0
108 | - matplotlib-base=3.3.4=py38h0efea84_0
109 | - mkl=2021.3.0=h06a4308_520
110 | - mkl-service=2.4.0=py38h497a2fe_0
111 | - mkl_fft=1.3.0=py38h42c9631_2
112 | - mkl_random=1.2.2=py38h1abd341_0
113 | - multidict=5.1.0=py38h497a2fe_1
114 | - mypy=0.910=py38h497a2fe_0
115 | - mypy_extensions=0.4.3=py38h578d9bd_4
116 | - ncurses=6.2=h58526e2_4
117 | - nettle=3.7.3=hbbd107a_1
118 | - networkx=2.6.3=pyhd8ed1ab_1
119 | - ninja=1.10.2=h4bd325d_0
120 | - numba=0.53.1=py38h8b71fd7_1
121 | - numpy=1.20.3=py38hf144106_0
122 | - numpy-base=1.20.3=py38h74d4b33_0
123 | - oauthlib=3.1.1=pyhd8ed1ab_0
124 | - olefile=0.46=pyh9f0ad1d_1
125 | - openh264=2.1.0=hd408876_0
126 | - openjpeg=2.3.0=hf38bd82_1003
127 | - openssl=1.1.1q=h7f8727e_0
128 | - opt_einsum=3.3.0=pyhd8ed1ab_1
129 | - packaging=21.0=pyhd8ed1ab_0
130 | - pandas=1.3.1=py38h1abd341_0
131 | - partd=1.2.0=pyhd8ed1ab_0
132 | - pcre=8.45=h9c3ff4c_0
133 | - pillow=8.3.1=py38h2c7a002_0
134 | - pip=21.1.3=pyhd8ed1ab_0
135 | - pooch=1.5.2=pyhd8ed1ab_0
136 | - protobuf=3.17.2=py38h709712a_0
137 | - psutil=5.8.0=py38h497a2fe_1
138 | - pyasn1=0.4.8=py_0
139 | - pyasn1-modules=0.2.8=py_0
140 | - pycodestyle=2.7.0=pyhd8ed1ab_0
141 | - pycparser=2.20=pyh9f0ad1d_2
142 | - pyjwt=2.1.0=pyhd8ed1ab_0
143 | - pyopenssl=20.0.1=pyhd8ed1ab_0
144 | - pyparsing=2.4.7=pyhd8ed1ab_1
145 | - pyqt=5.9.2=py38h05f1152_4
146 | - pysocks=1.7.1=py38h578d9bd_4
147 | - python=3.8.10=h49503c6_1_cpython
148 | - python-dateutil=2.8.2=pyhd8ed1ab_0
149 | - python-flatbuffers=1.12=pyhd8ed1ab_1
150 | - python_abi=3.8=2_cp38
151 | - pytorch=1.9.0=py3.8_cuda11.1_cudnn8.0.5_0
152 | - pytz=2021.3=pyhd8ed1ab_0
153 | - pyu2f=0.1.5=pyhd8ed1ab_0
154 | - pywavelets=1.1.1=py38h5c078b8_3
155 | - pyyaml=5.4.1=py38h497a2fe_0
156 | - qt=5.9.7=h5867ecd_1
157 | - readline=8.1=h46c0cb4_0
158 | - requests=2.25.1=pyhd3deb0d_0
159 | - requests-oauthlib=1.3.0=pyh9f0ad1d_0
160 | - rsa=4.7.2=pyh44b312d_0
161 | - scikit-image=0.18.1=py38h51da96c_0
162 | - scikit-learn=0.24.2=py38hdc147b9_0
163 | - scipy=1.6.2=py38had2a1c9_1
164 | - seaborn=0.11.2=pyhd3eb1b0_0
165 | - setuptools=52.0.0=py38h06a4308_1
166 | - sip=4.19.13=py38he6710b0_0
167 | - six=1.16.0=pyh6c4a22f_0
168 | - sqlite=3.36.0=h9cd32fc_0
169 | - tbb=2020.3=hfd86e86_0
170 | - tensorboard=2.6.0=pyhd8ed1ab_1
171 | - tensorboard-data-server=0.6.0=py38h2b97feb_0
172 | - tensorboard-plugin-wit=1.6.0=pyh9f0ad1d_0
173 | - tensorboardx=2.5.1=pyhd8ed1ab_0
174 | - tensorflow=2.4.1=mkl_py38hb2083e0_0
175 | - tensorflow-base=2.4.1=mkl_py38h43e0292_0
176 | - tensorflow-estimator=2.5.0=pyh81a9013_1
177 | - termcolor=1.1.0=py_2
178 | - threadpoolctl=2.2.0=pyh8a188c0_0
179 | - tifffile=2020.10.1=py38hdd07704_2
180 | - tk=8.6.10=h21135ba_1
181 | - toml=0.10.2=pyhd8ed1ab_0
182 | - toolz=0.11.1=py_0
183 | - torchaudio=0.9.0=py38
184 | - torchvision=0.10.0=py38_cu111
185 | - tornado=6.1=py38h497a2fe_1
186 | - typing-extensions=3.10.0.0=hd8ed1ab_0
187 | - typing_extensions=3.10.0.0=pyha770c72_0
188 | - urllib3=1.26.6=pyhd8ed1ab_0
189 | - werkzeug=1.0.1=pyh9f0ad1d_0
190 | - wheel=0.36.2=pyhd3deb0d_0
191 | - wrapt=1.12.1=py38h497a2fe_3
192 | - xz=5.2.5=h516909a_1
193 | - yaml=0.2.5=h516909a_0
194 | - yarl=1.6.3=py38h497a2fe_2
195 | - zipp=3.5.0=pyhd8ed1ab_0
196 | - zlib=1.2.11=h36c2ea0_1013
197 | - zstd=1.4.9=ha95c52a_0
198 | - pip:
199 | - anyio==3.6.1
200 | - argon2-cffi==21.3.0
201 | - argon2-cffi-bindings==21.2.0
202 | - asttokens==2.0.5
203 | - av==8.0.3
204 | - babel==2.10.1
205 | - backcall==0.2.0
206 | - beautifulsoup4==4.11.1
207 | - bleach==5.0.0
208 | - bottle==0.12.19
209 | - bottle-websocket==0.2.9
210 | - debugpy==1.6.0
211 | - defusedxml==0.7.1
212 | - eel==0.14.0
213 | - entrypoints==0.4
214 | - executing==0.8.3
215 | - fastjsonschema==2.15.3
216 | - future==0.18.2
217 | - fvcore==0.1.5.post20211019
218 | - gevent==21.1.2
219 | - gevent-websocket==0.10.1
220 | - greenlet==1.1.0
221 | - higher==0.2.1
222 | - imageio-ffmpeg==0.4.5
223 | - importlib-resources==5.7.1
224 | - iopath==0.1.9
225 | - ipykernel==6.13.0
226 | - ipython==8.3.0
227 | - ipython-genutils==0.2.0
228 | - jedi==0.18.1
229 | - jinja2==3.1.2
230 | - json5==0.9.8
231 | - jsonschema==4.5.1
232 | - jupyter-client==7.3.1
233 | - jupyter-core==4.10.0
234 | - jupyter-server==1.17.0
235 | - jupyterlab==3.4.2
236 | - jupyterlab-pygments==0.2.2
237 | - jupyterlab-server==2.13.0
238 | - markupsafe==2.1.1
239 | - matplotlib-inline==0.1.3
240 | - mistune==0.8.4
241 | - moviepy==1.0.3
242 | - nbclassic==0.3.7
243 | - nbclient==0.6.3
244 | - nbconvert==6.5.0
245 | - nbformat==5.4.0
246 | - nest-asyncio==1.5.5
247 | - notebook==6.4.11
248 | - notebook-shim==0.1.0
249 | - opencv-python==4.5.3.56
250 | - optoth==0.2.0
251 | - pad2d-op-v1==1.0
252 | - pandocfilters==1.5.0
253 | - parameterized==0.8.1
254 | - parso==0.8.3
255 | - perlin-noise==1.12
256 | - pexpect==4.8.0
257 | - pickleshare==0.7.5
258 | - portalocker==2.3.2
259 | - proglog==0.1.9
260 | - prometheus-client==0.14.1
261 | - prompt-toolkit==3.0.29
262 | - ptyprocess==0.7.0
263 | - pure-eval==0.2.2
264 | - pygments==2.12.0
265 | - pyrsistent==0.18.1
266 | - pytorchvideo==0.1.3
267 | - pyzmq==22.3.0
268 | - send2trash==1.8.0
269 | - sniffio==1.2.0
270 | - soupsieve==2.3.2.post1
271 | - stack-data==0.2.0
272 | - tabulate==0.8.9
273 | - terminado==0.15.0
274 | - tinycss2==1.1.1
275 | - torch-dct==0.1.5
276 | - torch-tb-profiler==0.3.1
277 | - torchmetrics==0.11.1
278 | - tqdm==4.62.0
279 | - traitlets==5.2.1.post0
280 | - ttach==0.0.3
281 | - wcwidth==0.2.5
282 | - webencodings==0.5.1
283 | - websocket-client==1.3.2
284 | - whichcraft==0.6.1
285 | - yacs==0.1.8
286 | - zope-event==4.5.0
287 | - zope-interface==5.4.0
288 | prefix: /opt/python_envs/anaconda3/envs/granules
289 |
--------------------------------------------------------------------------------
/main.py:
--------------------------------------------------------------------------------
1 | import random
2 | from types import SimpleNamespace
3 | import imageio
4 | import numpy as np
5 | import argparse
6 | import sys
7 | import os
8 | import json
9 | from tqdm.auto import tqdm
10 | import matplotlib.pyplot as plt
11 | import yaml
12 |
13 | import einops
14 | import torch
15 | import torch.nn as nn
16 | from torch.optim import Adam
17 | from torch.utils.data import DataLoader
18 | from torch.utils.tensorboard import SummaryWriter
19 |
20 | from SimulationHelper.simulation import Simulation
21 | from datasets.config_dl import config_dl
22 | from models import ddpm
23 | import trainer
24 |
25 | # Setting reproducibility
26 | SEED = 0
27 | random.seed(SEED)
28 | np.random.seed(SEED)
29 | torch.manual_seed(SEED)
30 |
31 | parser = argparse.ArgumentParser("")
32 | parser.add_argument(
33 | "--config", default="cfg/monuseg.yaml", type=str, help="path to .yaml config" # glas, monuseg
34 | )
35 | args = parser.parse_args()
36 |
37 | def count_parameters(net):
38 | return sum(p.numel() for p in net.parameters() if p.requires_grad)
39 |
40 | if __name__ == "__main__":
41 | # program arguments
42 | with open(args.config) as file:
43 | yaml_cfg = yaml.safe_load(file)
44 | cfg = json.loads(
45 | json.dumps(yaml_cfg), object_hook=lambda d: SimpleNamespace(**d)
46 | )
47 |
48 | device = torch.device("cuda")
49 | print(f"Using device: {device}\t" + (f"{torch.cuda.get_device_name(0)}"))
50 |
51 | # set up dataloader, model
52 | train_dl, test_dl = config_dl(cfg)
53 | if cfg.model.type == 'unet':
54 | model = ddpm.Network(
55 | dim=cfg.model.dim,
56 | channels=cfg.model.n_cin,
57 | cond_channels=cfg.model.n_cin_cond,
58 | init_dim=cfg.model.n_fm,
59 | dim_mults=tuple(cfg.model.mults),
60 | embedding=cfg.model.embedding,
61 | img_cond=cfg.general.img_cond,
62 | with_class_label_emb=cfg.general.with_class_label_emb,
63 | class_label_cond=cfg.general.class_label_cond,
64 | num_classes=cfg.general.num_classes,
65 | ).to(device)
66 |
67 | else:
68 | raise ValueError('Unknown model type!')
69 |
70 | # optimizer
71 | optim = Adam(model.parameters(), cfg.learning.lr)
72 |
73 | # Optionally, load a pre-trained model that will be further trained
74 | if cfg.general.resume_training:
75 | load_path = os.getcwd() + "/runs/" + cfg.general.modality
76 | load_path += "/" + cfg.general.load_path + "/models/"
77 | fnames = sorted([fname for fname in os.listdir(load_path) if fname.endswith(".pt")])
78 |
79 | model.load_state_dict(
80 | torch.load(load_path + fnames[-1], map_location=device)["state_dict"],
81 | strict=False,
82 | )
83 | print("\nINFO: succesfully retrieved learned model params from specified cfg dir/epoch!")
84 |
85 | # load optimizer state dict
86 | optim.load_state_dict(torch.load(load_path + fnames[-1], map_location=device)["optimizer"])
87 | print("\nINFO: succesfully retrieved optim state dict specified cfg dir/epoch!")
88 |
89 | # network params
90 | print("\nNetwork has %i params" % count_parameters(model))
91 |
92 | # simulation
93 | sim_name = str(cfg.general.modality)
94 | with Simulation(
95 | sim_name=sim_name, output_root=f'{os.path.join(os.getcwd(), "runs/")}'
96 | ) as simulation:
97 | writer = SummaryWriter(os.path.join(simulation.outdir, "tensorboard"))
98 | cfg.inference.load_exp = simulation.outdir.split("/")[-1]
99 | with open(os.path.join(simulation.outdir, "cfg.yaml"), "w") as f:
100 | yaml.dump({k: v.__dict__ for k, v in cfg.__dict__.items()}, f)
101 |
102 | # training
103 | if (cfg.general.corr_mode == "diffusion" or cfg.general.corr_mode == "diffusion_ls"):
104 | noise_level_dict={'s1': cfg.SMLD.sigma_1_m, 'sL': cfg.SMLD.sigma_L_m, 'L': cfg.SMLD.n_steps}
105 | beta_dict = {'beta1': cfg.SMLD.beta_1, 'betaT': cfg.SMLD.beta_T, 'T': cfg.SMLD.T}
106 |
107 | trainer.TrainScoreNetwork(noise_level_dict,beta_dict,sde=cfg.SMLD.sde,model_type=cfg.model.type,train_objective=cfg.SMLD.objective,loss_power=cfg.learning.loss,n_val=cfg.learning.n_val,val_dl=train_dl).do(
108 | model,
109 | train_dl,
110 | cfg.learning.epochs,
111 | cfg.learning.clip,
112 | optim=optim,
113 | device=device,
114 | simulation=simulation,
115 | writer=writer,
116 | img_cond=cfg.general.img_cond,
117 | class_label_cond=cfg.general.class_label_cond,
118 | )
119 |
--------------------------------------------------------------------------------
/models/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leabogensperger/generative-segmentation-sdf/3b8037e3e03d347a41f361f6ba844e6928240200/models/__init__.py
--------------------------------------------------------------------------------
/models/ddpm.py:
--------------------------------------------------------------------------------
1 | import math
2 | from inspect import isfunction
3 | from functools import partial
4 | import matplotlib.pyplot as plt
5 | from tqdm.auto import tqdm
6 | from einops import rearrange
7 | import numpy as np
8 |
9 | import torch
10 | from torch import nn, einsum
11 | import torch.nn.functional as F
12 |
13 | # taken and adapted from https://huggingface.co/blog/annotated-diffusion
14 |
15 | def exists(x):
16 | return x is not None
17 |
18 |
19 | def default(val, d):
20 | if exists(val):
21 | return val
22 | return d() if isfunction(d) else d
23 |
24 |
25 | class Residual(nn.Module):
26 | def __init__(self, fn):
27 | super().__init__()
28 | self.fn = fn
29 |
30 | def forward(self, x, *args, **kwargs):
31 | return self.fn(x, *args, **kwargs) + x
32 |
33 |
34 | def Upsample(dim):
35 | return nn.ConvTranspose2d(dim, dim, 4, 2, 1)
36 |
37 |
38 | def Downsample(dim):
39 | return nn.Conv2d(dim, dim, 4, 2, 1)
40 |
41 |
42 | class SinusoidalPositionEmbeddings(nn.Module):
43 | def __init__(self, dim):
44 | super().__init__()
45 | self.dim = dim
46 |
47 | def forward(self, time):
48 | device = time.device
49 | half_dim = self.dim // 2
50 | embeddings = math.log(10000) / (half_dim - 1)
51 | embeddings = torch.exp(torch.arange(half_dim, device=device) * -embeddings)
52 | embeddings = (
53 | time * embeddings[None, :]
54 | ) # time is already in the shape [batchsize,1]
55 | embeddings = torch.cat((embeddings.sin(), embeddings.cos()), dim=-1)
56 | return embeddings
57 |
58 | class GaussianFourierProjection(nn.Module): # for continuous training
59 | """Gaussian Fourier embeddings for noise levels.
60 | taken from https://github.com/yang-song/score_sde_pytorch/blob/cb1f359f4aadf0ff9a5e122fe8fffc9451fd6e44/models/layerspp.py#L32
61 | """
62 |
63 | def __init__(self, dim=256, scale=16.0):
64 | super().__init__()
65 | dim = dim//2
66 | self.W = nn.Parameter(torch.randn(dim) * scale, requires_grad=False)
67 |
68 | def forward(self, x):
69 | x_proj = x* self.W[None, :] * 2 * np.pi
70 | return torch.cat([torch.sin(x_proj), torch.cos(x_proj)], dim=-1)
71 |
72 |
73 | class Block(nn.Module):
74 | def __init__(self, dim, dim_out, groups=8):
75 | super().__init__()
76 | self.proj = nn.Conv2d(dim, dim_out, 3, padding=1)
77 | self.norm = nn.GroupNorm(groups, dim_out)
78 | self.act = nn.SiLU()
79 |
80 | def forward(self, x, scale_shift=None):
81 | x = self.proj(x)
82 | x = self.norm(x)
83 |
84 | if exists(scale_shift):
85 | scale, shift = scale_shift
86 | x = x * (scale + 1) + shift
87 |
88 | x = self.act(x)
89 | return x
90 |
91 |
92 | class ResnetBlock(nn.Module):
93 | """https://arxiv.org/abs/1512.03385"""
94 |
95 | def __init__(
96 | self,
97 | dim,
98 | dim_out,
99 | *,
100 | time_emb_dim=None,
101 | groups=8,
102 | class_label_cond=False,
103 | num_classes=None
104 | ):
105 | super().__init__()
106 | self.mlp = nn.Sequential(nn.SiLU(), nn.Linear(time_emb_dim, dim_out)) if exists(time_emb_dim) else None
107 |
108 | if class_label_cond:
109 | # TODO time_emb_dim, class_emd_dim should have their own params for dimentionality.
110 | self.class_label_mlp = nn.Sequential(
111 | nn.SiLU(), nn.Linear(time_emb_dim, dim_out)
112 | )
113 |
114 | self.block1 = Block(dim, dim_out, groups=groups)
115 | self.block2 = Block(dim_out, dim_out, groups=groups)
116 | self.res_conv = nn.Conv2d(dim, dim_out, 1) if dim != dim_out else nn.Identity()
117 |
118 | self.num_classees = num_classes
119 | self.class_label_cond = class_label_cond
120 |
121 | def forward(self, x, time_emb=None, class_lbl=None):
122 | h = self.block1(x)
123 |
124 | if exists(self.mlp) and exists(time_emb):
125 | time_emb = self.mlp(time_emb)
126 | h = rearrange(time_emb, "b c -> b c 1 1") + h
127 |
128 | if self.class_label_cond is True:
129 | class_lbl_emb = self.class_label_mlp(
130 | class_lbl
131 | ) # Bring the lbl_emb to correct shape.
132 | h = rearrange(class_lbl_emb, "b c -> b c 1 1") + h
133 |
134 | h = self.block2(h)
135 | return h + self.res_conv(x)
136 |
137 |
138 | class Attention(nn.Module):
139 | def __init__(self, dim, heads=4, dim_head=32):
140 | super().__init__()
141 | self.scale = dim_head**-0.5
142 | self.heads = heads
143 | hidden_dim = dim_head * heads
144 | self.to_qkv = nn.Conv2d(dim, hidden_dim * 3, 1, bias=False)
145 | self.to_out = nn.Conv2d(hidden_dim, dim, 1)
146 |
147 | def forward(self, x):
148 | b, c, h, w = x.shape
149 | qkv = self.to_qkv(x).chunk(3, dim=1)
150 | q, k, v = map(
151 | lambda t: rearrange(t, "b (h c) x y -> b h c (x y)", h=self.heads), qkv
152 | )
153 | q = q * self.scale
154 |
155 | sim = einsum("b h d i, b h d j -> b h i j", q, k)
156 | sim = sim - sim.amax(dim=-1, keepdim=True).detach()
157 | attn = sim.softmax(dim=-1)
158 |
159 | out = einsum("b h i j, b h d j -> b h i d", attn, v)
160 | out = rearrange(out, "b h (x y) d -> b (h d) x y", x=h, y=w)
161 | return self.to_out(out)
162 |
163 |
164 | class LinearAttention(nn.Module):
165 | def __init__(self, dim, heads=4, dim_head=32):
166 | super().__init__()
167 | self.scale = dim_head**-0.5
168 | self.heads = heads
169 | hidden_dim = dim_head * heads
170 | self.to_qkv = nn.Conv2d(dim, hidden_dim * 3, 1, bias=False)
171 |
172 | self.to_out = nn.Sequential(nn.Conv2d(hidden_dim, dim, 1), nn.GroupNorm(1, dim))
173 |
174 | def forward(self, x):
175 | b, c, h, w = x.shape
176 | qkv = self.to_qkv(x).chunk(3, dim=1)
177 | q, k, v = map(
178 | lambda t: rearrange(t, "b (h c) x y -> b h c (x y)", h=self.heads), qkv
179 | )
180 |
181 | q = q.softmax(dim=-2)
182 | k = k.softmax(dim=-1)
183 |
184 | q = q * self.scale
185 | context = torch.einsum("b h d n, b h e n -> b h d e", k, v)
186 |
187 | out = torch.einsum("b h d e, b h d n -> b h e n", context, q)
188 | out = rearrange(out, "b h c (x y) -> b (h c) x y", h=self.heads, x=h, y=w)
189 | return self.to_out(out)
190 |
191 |
192 | class PreNorm(nn.Module):
193 | def __init__(self, dim, fn):
194 | super().__init__()
195 | self.fn = fn
196 | self.norm = nn.GroupNorm(1, dim)
197 |
198 | def forward(self, x):
199 | x = self.norm(x)
200 | return self.fn(x)
201 |
202 |
203 | class Network(nn.Module):
204 | def __init__(
205 | self,
206 | dim,
207 | init_dim=None,
208 | out_dim=None,
209 | dim_mults=(1, 2, 4, 8),
210 | channels=1, # channels of sdf input
211 | cond_channels=1, # rgb vs gray input for conditioning
212 | embedding='sinusoidal',
213 | with_time_emb=True,
214 | resnet_block_groups=2,
215 | img_cond=None,
216 | with_class_label_emb=False,
217 | class_label_cond=False,
218 | num_classes=None,
219 | ):
220 | super().__init__()
221 |
222 | self.embedding = embedding
223 | assert self.embedding == 'fourier' or self.embedding == 'sinusoidal'
224 |
225 | self.class_label_cond = class_label_cond
226 | self.num_classes = num_classes
227 | self.with_class_label_emb = with_class_label_emb
228 |
229 | block_klass = partial(ResnetBlock, groups=resnet_block_groups)
230 |
231 | # time embeddings
232 | if with_time_emb:
233 | time_dim = dim * 4
234 | self.time_mlp = nn.Sequential(
235 | GaussianFourierProjection(dim) if self.embedding == 'fourier' else SinusoidalPositionEmbeddings(dim), # TODO: include option which embedding type to use
236 | # SinusoidalPositionEmbeddings(dim),
237 | nn.Linear(dim, time_dim),
238 | nn.GELU(),
239 | nn.Linear(time_dim, time_dim),
240 | )
241 | else:
242 | raise Exception("Time embedding is set to False, None of the other code can deal with it. Think. Idiot.")
243 |
244 | # class_label embeddings
245 | if class_label_cond == True:
246 | if with_class_label_emb:
247 | class_label_dim = dim * 4
248 | self.class_label_embedding_mlp = nn.Sequential(
249 | SinusoidalPositionEmbeddings(dim),
250 | nn.Linear(dim, class_label_dim),
251 | nn.GELU(),
252 | nn.Linear(class_label_dim, class_label_dim),
253 | )
254 | else:
255 | class_label_dim = dim * 4
256 | self.class_label_embedding_mlp = nn.Sequential(
257 | nn.Linear(dim, class_label_dim),
258 | nn.GELU(),
259 | nn.Linear(class_label_dim, class_label_dim),
260 | )
261 | else:
262 | self.class_label_embedding_mlp = None
263 |
264 | # determine dimensions
265 | self.channels = channels
266 | self.cond_channels = cond_channels
267 |
268 | # conditioning branch at beginning (SegDiff style)
269 | if img_cond == 1:
270 | self.encoder_img = nn.Sequential(
271 | block_klass(channels, init_dim, time_emb_dim=time_dim)
272 | )
273 | self.encoder_mask = nn.Sequential(
274 | block_klass(cond_channels, init_dim, time_emb_dim=time_dim)
275 | )
276 | init_dim *= 2
277 | channels = init_dim
278 |
279 | init_dim = default(init_dim, dim // 3 * 2)
280 | self.init_conv = nn.Conv2d(channels, init_dim, 7, padding=3)
281 |
282 | dims = [init_dim, *map(lambda m: dim * m, dim_mults)]
283 | in_out = list(zip(dims[:-1], dims[1:]))
284 |
285 | # layers
286 | self.downs = nn.ModuleList([])
287 | self.ups = nn.ModuleList([])
288 | num_resolutions = len(in_out)
289 |
290 | for ind, (dim_in, dim_out) in enumerate(in_out):
291 | is_last = ind >= (num_resolutions - 1)
292 |
293 | self.downs.append(
294 | nn.ModuleList(
295 | [
296 | block_klass(
297 | dim_in,
298 | dim_out,
299 | time_emb_dim=time_dim,
300 | class_label_cond=class_label_cond,
301 | num_classes=num_classes,
302 | ),
303 | block_klass(
304 | dim_out,
305 | dim_out,
306 | time_emb_dim=time_dim,
307 | class_label_cond=class_label_cond,
308 | num_classes=num_classes,
309 | ),
310 | Residual(PreNorm(dim_out, LinearAttention(dim_out))),
311 | Downsample(dim_out) if not is_last else nn.Identity(),
312 | ]
313 | )
314 | )
315 |
316 | mid_dim = dims[-1]
317 | self.mid_block1 = block_klass(mid_dim, mid_dim, time_emb_dim=time_dim)
318 | self.mid_attn = Residual(PreNorm(mid_dim, Attention(mid_dim)))
319 | self.mid_block2 = block_klass(mid_dim, mid_dim, time_emb_dim=time_dim)
320 |
321 | for ind, (dim_in, dim_out) in enumerate(reversed(in_out[1:])):
322 | is_last = ind >= (num_resolutions - 1)
323 |
324 | self.ups.append(
325 | nn.ModuleList(
326 | [
327 | block_klass(
328 | dim_out * 2,
329 | dim_in,
330 | time_emb_dim=time_dim,
331 | class_label_cond=class_label_cond,
332 | num_classes=num_classes,
333 | ),
334 | block_klass(
335 | dim_in,
336 | dim_in,
337 | time_emb_dim=time_dim,
338 | class_label_cond=class_label_cond,
339 | num_classes=num_classes,
340 | ),
341 | Residual(PreNorm(dim_in, LinearAttention(dim_in))),
342 | Upsample(dim_in) if not is_last else nn.Identity(),
343 | ]
344 | )
345 | )
346 |
347 | out_dim = default(out_dim, self.channels)
348 | self.final_conv = nn.Sequential(
349 | block_klass(dim, dim, time_emb_dim=None), nn.Conv2d(dim, out_dim, 1)
350 | )
351 |
352 | def forward(self, x, time, img_cond=None, class_lbl=None):
353 | if img_cond is not None:
354 | # x = self.encoder_img(x) + self.encoder_mask(cond)
355 | x = torch.cat((self.encoder_img(x), self.encoder_mask(img_cond)), 1)
356 |
357 | x = self.init_conv(x)
358 |
359 | t = self.time_mlp(time) if exists(self.time_mlp) else None
360 | class_lbl = (
361 | self.class_label_embedding_mlp(class_lbl)
362 | if exists(self.class_label_embedding_mlp)
363 | else None
364 | )
365 |
366 | h = []
367 |
368 | # downsample
369 | for block1, block2, attn, downsample in self.downs:
370 | x = block1(x, t, class_lbl)
371 | x = block2(x, t, class_lbl)
372 | x = attn(x)
373 | h.append(x)
374 | x = downsample(x)
375 |
376 | # bottleneck
377 | x = self.mid_block1(x, t, class_lbl)
378 | x = self.mid_attn(x)
379 | x = self.mid_block2(x, t, class_lbl)
380 |
381 | # upsample
382 | for block1, block2, attn, upsample in self.ups:
383 | x = torch.cat((x, h.pop()), dim=1)
384 | x = block1(x, t, class_lbl)
385 | x = block2(x, t, class_lbl)
386 | x = attn(x)
387 | x = upsample(x)
388 |
389 | return self.final_conv(x)
--------------------------------------------------------------------------------
/preprocess_data/precompute_sdf.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "code",
5 | "execution_count": 4,
6 | "metadata": {},
7 | "outputs": [],
8 | "source": [
9 | "import numpy as np\n",
10 | "import matplotlib.pyplot as plt\n",
11 | "from skimage.draw import ellipse\n",
12 | "from skimage.morphology import binary_erosion\n",
13 | "from scipy.ndimage import distance_transform_edt"
14 | ]
15 | },
16 | {
17 | "cell_type": "code",
18 | "execution_count": 37,
19 | "metadata": {},
20 | "outputs": [],
21 | "source": [
22 | "# generate toy mask\n",
23 | "N = 128\n",
24 | "m = np.zeros((N, N), dtype=int)\n",
25 | "\n",
26 | "# Define parameters for the ellipses (center, semi-axes, and orientation)\n",
27 | "ellipses = [\n",
28 | " (30, 40, 20, 20, np.deg2rad(30)), # (row, col, semi-major, semi-minor, rotation)\n",
29 | " (20, 80, 10, 20, np.deg2rad(75)),\n",
30 | " (90, 60, 30, 40, np.deg2rad(15)),\n",
31 | "]\n",
32 | "\n",
33 | "# Draw each ellipse on the mask\n",
34 | "for (r, c, r_radius, c_radius, angle) in ellipses:\n",
35 | " rr, cc = ellipse(r, c, r_radius, c_radius, rotation=angle, shape=m.shape)\n",
36 | " m[rr, cc] = 1"
37 | ]
38 | },
39 | {
40 | "cell_type": "code",
41 | "execution_count": 38,
42 | "metadata": {},
43 | "outputs": [
44 | {
45 | "data": {
46 | "image/png": "iVBORw0KGgoAAAANSUhEUgAAAigAAAEhCAYAAABV6lvQAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8fJSN1AAAACXBIWXMAAA9hAAAPYQGoP6dpAABaYklEQVR4nO3dd3QUZdvH8e9syaYSUiCFBBJ6LyItiqAUpVlQEVDBjlIUQVBsoPKAAiI+0qyAIqCvIiryoNgQBST0jpTQCREI6WXL/f4RsxISIMBuZje5PufkHHZ2ZufayebHtVPu0ZRSCiGEEEIID2LQuwAhhBBCiPNJgyKEEEIIjyMNihBCCCE8jjQoQgghhPA40qAIIYQQwuNIgyKEEEIIjyMNihBCCCE8jjQoQgghhPA40qAIIYQQwuNIg+KFxo0bh6ZpnDp16pLzduzYkY4dO7q/qHLu4MGDaJrGlClT9C5FVDDLli1j3LhxutawYMECpk2b5pbXjouL44EHHrjoPN7+9zd37lw0TePgwYMufd0HHniAuLg4l76mJ5EGpZybOXMmM2fO1LsMIcQVWrZsGa+88oquNbizQRFX7qWXXuKrr77Suwy3MeldgHCvhg0b6rLenJwcfH190TRNl/ULUREppcjNzcXPz0/vUoQbZWdn4+/vT61atfQuxa1kD4oXO3LkCL1796ZSpUoEBwdz33338ffffxeZ5/xDPOfuKp06dSrx8fEEBgbSrl071q5dW2TZ9evX07dvX+Li4vDz8yMuLo5+/fpx6NChIvMV7r784YcfeOihh6hSpQr+/v78/vvvaJrGwoULi9X+8ccfo2kaiYmJF3x/ha/7888/8+ijjxIWFkalSpUYMGAAWVlZJCcn06dPHypXrkxUVBTPPPMMVqu1yGu88sortGnThtDQUCpVqsQ111zDhx9+yPn3yPz555/p2LEjYWFh+Pn5Ub16de68806ys7MvWJ/VamXgwIEEBgaydOnSC84nKp69e/fSv39/qlatisVioUGDBsyYMcP5fG5uLi1atKB27dqkpaU5pycnJxMZGUnHjh2x2+088MADzuU0TXP+FB4q0DSNoUOHMnv2bBo0aIDFYmHevHlA6T/7ULCHpF27dgQGBhIYGEjz5s358MMPgYIM+e677zh06FCRGgrl5+czfvx46tevj8VioUqVKjz44IPFsshqtTJ69GgiIyPx9/fn+uuvZ926dZe1XR0OB//5z3+oXr06vr6+XHvttfz000/F5vv999/p1KkTQUFB+Pv7k5CQwHfffVdknsJD5ecr6XBMXFwcPXv2ZPny5VxzzTX4+flRv359Pvroo2LLr127luuuuw5fX1+io6MZM2ZMsVwC+Oyzz+jatStRUVH4+fnRoEEDnnvuObKysorM98ADDxAYGMi2bdvo2rUrQUFBdOrUyfnc+Yd4lFLMnDmT5s2b4+fnR0hICHfddRcHDhwoMt+mTZvo2bOn8zMaHR1Njx49OHr0aLFa9SJ7ULzYHXfcQZ8+fXj88cfZsWMHL730Ejt37uTPP//EbDZfdNkZM2ZQv359527bl156ie7du5OUlERwcDBQ0MzUq1ePvn37EhoayokTJ5g1axatWrVi586dhIeHF3nNhx56iB49evDJJ5+QlZVFQkICLVq0YMaMGfTr16/IvNOnT6dVq1a0atXqku/zkUceoXfv3ixatIhNmzbx/PPPY7PZ2LNnD7179+axxx7jxx9/5I033iA6OpoRI0Y4lz148CCDBg2ievXqQEF4DBs2jGPHjvHyyy875+nRowft27fno48+onLlyhw7dozly5eTn5+Pv79/sZrOnj1L79692bVrFytXrqRly5aXfB+iYti5cycJCQlUr16dN998k8jISL7//nuefPJJTp06xdixY/H19eXzzz+nZcuWPPTQQ3z55Zc4HA7uvfdelFIsXLgQo9HISy+9RFZWFl988QVr1qxxriMqKsr57yVLlrBq1SpefvllIiMjqVq1KlC6zz7Ayy+/zGuvvUbv3r0ZOXIkwcHBbN++3flFZObMmTz22GPs37+/2OEEh8PBbbfdxqpVqxg9ejQJCQkcOnSIsWPH0rFjR9avX+/cm/Poo4/y8ccf88wzz9ClSxe2b99O7969ycjIKPW2nT59OjVq1GDatGk4HA4mTZpEt27dWLlyJe3atQNg5cqVdOnShaZNm/Lhhx9isViYOXMmvXr1YuHChdxzzz2X8+t02rJlCyNHjuS5554jIiKCDz74gIcffpjatWtzww03AAW/+06dOhEXF8fcuXPx9/dn5syZLFiwoNjr7d27l+7duzN8+HACAgLYvXs3b7zxBuvWrePnn38uMm9+fj633norgwYN4rnnnsNms12wzkGDBjF37lyefPJJ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TdMwmUxFDmXl5OTwySefXHAZo9FImzZtqF+/Pp9++ikbN24s0qAYDAbeffddAgMDeeCBB8jKyuKJJ55w6/sQ3sPf358bb7yRTZs20bRpU+fevgt5/PHHOXz4MOvWrWP37t3cddddvPXWWzz99NPOec7du1nay4dL+9nv2rUrRqORWbNmOU8yLcn5ewoK9ezZk0WLFmG322nTps0Fly/MpAtlVmktXryYyZMnO5uLjIwMvv32W9q3b4/RaCQgIIA2bdqwePFipkyZ4txeDoeD+fPnExMTQ926dQGcV79s3bq1yMn63377banrOd+NN97IN998w8mTJ52Heex2O5999lmR+QobjvPz9d13373idUPB7+P111/n2LFj9OnTp1TLaJpGs2bNeOutt5g7dy4bN268qhpcSRoUL7Z48WJMJhNdunRxXsXTrFmzUn8wL6ZSpUrccMMNTJ48mfDwcOLi4li5ciUffvjhFR3aeOqpp5wBNmfOnKuurzR69OjB1KlT6d+/P4899hinT59mypQpxUJh9uzZ/Pzzz/To0YPq1auTm5vrvHywc+fOJb72m2++SVBQEIMHDyYzM5NRo0a5/f0I7/D2229z/fXX0759e5544gni4uLIyMhg3759fPvtt84rND744APmz5/PnDlzaNSoEY0aNWLo0KE8++yzXHfddbRu3Roo+KYL8MYbb9CtWzeMRuMlm5/Sfvbj4uJ4/vnnee2118jJyaFfv34EBwezc+dOTp065RwgrkmTJixevJhZs2bRsmVLDAYD1157LX379uXTTz+le/fuPPXUU7Ru3Rqz2czRo0f55ZdfuO2227jjjjto0KAB9913H9OmTcNsNtO5c2e2b9/OlClTSnXoopDRaKRLly6MGDECh8PBG2+8QXp6epGB7CZOnEiXLl248cYbeeaZZ/Dx8WHmzJls376dhQsXOpuD7t27ExoaysMPP8yrr76KyWRi7ty5HDlypNT1nO/FF1/km2++4aabbuLll1/G39+fGTNmFLt0OCEhgZCQEB5//HHGjh2L2Wzm008/ZcuWLVe8boDrrruOxx57jAcffJD169dzww03EBAQwIkTJ/j9999p0qQJTzzxBEuXLmXmzJncfvvt1KxZE6UUixcv5uzZs5d9DpRb6XqKrrgihWefb9iwQfXq1UsFBgaqoKAg1a9fP3Xy5Mki817o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",
47 | "text/plain": [
48 | ""
49 | ]
50 | },
51 | "metadata": {},
52 | "output_type": "display_data"
53 | }
54 | ],
55 | "source": [
56 | "# extract boundaries\n",
57 | "m_bd = np.abs(binary_erosion(m) - m)\n",
58 | "\n",
59 | "fig, ax = plt.subplots(1,2)\n",
60 | "ax[0].imshow(m), ax[0].set_title('binary mask')\n",
61 | "ax[1].imshow(m_bd), ax[1].set_title('extracted boundaries')\n",
62 | "plt.show()"
63 | ]
64 | },
65 | {
66 | "cell_type": "code",
67 | "execution_count": 55,
68 | "metadata": {},
69 | "outputs": [
70 | {
71 | "data": {
72 | "image/png": "iVBORw0KGgoAAAANSUhEUgAAAigAAAEhCAYAAABV6lvQAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8fJSN1AAAACXBIWXMAAA9hAAAPYQGoP6dpAADWNUlEQVR4nOx9aaAdVZX12udU3fvyMgEBQkIYwiSjAoIIiuAnYCutKLZMdiPSIn7QCtItijiArSCoiC0KajeIA06tn+IsitIqKCCggDYOQEAgBBGSkOTeW3X2/n7sc05V3fdeBvJe3pCzuss733tuvVC17tprr00iIkhISEhISEhImEAw472AhISEhISEhIR+JIKSkJCQkJCQMOGQCEpCQkJCQkLChEMiKAkJCQkJCQkTDomgJCQkJCQkJEw4JIKSkJCQkJCQMOGQCEpCQkJCQkLChEMiKAkJCQkJCQkTDomgJCQkJCQkJEw4JIIyCXHeeeeBiPDXv/51jc899NBDceihh479oqY47r//fhARPvShD433UhImEH71q1/hla98Jbbddlu0223MnTsXBx54IP71X/+18bxDDz0URAQigjEGM2fOxE477YRXv/rV+O///m8w85D33n777eNr+rennnpqxDU9/PDDOO+883DHHXeM9tcdE9x+++045JBDMHv2bBARLr300vFe0qggHDM+85nPjPdS1hoT7TiXjfcCEsYWn/jEJ8Z7CQkJUxLf+c538PKXvxyHHnooLr74YsybNw+PPPIIbr31VnzpS1/Chz/84cbzd9hhB3zhC18AAKxYsQL33XcfvvGNb+DVr341Dj74YHzrW9/C7NmzG6953vOeN+zJYnBwcMR1Pfzwwzj//POx/fbbY++9917/LzrGOPnkk7FixQp86Utfwqabbortt99+vJeUMEGQCMoUx+677z4un7tq1SoMDAyAiMbl8xMSxhoXX3wxFi5ciB/84AfIsupQetxxx+Hiiy8e8vxp06bhuc99buO+17/+9bjqqqtw8skn4w1veAO+/OUvNx7fZJNNhrxmtLFy5crVEp6xxl133YVTTjkFL3nJS0bl/YqiABE1/iYJkxOpxDOJ8eCDD+Loo4/GrFmzMHv2bPzjP/4jHnvsscZz+ks8dQnvkksuwcKFCzFjxgwceOCB+OUvf9l47a233orjjjsO22+/PaZNm4btt98exx9/PBYtWtR43mc+8xkQEX74wx/i5JNPxhZbbIHBwUH8/Oc/BxHhi1/84pC1f/aznwUR4ZZbbhnx+4X3vf7663HKKadgzpw5mDVrFk488USsWLECixcvxjHHHINNNtkE8+bNw7/927+hKIrGe5x//vk44IADsNlmm2HWrFnYd9998V//9V/on5F5/fXX49BDD8WcOXMwbdo0bLvttnjVq16FlStXjri+oijw2te+FjNmzMC3v/3tEZ+XMDXx+OOPY/PNNx/2RGjM2h9aX/e61+GlL30pvvrVrw75b2td8dOf/hT7779/fN9QEjrvvPMAACeddBJmzJiBO++8E0cccQRmzpyJF73oRQCA6667DkcddRQWLFiAgYEB7LTTTjj11FOHlJJDifnuu+/G8ccfj9mzZ2Pu3Lk4+eSTsXTp0sZzv/rVr+KAAw7A7NmzMTg4iB122AEnn3wygOq/77Iscfnll8e1Btx111046qijsOmmm2JgYAB77703rr766iHfl4jwuc99Dv/6r/+KrbfeGu12G3/605/id/3f//1fvPjFL8b06dMxb948fOADHwAA/PKXv8Tzn/98TJ8+HbvsssuQ9waAxYsX49RTT8WCBQvQarWwcOFCnH/++SjLsvG8hx9+GMcccwxmzpyJ2bNn49hjj8XixYvX6m+WjnMjI1HMSYxXvvKVOOaYY/DGN74Rd999N971rnfhd7/7HX71q18hz/PVvvbjH/84dt1111jvfde73oWXvvSluO+++6LMfP/99+MZz3gGjjvuOGy22WZ45JFHcPnll2P//ffH7373O2y++eaN9zz55JNx5JFH4nOf+xxWrFiBgw46CPvssw8+/vGP4/jjj28897LLLsP+++8fD6arw+tf/3ocffTR+NKXvoTbb78d73jHO1CWJe655x4cffTReMMb3oAf/ehHuOiiizB//nycddZZ8bX3338/Tj31VGy77bYA9KD0pje9CQ899BDe/e53x+cceeSROPjgg3HllVdik002wUMPPYTvf//76PV6w/66fPLJJ3H00Ufj97//PW644QY8+9nPXuP3SJhaOPDAA/Gf//mfePOb34zXvOY12Hfffdf4391IePnLX47vfve7+NnPfobtttsu3i8iQ06GxpgRCdC+++6Lq666Cq973evwzne+E0ceeSQAYMGCBfE5vV4PL3/5y3Hqqafi7W9/e3z/P//5zzjwwAPx+te/HrNnz8b999+PSy65BM9//vNx5513Dvlur3rVq3Dsscfin//5n3HnnXfinHPOAQBceeWVAICbbroJxx57LI499licd955GBgYwKJFi3D99dcDAI488kjcdNNNOPDAA/EP//APDd/OPffcg4MOOghbbrkl/uM//gNz5szB5z//eZx00kl49NFHcfbZZzfWcs455+DAAw/EFVdcAWMMttxySwB6cj366KPxxje+EW9961txzTXX4JxzzsGyZcvwta99DW9729uwYMECfOxjH8NJJ52EPffcM/63vHjxYjznOc+BMQbvfve7seOOO+Kmm27C+973Ptx///246qqrAKhafNhhh+Hhhx/GhRdeiF122QXf+c53cOyxx67x715HOs4NA0mYdHjPe94jAOQtb3lL4/4vfOELAkA+//nPx/sOOeQQOeSQQ+Lt++67TwDIXnvtJWVZxvtvvvlmASBf/OIXR/zcsizlqaeekunTp8tHP/rReP9VV10lAOTEE08c8prw2O233z7ks66++urVfs/w2je96U2N+1/xilcIALnkkksa9++9996y7777jvh+zjkpikLe+973ypw5c4SZRUTkv//7vwWA3HHHHSO+Nuy3D37wg3LffffJ7rvvLrvvvrvcf//9q/0OCVMXf/3rX+X5z3++ABAAkue5HHTQQXLhhRfK8uXLG8895JBDZI899hjxvb73ve8JALnooovifdttt1187/p27rnnrnZdt9xyiwCQq666ashjr33tawWAXHnllat9D2aWoihk0aJFAkC++c1vxsfC8efiiy9uvOa0006TgYGB+N/Vhz70IQEgTz755Go/C4CcfvrpjfuOO+44abfb8sADDzTuf8lLXiKDg4PxPX/yk58IAHnBC14w4nf92te+Fu8rikK22GILASC33XZbvP/xxx8Xa62cddZZ8b5TTz1VZsyYIYsWLWq8b/hed999t4iIXH755UP2kYjIKaecMuLfoY50nBsZqcQzifGa17ymcfuYY45BlmX4yU9+ssbXHnnkkbDWxtvPfOYzAaAhMT/11FN429vehp122glZliHLMsyYMQMrVqzA73//+yHv+apXvWrIfccffzy23HJLfPzjH4/3fexjH8MWW2yx1r8w/v7v/75xe7fddovfof/+fon8+uuvx2GHHYbZs2fDWos8z/Hud78bjz/+OJYsWQIA2HvvvdFqtfCGN7wBV199Ne69994R13Lbbbfhuc99LubOnYtf/OIXjV+7CRsX5syZg5/97Ge45ZZb8IEPfABHHXUU/vCHP+Ccc87BXnvttVZddgHSJ8UHPP/5z8ctt9zS2E477bT1Xvtw/60uWbIEb3zjG7HNNtsgyzLkeR7/fQ/33/vLX/7yxu1nPvOZ6HQ68b+roI4ec8wx+MpXvoKHHnpordd3/fXX40UvehG22Wabxv0nnXQSVq5ciZtuummN3wcAiAgvfelL4+0sy7DTTjth3rx52GeffeL9m222GbbccsvG8ePb3/42XvjCF2L+/PkoyzJuwStzww03AAB+8pOfYObMmUP2xwknnLDW3xdIx7nhkAjKJMZWW23VuJ1lGebMmYPHH398ja+dM2dO43a73QagcmXACSecgMsuuwyvf/3r8YMf/AA333wzbrnlFmyxxRaN5wXMmzdvyH3tdhunnnoqrrnmGjz55JN47LHH8JWvfAWvf/3r42euCZtttlnjdqvVGvH+TqcTb99888044ogjAACf/vSn8Ytf/AK33HILzj333MZ33XHHHfGjH/0IW265JU4//XTsuOOO2HHHHfHRj350yFquu+46PProo3j961+PTTbZZK3WnzC1sd9+++Ftb3sbvvrVr+Lhhx/GW97yFtx///3DGmVHQjjhzJ8/v3H/7Nmzsd9++zW2/uesKwYHBzFr1qzGfcyMI444Al//+tdx9tln48c//jFuvvnm6Esb7r/3NR1DXvCCF+Ab3/gGyrLEiSeeiAULFmDPPfcc1pPWj8cff3zY40n47v3HuOGeG77rwMBA475WqzXk2BHurx8/Hn30UXzrW99CnueNbY899gCASEAff/xxzJ07d8j79R+f14R0nBuK5EGZxFi8eDG23nrreLssSzz++ONDDhxPB0uXLsW3v/1tvOc978Hb3/72eH+328Xf/va3YV8zUsfO//2//xcf+MAHcOWVV6LT6aAsS7zxjW9c7zWuCV/60peQ5zm+/e1vNw5S3/jGN4Y89+CDD8bBBx8M5xxuvfVWfOxjH8OZZ56JuXPn4rjjjovPe+tb34o///nPOPHEE+OBNyEhIM9zvOc978FHPvIR3HXXXWv9umuvvRZEhBe84AVjuDrFcP+d3nXXXfjNb36Dz3zmM3jta18b7//Tn/60Xp911FFH4aijjkK328Uvf/lLXHjhhTjhhBOw/fbb48ADDxzxdXPmzMEjjzwy5P6HH34YAIb438aiW3DzzTfHM5/5TLz//e8f9vFAlubMmYObb755yONra5JdX0zl41xSUCYxQqZCwFe+8hWUZTkqwWxEBBEZonL853/+J5xz6/Re8+bNw6tf/Wp84hOfwBVXXIGXvexl0cw1lgithvVS1qpVq/C5z31uxNdYa3HAAQfEktRtt93WeNwYg09+8pM444wzcNJJJ+Hyyy8fm8UnTHgMdwIFqnLI2iodV111Fb73ve/h+OOPH5X/LoZTQ9eEcILv/+/9k5/85HqvJ7zvIYccgosuugiAhrOtDi960Ytw/fXXR0IS8NnPfhaDg4Nj3noNaMnlrrvuwo477jhExaorWS984QuxfPlyXHvttY3XX3PNNWO+RmBqH+eSgjKJ8fWvfx1ZluHwww+PXTzPetazcMwxx6z3e8+aNQsveMEL8MEPfhCbb745tt9+e9xwww34r//6r6cl+Z1xxhk44IADACC638caRx55JC655BKccMIJeMMb3oDHH38cH/rQh4YchK+44gpcf/31OPLII7Htttui0+nEToTDDjts2Pf+8Ic/jJkzZ+K0007DU089hbe+9a1j/n0SJhZe/OIXY8GCBXjZy16GXXfdFcyMO+64Ax/+8IcxY8YMnHHGGY3nr1q1qlEyuffee/GNb3wD3/72t3HIIYfgiiuuGJV17bjjjpg2bRq+8IUvYLfddsOMGTMwf/781RKmXXfdFTvuuCPe/va3Q0Sw2Wab4Vvf+hauu+66p72Od7/73fjLX/6CF73oRViwYAGefPJJfPSjH0We5zjkkENW+9r3vOc90QPy7ne/G5ttthm+8IUv4Dvf+Q4uvvjiIYF2Y4H3vve9uO6663DQQQfhzW9+M57xjGeg0+ng/vvvx3e/+11cccUVWLBgAU488UR85CMfwYknnoj3v//92HnnnfHd734XP/jBD8Z8jcDUPs4lgjKJ8fWvfx3nnXdezBB42ctehksvvTTWLtcX11xzDc444wycffbZKMsSz3ve83DdddcNMW2tDZ7znOfEPJWQuzDW+D//5//gyiuvxEUXXYSXvexl2HrrrXHKKadgyy23xD//8z/H5+2999744Q9/iPe85z1YvHgxZsyYgT33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",
73 | "text/plain": [
74 | ""
75 | ]
76 | },
77 | "metadata": {},
78 | "output_type": "display_data"
79 | }
80 | ],
81 | "source": [
82 | "# extract signed distance function (SDF)\n",
83 | "distance = distance_transform_edt(np.where(m_bd==0., np.ones_like(m_bd), np.zeros_like(m_bd)))\n",
84 | "m_sdf = np.where(m == 1, distance*-1, distance) # ensure signed DT\n",
85 | "\n",
86 | "# truncate at threshold and normalize between [-1,1]\n",
87 | "thresh = 15\n",
88 | "m_sdf[m_sdf >= thresh] = thresh\n",
89 | "m_sdf[m_sdf <= -thresh] = -thresh\n",
90 | "m_sdf /= thresh\n",
91 | "\n",
92 | "fig, ax = plt.subplots(1,2)\n",
93 | "ax[0].imshow(m), ax[0].set_title('binary mask')\n",
94 | "ax[1].imshow(m_sdf), ax[1].set_title('SDF transformed mask')\n",
95 | "plt.show()"
96 | ]
97 | },
98 | {
99 | "cell_type": "code",
100 | "execution_count": 56,
101 | "metadata": {},
102 | "outputs": [
103 | {
104 | "name": "stdout",
105 | "output_type": "stream",
106 | "text": [
107 | "SDF mask values living in [-1,1]\n"
108 | ]
109 | }
110 | ],
111 | "source": [
112 | "# check SDF normalization\n",
113 | "print('SDF mask values living in [%i,%i]' %(m_sdf.min(),m_sdf.max()))"
114 | ]
115 | },
116 | {
117 | "cell_type": "code",
118 | "execution_count": 57,
119 | "metadata": {},
120 | "outputs": [
121 | {
122 | "name": "stdout",
123 | "output_type": "stream",
124 | "text": [
125 | "Retrieved mask from thresholding SDF agrees with original binary mask: True\n"
126 | ]
127 | }
128 | ],
129 | "source": [
130 | "# retrieve binary mask from SDF mask by thresholding \n",
131 | "m_retrieved = np.where(m_sdf <= 0, np.ones_like(m_sdf), np.zeros_like(m_sdf))\n",
132 | "print('Retrieved mask from thresholding SDF agrees with original binary mask: %s' %np.allclose(m_retrieved,m)) "
133 | ]
134 | },
135 | {
136 | "cell_type": "code",
137 | "execution_count": null,
138 | "metadata": {},
139 | "outputs": [],
140 | "source": []
141 | }
142 | ],
143 | "metadata": {
144 | "kernelspec": {
145 | "display_name": "misc",
146 | "language": "python",
147 | "name": "python3"
148 | },
149 | "language_info": {
150 | "codemirror_mode": {
151 | "name": "ipython",
152 | "version": 3
153 | },
154 | "file_extension": ".py",
155 | "mimetype": "text/x-python",
156 | "name": "python",
157 | "nbconvert_exporter": "python",
158 | "pygments_lexer": "ipython3",
159 | "version": "3.12.2"
160 | }
161 | },
162 | "nbformat": 4,
163 | "nbformat_minor": 2
164 | }
165 |
--------------------------------------------------------------------------------
/sampler.py:
--------------------------------------------------------------------------------
1 | import random
2 | from types import SimpleNamespace
3 | import imageio
4 | import numpy as np
5 | import argparse
6 | import sys
7 | import os
8 | import json
9 | from tqdm.auto import tqdm
10 | import matplotlib.pyplot as plt
11 | from PIL import ImageDraw
12 | import numpy.ma as ma
13 |
14 | import einops
15 | import torch
16 | import torch.nn as nn
17 | import torch.nn.functional as F
18 | from torch.optim import Adam
19 | from torch.utils.data import DataLoader
20 | from torch.utils.tensorboard import SummaryWriter
21 | import yaml
22 | from torchvision.transforms import PILToTensor, ToPILImage
23 | from PIL import ImageFont, ImageDraw, Image
24 | from torchvision.utils import make_grid
25 | from torchmetrics import JaccardIndex, Dice, F1Score
26 | import torchmetrics
27 | from torchvision.utils import save_image
28 | from torch.nn.functional import one_hot
29 |
30 | from SimulationHelper.simulation import Simulation
31 | from datasets.config_dl import config_dl
32 | from models import ddpm
33 |
34 | parser = argparse.ArgumentParser("")
35 | parser.add_argument(
36 | "--config", default="cfg/monuseg.yaml", type=str, help="path to .yaml config"
37 | )
38 | parser.add_argument("--seed", default=0, type=int, help="seed for reproducibility") # 1
39 | args = parser.parse_args()
40 |
41 | # Setting reproducibility
42 | def set_seed(SEED=0):
43 | random.seed(SEED)
44 | np.random.seed(SEED)
45 | torch.manual_seed(SEED)
46 |
47 | def store_gif(frames, frames_per_gif, load_path, sample_str=''):
48 | gif_name = load_path + "/samples/samples" + sample_str + ".gif"
49 |
50 | with imageio.get_writer(gif_name, mode="I") as writer:
51 | for idx, frame in enumerate(frames):
52 | writer.append_data(frame)
53 | if idx == len(frames) - 1:
54 | for _ in range(frames_per_gif // 3):
55 | writer.append_data(frames[-1])
56 |
57 | def show_images(images, vmin=None, vmax=None, save_name="", overlay=None):
58 | """Shows the provided images as sub-pictures in a square"""
59 | alpha=0.6 if overlay is not None else 1. # alpha channel if additional overlay image is given
60 | if vmin is None:
61 | vmin = images.min().item()
62 | if vmax is None:
63 | vmax = images.max().item()
64 |
65 | if overlay is not None:
66 | overlay = overlay.detach().cpu().numpy()
67 |
68 | # Converting images to CPU numpy arrays
69 | if type(images) is torch.Tensor:
70 | images = images.detach().cpu().numpy()
71 |
72 | # Defining number of rows and columns
73 | fig = plt.figure(figsize=(8, 8))
74 | rows = int(len(images) ** (1 / 2))
75 | cols = round(len(images) / rows)
76 |
77 | # Populating figure with sub-plots
78 | idx = 0
79 | for r in range(rows):
80 | for c in range(cols):
81 | fig.add_subplot(rows, cols, idx + 1)
82 |
83 | if idx < len(images):
84 | if overlay is not None:
85 | plt.imshow(overlay[idx][0], cmap="gray")
86 | images[:,:,0,0] = vmax # this is just for plotting!
87 | images[:,:,0,1] = 1
88 | mask = np.ma.masked_where(images[idx][0] == 0, images[idx][0])
89 | plt.imshow(mask, alpha=alpha), plt.axis("off")
90 | else:
91 | plt.imshow(images[idx][0], alpha=alpha, cmap="gray", vmin=vmin, vmax=vmax), plt.axis("off")
92 | idx += 1
93 |
94 | # Showing the figure
95 | plt.savefig(save_name, bbox_inches="tight", dpi=250)
96 | plt.close()
97 |
98 |
99 | def compute_metrics(x,x_gt,thresh,corr_mode,num_classes):
100 | # compute IoU between thresholded x and x_gt
101 | x_thresh = torch.where(x > thresh, torch.zeros_like(x), torch.ones_like(x)).type(torch.int8).squeeze().cpu()
102 | x_gt_thresh = torch.where(x_gt > 0., torch.zeros_like(x_gt), torch.ones_like(x_gt)).type(torch.int8).squeeze().cpu()
103 |
104 | jaccard = JaccardIndex(task="binary")
105 | dice = Dice(task='binary',average='macro',num_classes=2,ignore_index=0) # dice=f1 in binary segmentation
106 | iou, dice = jaccard(x_thresh, x_gt_thresh), dice(x_thresh, x_gt_thresh)
107 | return iou, dice
108 |
109 |
110 | def plot_all(x,cond,x_gt,img_cond,load_path,std_min,corr_mode,sample_str=''):
111 | sdf_min = x_gt.min().item()
112 | sdf_max = x_gt.max().item()
113 |
114 | show_images(x, vmin=sdf_min, vmax=sdf_max, save_name=load_path + "/samples/samples_" + str(sample_str) + ".png")
115 | if img_cond == 1:
116 | show_images(cond, save_name=load_path + "/samples/condition.png")
117 | show_images(x_gt, vmin=sdf_min, vmax=sdf_max, save_name=load_path + "/samples/groundtruth.png")
118 |
119 | x_thresh = torch.where(x > 3.*std_min, torch.zeros_like(x), torch.ones_like(x))
120 | show_images(x_thresh, save_name=load_path + "/samples/samples_thresholded_.png")
121 |
122 | x_gt_thresh = torch.where(x_gt > 0., torch.zeros_like(x_gt), torch.ones_like(x_gt))
123 | show_images(x_gt_thresh, save_name=load_path + "/samples/groundtruth_thresholded.png")
124 |
125 | # show thresholded maps on top of conditioning image
126 | vmax = x_gt.shape[1]
127 | show_images(x_thresh, save_name=load_path + "/samples/samples_thresholded_overlay.png", vmax=vmax, overlay=cond)
128 | show_images(x_gt_thresh, save_name=load_path + "/samples/groundtruth_thresholded_overlay.png", vmax=vmax, overlay=cond)
129 |
130 | class Sampling:
131 | def __init__(self, scorenet, model_type, device, load_path, sz, noise_level_dict, beta_dict, sde, img_cond, corr_mode, save_images=True):
132 | # general params
133 | self.scorenet = scorenet
134 | self.device = device
135 | self.load_path = load_path
136 | self.sz = sz
137 | self.sde = sde
138 | self.img_cond = img_cond
139 | self.model_type = model_type
140 | self.corr_mode = corr_mode
141 |
142 | # if set to False, no images are saved
143 | self.save_images = save_images
144 |
145 | if self.sde == 've':
146 | self.s1, self.sL, self.L = noise_level_dict['s1'], noise_level_dict['sL'], noise_level_dict['L']
147 | self.sigmas = torch.tensor(np.exp(np.linspace(np.log(self.s1),np.log(self.sL), self.L))).type(torch.float32)
148 | elif self.sde == 'vp':
149 | self.beta1, self.betaT, self.T = beta_dict['beta1'], beta_dict['betaT'], beta_dict['T']
150 | self.betas = np.linspace(1.E-4, 0.02, 1000, dtype=np.float32)
151 | self.alphas = 1 - self.betas
152 | self.alpha_bars = torch.from_numpy(np.asarray([np.prod(self.alphas[:i + 1]) for i in range(len(self.alphas))]))
153 |
154 | def get_sigma(self,t):
155 | return self.sigmas[-1]*(self.sigmas[0]/self.sigmas[-1])**t
156 |
157 | def sample(self, x, m_gt, n_samples, N, M, r, num_classes=2):
158 | if self.sde == 've':
159 | return self._sample_ve(x, m_gt, n_samples=n_samples, N=N, M=M, r=r,num_classes=num_classes)
160 | elif self.sde == 'vp':
161 | return self._sample_vp(x, m_gt, n_samples=n_samples,num_classes=num_classes)
162 |
163 | def _sample_vp(self,x, m_gt, n_samples, num_classes):
164 | # TODO: sample according to DDPM paper, note up to date this is fixed to 1000 time steps but could be adapted with a continuous loss function
165 | """
166 | Sample according to DDPM paper
167 | """
168 | m = torch.randn(n_samples,1,self.sz,self.sz).float().to(self.device)
169 | device = x.device
170 | m_list = []
171 | with torch.no_grad():
172 | for i, t in tqdm(enumerate(list(range(self.T))[::-1])):
173 | # Estimating noise to be removed
174 | time_tensor = (torch.ones(n_samples, 1) * t).to(self.device).long()
175 | eta_theta = self.scorenet(m,t*torch.ones((n_samples,1)).to(self.device),img_cond=x)
176 | alpha_t = self.alphas[t]
177 | alpha_t_bar = self.alpha_bars[t]
178 |
179 | # Partially denoising the image
180 | m = (1 / np.sqrt(alpha_t)) * (
181 | m - (1 - alpha_t) / np.sqrt(1 - alpha_t_bar) * eta_theta
182 | )
183 |
184 | m_list.append(m.detach().cpu())
185 | if t > 0: # no noise added in last sampling step
186 | z = torch.randn(n_samples, 1, self.sz, self.sz).to(device)
187 | beta_t = self.betas[t]
188 | # # Option 1: sigma_t squared = beta_t
189 | # sigma_t = np.sqrt(beta_t)
190 |
191 | # Option 2: sigma_t squared = beta_tilda_t
192 | prev_alpha_t_bar = self.alpha_bars[t-1] if t > 0 else self.alphas[0]
193 | beta_tilde_t = ((1 - prev_alpha_t_bar)/(1 - alpha_t_bar)) * beta_t
194 | sigma_t = np.sqrt(beta_tilde_t)
195 |
196 | # Adding some more noise like in Langevin Dynamics fashion
197 | m = m + sigma_t * z
198 |
199 | if self.save_images:
200 | plot_all(m,x,m_gt,self.img_cond,load_path,std_min=1e-3,corr_mode=self.corr_mode,sample_str='vp')
201 |
202 | # x_list.append(x.detach().cpu())
203 | return m, m_list
204 |
205 | def _sample_ve(self, cond, x_gt, n_samples, N, M, r, num_classes):
206 | """
207 | Sample using reverse-time SDE (VE-SDE)
208 | N number of predictor steps
209 | M number of corrector steps
210 | r "signal-to-noise" ratio
211 | """
212 |
213 | frames = []
214 | frames_thresh = []
215 | frames_all = []
216 | frames_per_gif = 100
217 | frame_idxs = np.linspace(0, N, frames_per_gif).astype(np.uint)
218 |
219 | t = torch.linspace(1-(1./N),0,N) # TODO: fix start at 1 or 1-dt
220 | sigma_t = self.get_sigma(t[0])
221 | x_list = []
222 |
223 | # initialize x and sample
224 | n_samples = x_gt.shape[0]
225 | x = self.sigmas[0]*torch.clip(torch.randn(n_samples,num_classes,self.sz,self.sz),-2.,2.).to(self.device)
226 |
227 | with torch.no_grad():
228 | for i, t_curr in enumerate(t):
229 | if i % 20 == 0:
230 | iou, dice = compute_metrics(x,x_gt,thresh=3*self.sigmas[-1].item(),corr_mode=self.corr_mode,num_classes=num_classes)
231 | print('PC sampling it [%i]:\t IoU [%.6f], Dice [%.6f]' %(i,iou,dice))
232 |
233 | # set sigma(t)
234 | sigma_t_prev = sigma_t.clone()
235 | sigma_t = self.get_sigma(t_curr)
236 |
237 | # get scores, sample noise
238 | if self.model_type == 'unet':
239 | scores = self.scorenet(x,sigma_t_prev*torch.ones((n_samples,1)).to(self.device),img_cond=cond)
240 | elif self.model_type == 'tdv':
241 | scores = self.scorenet.grad(torch.cat([x,cond],1),sigma_t_prev*torch.ones((n_samples,1,1,1)).to(self.device))[:,0:1]
242 |
243 | z = torch.clip(torch.randn_like(x),-2.,2.)
244 | tau = (sigma_t_prev**2 - sigma_t**2)
245 |
246 | # predictor step
247 | x = x + tau*scores
248 | x_list.append(x.detach().cpu())
249 | x += np.sqrt(tau)*z
250 |
251 | # corrector steps
252 | for j in range(M):
253 | # z = torch.randn_like(x)
254 | z = torch.clip(torch.randn_like(x),-2.,2.)
255 |
256 | # compute eps
257 | if self.model_type == 'unet':
258 | scores_corr = self.scorenet(x,sigma_t*torch.ones((n_samples,1)).to(self.device),img_cond=cond)
259 | elif self.model_type == 'tdv':
260 | scores_corr = self.scorenet.grad(torch.cat([x,cond],1),sigma_t*torch.ones((n_samples,1,1,1)).to(self.device))[:,0:1]
261 |
262 | eps = 2*(r*torch.norm(z).item()/torch.norm(scores_corr).item())**2
263 | x = x + eps*scores_corr
264 |
265 | x_list.append(x.detach().cpu())
266 | x += np.sqrt(2*eps)*z
267 |
268 | if self.save_images and (i in frame_idxs or t_curr == 0): # TODO: if other samplers than PC are used, make sure that the gif is also generated for them
269 | # Putting digits in range [0, 255]
270 | normalized = x.clone()
271 | if self.corr_mode == 'diffusion_ls':
272 | normalized_thresh = torch.where(x > 3*self.sigmas[-1], torch.zeros_like(x), torch.ones_like(x))
273 | elif self.corr_mode == 'diffusion':
274 | normalized_thresh = torch.where(x < 0.5, torch.zeros_like(x), torch.ones_like(x))
275 |
276 | for i in range(len(normalized)):
277 | normalized[i] -= torch.min(normalized[i])
278 | normalized[i] *= 255 / torch.max(normalized[i])
279 |
280 | normalized_thresh[i] -= torch.min(normalized_thresh[i])
281 | normalized_thresh[i] *= 255 / torch.max(normalized_thresh[i])
282 |
283 | # Reshaping batch (n, c, h, w) to be a (as much as it gets) square frame
284 | frame = einops.rearrange(
285 | normalized,
286 | "(b1 b2) c h w -> (b1 h) (b2 w) c",
287 | b1=int(n_samples**0.5),
288 | )
289 | frame = frame.cpu().numpy().astype(np.uint8)
290 | frames.append(frame)
291 |
292 | # append thresholded
293 | frame_thresh = einops.rearrange(
294 | normalized_thresh,
295 | "(b1 b2) c h w -> (b1 h) (b2 w) c",
296 | b1=int(n_samples**0.5),
297 | )
298 | frame_thresh = frame_thresh.cpu().numpy().astype(np.uint8)
299 | frames_thresh.append(frame_thresh)
300 |
301 | # plotting
302 | if self.save_images:
303 | plot_all(x_list[-1],cond,x_gt,self.img_cond,load_path,std_min=cfg.SMLD.sigma_L,corr_mode=self.corr_mode,sample_str='ve')
304 |
305 | return x_list[-1], x_list
306 |
307 | if __name__ == "__main__":
308 | with open(args.config) as file:
309 | yaml_cfg = yaml.safe_load(file)
310 | cfg = json.loads(json.dumps(yaml_cfg), object_hook=lambda d: SimpleNamespace(**d))
311 |
312 | device = torch.device("cuda")
313 | print(f"Using device: {device}\t" + (f"{torch.cuda.get_device_name(0)}"))
314 |
315 | set_seed(SEED=0)
316 |
317 | # set up dataloader, model
318 | train_dl, test_dl = config_dl(cfg)
319 | if cfg.model.type == 'unet':
320 | model = ddpm.Network(
321 | dim=cfg.model.dim,
322 | channels=cfg.model.n_cin,
323 | cond_channels=cfg.model.n_cin_cond,
324 | init_dim=cfg.model.n_fm,
325 | dim_mults=tuple(cfg.model.mults),
326 | embedding=cfg.model.embedding,
327 | img_cond=cfg.general.img_cond,
328 | with_class_label_emb=cfg.general.with_class_label_emb,
329 | class_label_cond=cfg.general.class_label_cond,
330 | num_classes=cfg.general.num_classes,
331 | ).to(device)
332 |
333 | else:
334 | raise ValueError('Unknown model type!')
335 |
336 | load_path = os.getcwd() + "/runs/" + cfg.general.modality
337 |
338 | if cfg.inference.latest:
339 | print("\nINFO: inference the lastest experiment!")
340 | load_path += "/" + sorted(os.listdir(load_path))[-1]
341 | else:
342 | print("\nINFO: inference from selected experiment, *not* the latest!")
343 | load_path += "/" + cfg.inference.load_exp
344 |
345 | print(f"Loading *latest* checkpoint from {load_path + '/models/'}")
346 | if not os.path.exists(load_path + "/samples"): # makedir for samples
347 | os.mkdir(load_path + "/samples")
348 |
349 | # load the model weights
350 | fnames = sorted(
351 | [fname for fname in os.listdir(load_path + "/models/") if fname.endswith(".pt")]
352 | )
353 | model.load_state_dict(torch.load(load_path + "/models/" + fnames[-1], map_location=device)["state_dict"],strict=True) # strict=False
354 |
355 | model.eval()
356 | print("\nModel loaded from %s" % (load_path + "/models/" + fnames[-1]))
357 |
358 | # sample and save generated images
359 | if cfg.general.corr_mode == "diffusion" or cfg.general.corr_mode == "diffusion_ls":
360 | noise_level_dict={'s1': cfg.SMLD.sigma_1_m, 'sL': cfg.SMLD.sigma_L_m, 'L': cfg.SMLD.n_steps}
361 | beta_dict = {'beta1': cfg.SMLD.beta_1, 'betaT': cfg.SMLD.beta_T, 'T': cfg.SMLD.T}
362 |
363 | Sampler = Sampling(
364 | scorenet=model,
365 | model_type=cfg.model.type,
366 | device=device,
367 | load_path=load_path,
368 | sz=cfg.general.sz,
369 | noise_level_dict=noise_level_dict,
370 | beta_dict=beta_dict,
371 | sde=cfg.SMLD.sde,
372 | img_cond=cfg.general.img_cond,
373 | corr_mode=cfg.general.corr_mode,
374 | save_images=True)
375 |
376 | # load conditioning image and ground truth
377 | it_test_dl = iter(test_dl)
378 | batch = next(it_test_dl)
379 | x = None if cfg.general.img_cond==0 else batch['mask'].to(device)
380 | m_gt = None if cfg.general.img_cond==0 else batch['image'].to(device)
381 |
382 | # generate samples
383 | samples, samples_list = Sampler.sample(x, m_gt, n_samples=cfg.inference.n_samples, N=cfg.SMLD.N,M=cfg.SMLD.M,r=cfg.SMLD.r, num_classes=cfg.model.n_cin)
384 |
385 | # eval metrics
386 | iou, dice = compute_metrics(samples, m_gt.cpu(), corr_mode=cfg.general.corr_mode,thresh=3.*cfg.SMLD.sigma_L_m,num_classes=cfg.model.n_cin)
387 | print('\nFinal metrics: IoU [%f], Dice [%f]' %(iou,dice))
388 |
--------------------------------------------------------------------------------
/trainer.py:
--------------------------------------------------------------------------------
1 | import random
2 | import imageio
3 | import numpy as np
4 | import argparse
5 | import sys
6 | import os
7 | import json
8 | from tqdm.auto import tqdm
9 | import matplotlib.pyplot as plt
10 |
11 | import einops
12 | import torch
13 | import torch.nn as nn
14 | from torch.optim import Adam
15 | from torch.utils.data import DataLoader
16 | from torch.utils.tensorboard import SummaryWriter
17 | from torchvision.utils import make_grid
18 |
19 | from SimulationHelper.simulation import Simulation
20 | from datasets.config_dl import config_dl
21 | from datasets.transform_factory import inv_normalize, transform_factory
22 | from models import ddpm
23 |
24 | class TrainScoreNetwork:
25 | def __init__(self, noise_level_dict, beta_dict, sde, model_type, train_objective, anneal_power=2, loss_power=2, n_val=8, val_dl=None):
26 | self.sde = sde
27 | self.model_type = model_type
28 |
29 | if self.sde == 've':
30 | self.s1, self.sL, self.L = noise_level_dict['s1'], noise_level_dict['sL'], noise_level_dict['L']
31 | self.sigmas = torch.tensor(np.exp(np.linspace(np.log(self.s1),np.log(self.sL), self.L))).type(torch.float32)
32 |
33 | self.model_type = model_type
34 | self.anneal_power = anneal_power
35 | self.loss_power = loss_power
36 | self.train_objective = train_objective
37 | assert train_objective == 'disc' or train_objective == 'cont'
38 |
39 | if val_dl: # then use test dataloader
40 | val_batch = next(iter(val_dl))
41 | self.x_val = val_batch['image'][:n_val]
42 | self.cond_val = val_batch['mask'][:n_val]
43 |
44 | eta_val = torch.randn_like(self.x_val)
45 | self.used_sigmas_val = torch.linspace(self.sigmas[0],self.sigmas[-1], self.x_val.shape[0])[:,None,None,None]
46 | self.z_val = self.x_val + eta_val*self.used_sigmas_val
47 |
48 | elif self.sde == 'vp':
49 | self.beta1, self.betaT, self.T = beta_dict['beta1'], beta_dict['betaT'], beta_dict['T']
50 | self.betas = np.linspace(1.E-4, 0.02, 1000, dtype=np.float32)
51 | self.alphas = 1 - self.betas
52 | self.alpha_bars = torch.from_numpy(np.asarray([np.prod(self.alphas[:i + 1]) for i in range(len(self.alphas))]))
53 |
54 | if val_dl: # TODO: implement validation
55 | val_batch = next(iter(val_dl))
56 | self.x_val = val_batch['image'][:n_val]
57 | pass
58 |
59 | else:
60 | raise ValueError('Unknown SDE type!')
61 |
62 | def get_grad_norm(self, model):
63 | parameters = [p for p in model.parameters() if p.grad is not None and p.requires_grad]
64 | norms = [p.grad.detach().abs().max().item() for p in parameters]
65 | return np.asarray(norms).max()
66 |
67 | def do(self, scorenet, dl, n_epochs, clip, optim, device, simulation, writer, img_cond, class_label_cond=False):
68 | if self.sde == 've':
69 | self._do_ve(scorenet, dl, n_epochs, clip, optim, device, simulation, writer, img_cond, class_label_cond)
70 |
71 | elif self.sde == 'vp':
72 | self._do_vp(scorenet, dl, n_epochs, clip, optim, device, simulation, writer, img_cond, class_label_cond)
73 |
74 | def _do_ve(self, scorenet, dl, n_epochs, clip, optim, device, simulation, writer, img_cond=0, class_label_cond=False):
75 | if img_cond == 0:
76 | self.cond_val = None
77 | else:
78 | self.cond_val = self.cond_val.to(device)
79 | best_loss = float("inf")
80 |
81 | for epoch in tqdm(range(n_epochs), desc=f"Training progress", colour="#00ff00"):
82 | epoch_loss = 0.0
83 | grad_norms_epoch = []
84 |
85 | for step, batch in enumerate(tqdm(dl, leave=False, desc=f"Epoch {epoch + 1}/{n_epochs}", colour="#005500")):
86 | # Loading data
87 | x = batch['image'].to(device)
88 | cond = None if img_cond==0 else batch['mask'].to(device)
89 | lbl = None if class_label_cond is False else batch['label'].to(device).unsqueeze(1)
90 | n = len(x)
91 |
92 | # noise-conditional score network corruption
93 | if self.train_objective == 'disc':
94 | sigmas_idx = torch.randint(0, self.L, (n,))#.to(device)
95 | used_sigmas = (self.sigmas[sigmas_idx][:,None,None,None]).to(device)
96 | elif self.train_objective == 'cont': # continuous training objective (SDE style)
97 | t = torch.from_numpy(np.random.uniform(1e-5,1,(n,))).float()
98 | used_sigmas = (self.sigmas[-1]*(self.sigmas[0]/self.sigmas[-1])**t)[:,None,None,None].to(device)
99 |
100 | # noise corruption
101 | eta = torch.randn_like(x).to(device)
102 | z = x + eta*used_sigmas.to(device)
103 |
104 | # compute score matching loss
105 | target = 1/(used_sigmas**2) * (x-z)
106 | if self.model_type == 'unet':
107 | scores = scorenet(z, used_sigmas.reshape(n,-1), img_cond=cond, class_lbl=lbl)
108 | elif self.model_type == 'tdv':
109 | scores = scorenet.grad(torch.cat([z,cond],1), used_sigmas.reshape(n,1,1,1))[:,0:1]
110 |
111 | if step % 100 == 0: # Sanity Check. Whats going into the network?
112 | with torch.no_grad():
113 | scorenet.eval()
114 | if self.x_val is not None: # always take same val/test batch
115 | if self.model_type == 'unet':
116 | scores_val = scorenet(self.z_val.to(device), self.used_sigmas_val.to(device).reshape(self.z_val.shape[0],-1), img_cond=self.cond_val, class_lbl=lbl)
117 | elif self.model_type == 'tdv':
118 | scores_val = scorenet.grad(torch.cat([self.z_val.to(device),self.cond_val],1), self.used_sigmas_val.to(device).reshape(self.z_val.shape[0],1,1,1))[:,0:1]
119 |
120 | x_mmse_val = self.z_val.to(device) + self.used_sigmas_val.to(device)**2 * scores_val
121 |
122 | # for multi-class plotting just take a random class
123 | if self.x_val.shape[1] > 1:
124 | class_idx = 4
125 | x_val, z_val, x_mmse_val = self.x_val[:,class_idx][:,None], self.z_val[:,class_idx][:,None], x_mmse_val[:,class_idx][:,None]
126 | else:
127 | x_val, z_val = self.x_val, self.z_val
128 |
129 | all_stacked = torch.cat([
130 | make_grid(x_val, nrow=x_val.shape[0], normalize=True, scale_each=True).cpu(),
131 | make_grid(z_val, nrow=x_val.shape[0], normalize=True,scale_each=True).cpu(),
132 | make_grid(x_mmse_val, nrow=self.x_val.shape[0], normalize=True, scale_each=True).cpu()], dim=1)
133 |
134 | else: # check on random input data
135 | x_mmse = z + used_sigmas**2*scores
136 | all_stacked = torch.cat([
137 | make_grid(x, nrow=x.shape[0], normalize=True, scale_each=True).cpu(),
138 | make_grid(z, nrow=x.shape[0], normalize=True,scale_each=True).cpu(),
139 | make_grid(x_mmse, nrow=x.shape[0], normalize=True, scale_each=True).cpu()], dim=1)
140 |
141 | # plot clean, noisy, and denoised (using Tweedie's formula)
142 | if step % 100 == 0:
143 | writer.add_image(f'training', all_stacked, global_step=epoch)
144 | writer.flush()
145 | scorenet.train()
146 |
147 | # Optimizing the MSE between the noise plugged and the predicted noise #
148 | loss_batches = ((torch.abs(target - scores))**self.loss_power).sum((-3,-2,-1))*used_sigmas.squeeze()**self.anneal_power # NOTE: L1 loss and anneal_power should match
149 | loss = loss_batches.mean()
150 |
151 | optim.zero_grad()
152 | loss.backward()
153 |
154 | if isinstance(clip,float):
155 | torch.nn.utils.clip_grad_norm_(scorenet.parameters(), max_norm=clip, norm_type='inf')
156 | grad_norms_epoch.append(self.get_grad_norm(scorenet))
157 |
158 | optim.step()
159 | epoch_loss += loss.item() * len(x) / len(dl.dataset)
160 |
161 |
162 | log_string = f"Loss at epoch {epoch + 1}: {epoch_loss:.8f}"
163 | if epoch % 50 == 0:
164 | writer.add_scalar(f'train/epoch_loss', epoch_loss, epoch)
165 |
166 | writer.add_scalar(f'train/epoch_max_grad', np.asarray(grad_norms_epoch).max(), epoch)
167 | writer.add_scalar(f'train/epoch_mean_grad', np.asarray(grad_norms_epoch).mean(), epoch)
168 |
169 | # Storing the model
170 | if epoch % 5000 == 0: # save every 5000th epochs model, no matter what?
171 | checkpoint = {'state_dict': scorenet.state_dict()}
172 | simulation.save_pytorch(checkpoint, overwrite=False, subdir='models_sanity', epoch='_'+'{0:07}'.format(epoch))
173 |
174 | if best_loss > epoch_loss:
175 | best_loss = epoch_loss
176 |
177 | # save last 3 checkpoints
178 | if epoch > 0:
179 | cp_dir = simulation._outdir + '/models'
180 | if len([name for name in os.listdir(cp_dir) if os.path.isfile(os.path.join(cp_dir,name))]) == 3:
181 | fnames = sorted([fname for fname in os.listdir(cp_dir) if fname.endswith('.pt')])
182 | os.remove(os.path.join(cp_dir,fnames[0]))
183 | checkpoint = {'epoch': epoch,
184 | 'state_dict': scorenet.state_dict(),
185 | 'optimizer': optim.state_dict()}
186 | simulation.save_pytorch(checkpoint, overwrite=False, epoch='_'+'{0:07}'.format(epoch))
187 | log_string += " --> Best model ever (stored)"
188 |
189 | print(log_string)
190 |
191 | def _do_vp(self, scorenet, dl, n_epochs, clip, optim, device, simulation, writer, img_cond, class_label_cond=False):
192 | best_loss = float("inf")
193 | mse = nn.MSELoss()
194 |
195 | for epoch in tqdm(range(n_epochs), desc=f"Training progress", colour="#00ff00"):
196 | epoch_loss = 0.0
197 | grad_norms_epoch = []
198 |
199 | for step, batch in enumerate(tqdm(dl, leave=False, desc=f"Epoch {epoch + 1}/{n_epochs}", colour="#005500")):
200 | # Loading data
201 | m = batch['image'].to(device)
202 | m *= 5. # TODO: comment if a clean data loader *without* scaling to [-0.2,0.2] is used - this is to get the mask to [-1,1] like the image
203 | x = None if img_cond==0 else batch['mask'].to(device)
204 | lbl = None if class_label_cond is False else batch['label'].to(device).unsqueeze(1)
205 | n = len(m)
206 |
207 | # noise corruption
208 | t = torch.randint(0, self.T, (n,)).to(device)
209 | a_bar = self.alpha_bars.to(device)[t]#.to(x.device)
210 | eta = torch.randn_like(x).to(device)
211 | m_noisy = a_bar.sqrt().reshape(n, 1, 1, 1) * m + (1 - a_bar).sqrt().reshape(n, 1, 1, 1) * eta
212 |
213 | # compute score matching loss
214 | if self.model_type == 'unet':
215 | eta_estimated = scorenet(m_noisy, t.reshape(n,-1), img_cond=x, class_lbl=lbl)
216 |
217 | elif self.model_type == 'uvit':
218 | eta_estimated = scorenet(m_noisy, t.reshape(n,-1), img_cond=x)
219 |
220 | elif self.model_type == 'tdv':
221 | raise NotImplementedError
222 |
223 | # Optimizing the MSE between the noise plugged and the predicted noise #
224 | loss = mse(eta_estimated, eta)
225 | optim.zero_grad()
226 | loss.backward()
227 |
228 | if isinstance(clip,float):
229 | torch.nn.utils.clip_grad_norm_(scorenet.parameters(), max_norm=clip, norm_type='inf')
230 | grad_norms_epoch.append(self.get_grad_norm(scorenet))
231 |
232 | optim.step()
233 | epoch_loss += loss.item() * len(x) / len(dl.dataset)
234 |
235 | log_string = f"Loss at epoch {epoch + 1}: {epoch_loss:.8f}"
236 | if epoch % 10 == 0:
237 | writer.add_scalar(f'train/epoch_loss', epoch_loss, epoch)
238 | writer.add_scalar(f'train/epoch_max_grad', np.asarray(grad_norms_epoch).max(), epoch)
239 | writer.add_scalar(f'train/epoch_mean_grad', np.asarray(grad_norms_epoch).mean(), epoch)
240 |
241 | if best_loss > epoch_loss:
242 | best_loss = epoch_loss
243 | # save last 3 checkpoints
244 | if epoch > 0:
245 | cp_dir = simulation._outdir + '/models'
246 | if len([name for name in os.listdir(cp_dir) if os.path.isfile(os.path.join(cp_dir,name))]) == 3:
247 | fnames = sorted([fname for fname in os.listdir(cp_dir) if fname.endswith('.pt')])
248 | os.remove(os.path.join(cp_dir,fnames[0]))
249 | checkpoint = {'epoch': epoch,
250 | 'state_dict': scorenet.state_dict(),
251 | 'optimizer': optim.state_dict()}
252 | simulation.save_pytorch(checkpoint, overwrite=False, epoch='_'+'{0:07}'.format(epoch))
253 | log_string += " --> Best model ever (stored)"
254 | print(log_string)
--------------------------------------------------------------------------------