├── .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 | drawing 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/iPe85z1YvHgxZsyYgT333BPXXnttrO0Oh/POOw8zZszAW9/6Vjz11FM4//zzN8TXSpggeOc734lvfvOb+MhHPoJHHnkE3W4X8+bNw2GHHYZzzjknmhwD7r333ljWmD59OubOnYt9990XX/3qV3H00UevU3bK6jA4OIgrr7wS559/Po444ggURYH3vOc9MQtlOOR5jm9961s444wzcOqppyLLMhx22GH40Y9+9LRVnQMOOAC33nor3va2t+Gxxx7DJptsgv322w/XX3999HGMhGc84xm48cYb8Y53vAOnn346Vq1ahd122w1XXXUVTjrppKe1nnXFvHnzcOutt+Lf//3f8cEPfhB/+ctfMHPmTCxcuBB/93d/h0033RSA7u/rr78eZ5xxBt7+9reDiHDEEUfgS1/6Eg466KAxX+dUPs6RjGQfT0gYRfz2t7/Fs571LHz84x8flS6EhISEhISpjURQEsYUf/7zn7Fo0SK84x3vwAMPPIA//elP4xqrnZCQkJAwOZBMsgljin//93/H4Ycfjqeeegpf/epXEzlJSEhISFgrJAUlISEhISEhYcIhKSgJCQkJCQkJEw7jSlA+8YlPYOHChRgYGMCzn/1s/OxnPxvP5SQkJEwCpONGQsLGgXEjKF/+8pdx5pln4txzz8Xtt9+Ogw8+GC95yUvwwAMPjNeSEhISJjjScSMhYePBuHlQDjjgAOy7776NhLrddtsNr3jFK3DhhReu9rXMjIcffhgzZ84ck4jjhISENUNEsHz5csyfP3/UMjzWhPU5bgDp2JGQMN5Yl+PGuAS19Xo9/PrXv27MeAGAI444AjfeeOOQ53e7XXS73Xj7oYcewu677z7m60xISFgzHnzwQSxYsGDMP2ddjxtAOnYkJExUrM1xY1wIyl//+lc454ZMgJw7d+6wA5YuvPDCYdPrno+XIkM+ZutMmLpYcdR++MIHLsN0Gvnfz3P/52Ts+IbfbMBVTS6UKPBzfBczZ87cIJ+3rscNYORjx6LbtsesGROnR8AJgyHoSoFSGAUEPREUAnTFgEHoiEUpFj1YFGJQSoaO5HBi0EUGJ4RCMjgYsBAcDETG/jsSMSwYhgQWjJxKWBK0UcISY4AKZFQiJ0YLDhk5DJCDgaBNjJyAFhFaZJDDok0ZLDXX3ZUChTgs5RI9AZ7kFrqS4SkeQEdyrJQWlrsBdCXHU66NUiyeKvVylcvR4RylGKwsW3BMWFXmcGxQOIuCDUpnUToD5wiutBAm3UoDOAKYAF9rICZQsXr1TYxAcgEIgBEgZ5AVGCMwhmEyhrUCaxjtXPfTtLxAbhymZQUGbIkBU2B61kPLlJiVrcKgKTDddjDddDGDOphpOhg0Pcw2PbRJMMtYDFCG9mqOaRMBy55ibLfv/Wt13BjXqPt+iVVEhpVdzznnHJx11lnx9rJly7DNNtsgQ45sgv8xEiYIiPDYqc/F8u31pt3hKWw7exA52RFf8ur9f49vXPQCbP/tVTA/v2ODLHNSIRywN3CpZG2PG8DIx45ZMwxmzRx/glIRkxIOAicCEsAJwcKAxcCIhYBAkgNiQZIBYiGSQSSHCIGlBRHS+0AQMRAQeAMQFEMMgUDiZQkmgVAPQgKhAqASRC5eEhUwEFhysMTISZBBkJFDmwxyIhhQJCormdAVoMsGEIM25yBYdFwLmeTIuI3MDaCUDNYNoOAcKNu6H1yuGxtI2dL7Cr/fSgtxFmADlAbkDCjzpKQ0IKPkhBwB7L8wE4wQaDXmCM6UoEjmSUrGgAEod+r8tA6wAliGZCVgGZRnIOtAWQayBchaZBkhNwZ5xrDGoG0dWoYxYB0GTIkWCaYZhwFizDCENhEGN1C5dX2xNseNcSEom2++Oay1Q371LFmyZMivI0AnYfYPPkpIWBdQq4Vtj78X39i5PsBrZHICAB/c6nZ88MTbsddfT8P8XxCQIoPGFet63AAm/rGjhEMhDh1xKETQEaAAoRCDQgx6MOhIDha9dCB0uIVCLHpi0ZEWWKh6DudgEJxXXQDAjSFJsaRnbSUbDAPBgClgiMFEMCRwRGiRRUEOA0ZVHiYDQwyHAi1hONKTrIPAokQBh0FqgUUnpxdwKCAoQGAQerDocI5CMnRYFZKOZOj6613O/GbR4ww9l6EUg4ItHBuUbPTS6W3njConQhBHENennJQE8kPciQFTrn6/GCHlMwSIFX0vAcQYiAhABsp4DJwxIBKUXF2yUXJZiEEGE/+mDgYMo9fFwJFBAQMLQQFBDoYTHqJATVaMC0FptVp49rOfjeuuuw6vfOUr4/3XXXcdjjrqqPFYUsIUxpLTD8Lsox7Gedt8DWsiJcPh6H+6AdcevBe2ehfAv/3f0V9gwlphKh03ClFi0pUSHeFITDpiPTkJpRwbyzgd0ROykhO97LKSli7nKMTqCdiXeAqp/q2zjL7KZWoSQk4ulni6kiEnh8JYPXEai5wcWlSiIzlyKtEjC0uMgvSxARQo4JALgw0jF4CpC0OEHBaFMDoifn/ofin8/tF9kqHgsE8y9GrkpFPm6LgMBVt0Sy2F9UpPVJxB6Us67HxZpzBKTIJyUhJMj+C5GEiUpIC13NMACQIfNIUnKRkgOZSgEAGmup+IwUJgNnBCMKx/u5It2Diw+HKdGDBR429fwMIKoxAlKD1xyMFokwBThKSMW4nnrLPOwj/90z9hv/32w4EHHohPfepTeOCBB/DGN75xvJaUMMVgBgZgNp+DJ/fp4fY9v4GnQ04A4Pwt7sYpm/4Kx+3yr5i9ZC7KR5ckNWWcMBWOG064QU4KAB3RX8Lx5CsZev4k1OGWV04qghIug2LS8QSlZCU33FBQxq4EZz1JKWFhiJGTg4P+sgdUWQEAJj3BOjJwRIABrAhgvMLjz6UMghWBgwBg5CCAgEIEThDJWyEZnBjdR/WNLUquLks2KIPa4ElAVE3YgNkoOfGX4omJbgAcQGETAOIvPTkJqkoEEYSkUluc3/dWfSlwpG/CBAhBBL5ER81LrxQBaJIUf73n90EOBweCA/nlChgM8zSPdRMN40ZQjj32WDz++ON473vfi0ceeQR77rknvvvd72K77bYbryUlTDEse9mz8Nr3fgsXTvsagIH1eq95dhBnX/g5fOLBF8K+elO4x/82OotMWCdM9uNGIQ4rpYeuVwQ6oqrJSq+ErOC2VwayeBLqcA4HU6kkta3DOVgIXc5QhhO06Ik4nMwAxJPdaCKQD0MCQwxLgowYuVEjbNdkMCRRUalvHSlgwZguqqAUsFFNcUxeeSlhRdAmh0KUxHW8MbgjoazTitersk5QTyw6LkfXZeiWGQo26BaZmmNLC2Y1xDL7sk6fckKFbsYTFDBgfKmmUlB0X5AAgQcSE8QCDIEBIM4rJpYgLdayj4j+xgkkxe9T8X+rkg16zqJrlGh0WU/VAyaQU4eeWOSk/w4s1FBdQFCIg6GpQVHG1SR72mmn4bTTThvPJSRMQZjp09E9cFc89myDf571F1haP3ICAJYMXj59Jf605e/xI7PjKKwy4elish43QlmnEI4dOqGkE8o5gZx0OK8ISvSgZNFnEghKl7OKoLDv7mEbT3LA2JR3AkKZJzPqP3HG6a99Q43HVTlRpcRB1Yncl3wcTLBjAAA6yGAhMKKeCgNWXw5MVAuiHyMQMWjXUv2+kq0vlyhhC6qJKicEdqYiJzXlhBxVyoknIg3FJCgpnqAEZQUGSj5YL8lfwlTPEaDBRsQrJuLXFzwoltgTTAMWjt9Nv7d+VwBxP7iglgFefZoaGFeCkpAwJthpW/zrFV/AIQNPjgo5SUhYXzhhrJQeCmGs9MrJSs6wwvtKVkhLL7kdfQZ1f4kTg5Xcit6SfsUkXPactiKHEzOgJ8GxAtUJCmkXTss6ZMzomQwZhUuHtinRZVVVCtNTD4r0kFOJwnhFiFoYML1YulB1RUlPMAw3FZQ8mmQr70mGXm2/xHbiMrQSV74TcX2txAxQT4mIKSgSFONNsqHUU1dQiBEJSvCfiABMpGWd1UAAMCuLKX33jTMMZ7Q01WMtnZVsYcFxP7FvKW+R96nAoCcGOTFYfOfQFEAiKAlTBpRl+Otr98cTewr2av0VM8yM8V5SQkLNEKvKSSjr9GCiQhI6c4I6ooZYi4IzdL3foutNoHWlJCgoPa8W9DjzikFQEmjMCYohgRP/q98rJyV5oy5JVFRYDAqjJR4gGGs5Kiqqrqia4qgEGBgwBQDEdulgiHU+56Ve7oreEzGRoFXqCUUzKoeMk75unYZy4ks5oawT1BTjFZSorkh1H2dQYlCv1wwHIcARhAxADGYCEfzfC748V98qhYh963hTQRn692Uwnq7nbiIhEZSEKQNqt/GcN96OT2z9SwBjSE7MFPl5kjDmcMI+ZCx0oQArOYteiko50cuVXkHpcN4gJlrGybUcxHqCLr3PovQn5ZKbbbQCbDCCQlAVxRpGaQwyo7/6M1/yycSgR4KWlCjJKwDeX5GTAyzg2KgHxbcls1ETK9e6UXq19uou57FzR83BJu4H3UemYYZVgkKahxLISVm1ElPhlZPSKynOk5Oyppx4YkJlZZY1TnwLMVXKxYjkBPocJi0FOYIYA65187Av92irMXmDLDVKdex9Ro5CaYd8KzLgVhfQMsmQCErClMCjbzoI/KIn8J45n8dY/nJ4yYy78PlP74/ejTth6w8MH6+ekADAqyYFVopDTwQrOHTqVORESzqBmGRYWVNSgmISCEmPs6gQqDpgo3LSyPeIfovK3zBWIBIQCawRDZdjzfWwRs2yhi1K45AZh8wwCrbIjQODkEmlpLAv54T25Ho3kDNqmjXgqqW4v3tHhnbvFM5quq7zpuGgnIh20Ki3pGolDgQlGmJrZR3j76My5KBIVd6Bvh15j0n9MvhQqPCkhwxgBZIBYAEMqaVFqlJc/2Xd4BzyTzYWJIKSMCWw7Nld3PucL2KsZc3dWoO4bb8vYx85bkw/J2FyI7QSB0NsVxDbiIOHIpxsY+BYvG5jN0opFl2XxZKO+kyyWMYoaidjAbzfwsRcjQ1FUJgFzhByy5prxgZsCZnRM3gpBhkzWrYEC3lDrQa5Mak60DZlbE9mMrBg347McGC0CCMQE6N+nKCgeOUkKhG1TWJrL5RVCJqtxH2G2CHKCTefS6y+k7iHg2HWXyf2bcdeXBHnu31iB49v/wlkpO9ydeBamWeqeE76kQhKQkJCwijCCWOV9LBSHDo15WS5L+Ms8/NjOpxH5WS5G0AhFk85LfGscnlUTIb4S7g6CRch2MtZOKZ4YmYmOFedkMcKREpSrGUYY1A6nS8TFBVDgtxYWMPIiJWoEKuCwtqpYoijgTYzjMK3HgNKSJwQBkwBDiZZzmPLtRI5bSXWxFiLwuk+K0LXjui+cM6HsTmNtY/R9X2InhOvnPiEflVOCvGXwZMicLm2FatC4l8HxCRZAwIbmUK9NRsOiaAkTGpkW81Fb6d52GSzp8Z7KQkJAEJ8fdVKXAWwZQ3lJCoofSWLXmgZrikmDELPaRR7z5dz6q2zZSxj+BZa30YrQmN6YiRUnTwiokPy/HUAsF5BYaFooGVDyFjP4l22yMNMFn9WL/x8rI7o6akQp6FuqLXVSgikq0LN6obSqnWXKpImteust8kHppFvfIktw41Atppywn1qSijzEEAsAOl7iX+91BWVhHVGIigJkxqPvHIHfPXtH8Tm1gKYNt7LSdjIsZJ7KOCwQhgrfbdOXTkJhtiOtLCSW1jpFZN+5WSVy9FzSlYK51tmnY2GybpiIoC2zQrBOW8CBcClUfNmfxz7KIKMntmNkJ7gDcNag5IEThwINioqmXXRnwJoRkrLWGReXenZEi1TwsFUCgplMfejRVoe63oDcVBSui5TBcVpe3GYUNwrtfzjnPGZJ1UQG4RA3ndiCuor71Sb8epJv3JiC4lZKMYIGBroxhANeaNETkYDiaAkTGpwTtgxT+3ECeMPJ6xD7YRVOfHD3hqpsJJXHSi1sLVg8uyxnmxDJ4pe2ljOCeZXrpUuJF4iRrZDNME0mkHHwodCov4WEgiRGkW9DBIm1RI1FRVDAvhuoxiDLwzYEoZDVw97dSUDDBBmDoXrDjXvjdS6d3zZq/RlLqkrSqJdM2HfDEscwmydkGviLylskbRIbDGeQg0zExKJoCQkJCSsJ5xwLO1ohH0wxFbR7FUrcStO3dXrqpiUbP2liRHtJRt0yiyWccQrKC4YYT0J0RMwRVIigspnMWYKCgFG4vA7IkBIIJZARpQMkChJsByNn9bodUMSPSmlGLAtUIqBgSDznSoh74ONaYTW1Tt34sydOHfHK0xSK3VxPfNk+G/Tn3cSyzd1RcV375jSl7XIG2o3nsaaDYpEUCYB7OZzcM+5O0M2LUb9vemJHM94/x/h/vr4qL/3WMLutBC//7ct8Ow9/jDeS0lI8ORETbFdAbphVk7f3JiuL0vUZ8f0agQllHW6LoulilDOKZz1/pIqbEw7UxCj2iUkogoiMaGSxqbUQH5KrwMgRkUao8YLMboRARBWwpQBvRKwhmKpR0PcOLbStqSEQY5MTBxEaGuMouE9aagnVh+LgwADifO+E655Tzi0FPeVd2oR9iEHRR+TyndSU1KEMGW7ZyYKEkGZoKC8BWrlemPu5vjw338er5g++kbQb6yYgU/955EwqzoAAOkVkKI36p8z2ii2mo0fveSSVN5JmBAILcWFAD0xMZY9lHfqU3f7yYkqJdp5Ej0ntZJOMMCqzwSV+VW0KwVhCq+gimyvt7qOJUGx3l3qQh+t/yhWdUWj3r0aQnFJEBE4JiDzmSEkcQJvmN/TdXp6sn2SR3+qaj01lr0xtmovDrkniO28FIYBlk3vSd0cWxGRWjmH6/dVGSYJY4dEUCYoHjpjPxx87G0AgJnZPThkYAmAwVH/nEMGluCmz9+D5aXOrPnZl/fD/A+lALKEhLVBiLEPYWwdMejWlRPRVuJerbRTL+sE5WRl2UIpBqvKHI4Nus6idDo7pnBW22RLf9J11QlXSorR6QFU6sk3wPTGjqDUaxuShQAyHzRmBTACBkPERBOvMQbGCIxRD0vmyz9iCaUv7ZTej1KIgbNVrH3blDFdtQqtszHzJHYyhfKO319x3s5w8KFspkQVY9+Xe6LtxgJyEo2zko/BPk1oIBGUCQLKW6Bn7ABk+h/oir06PrI9YPTJCQBsagdx0dw74u0d9todZu/d9UbJkHvunViKChHsLjviyYXTkCd5NWGcwf7/XK2luAfj56UQeqH04OfquFoKbNxqEe0xDbamnIRShRpg0VRM4hTexqLgm2AA1MoXow1qfo7E8I/aE+DNuvACBhuEWosIwRgGnJ/i65WTknXqcSnqoSmJ0SMBDGAkGGtNFWqGKoxOgh+n1locFaW+jfouY3lnmOcFJaUyx4o3yKaD0FgiEZQJArPz9njJl2/CAYN/AgBsY7sY03kyI+AXL/wPPPiCNgDgVyt3wvdefQDc7yaOz8Nusgk2u+qv+OD8z2FrOzakLSFhbRDSYleKw0oBOmKx0k/YrYexDdcaG9STjg8Y61dOeqX3UjiDstAWYi49MSlMZXz1J1HTM5GEUKktr+Fx8sPsTDl6J1POfHtxrzZ/hgniwiIAbvmAEDEQJ3BWQEa0+sMm5qcEA60IwVldeGhFbpky5py4YLIF+84nP73Zm2MDYdESEqJZWGqlL+oZkANsj2rmV2r4S4yfvWNKGaqm+BJPwoZBIijjCHne3nhiF83uWDWX8OIZv8Mu+XT/6Pjoh/OyGZjn/1VsYn6HT778SMzZYRMMfPfXALvVv3hDwBB2nfEI9milzJOE8UO9a8eJoOcNmz3Y2FKsbcWtKoQtzM3xYWIMQumj2oNyEgyxLP0mT2+ErXlMqGbqCCQEQJwlQzVVIES3jxYMkfeXQDlI4CmBrJCuT+J0PL/U4E0h3/3DBEDJimMCYKM5tmQDIIMhQUHallwSg4lqpZ1qanMIrpOooAwtbQV/SaWMoGonrikk9cf7vSfESNhASARlHPHH12X400s+Hm9bmr6aZ2947JJPx2/edBle/+AhWPyjHNyZAAQlIWECgCG+a4fR8V07YZZO1VbcVs+JD2ELmSclWx8spl07BVt0ffdJr1QSU5a24TkRV1NOBLELBfC/+ovqZBxOusa31DYSUdc3PCyMjRGADPnOHYCt+ORUis8DAM4I0mIlVsFIa0Tbkq0AsBBT69Jhne0T1JLcVsec0JqcG1f5T/zwvKLm2YnEbk3fxRuITVl5TRoqiW83NnXvidPyTsKGQSIo4wA+eB/8+ZgW/mHfm2EneAO9JYN/2PwW/MtHTozGux2/0oP52e3jvLKEhPEDg7VrB0ABQg+qnhQI6kloM87QFZ2pEzp3dAhg1RrbiKv3ZR3nKs+J1PNMpDLBxpJNjYCEAXUkauSMykBNZVkfBUCMzzuxUD+M950YkL8toaqjaxQdBhgIi2Si38N5lYOcDr1zSlas5Zj3Erp5LDF6lMXcFDCiahI6eAQ6HDhmn/iW65h9sqagukDcap07/a3H2m7sycnEPmxPGSSCsoFBWYbH9pmGe1/1ifFeylrjyMEOjjzqU/H23n88DVvdlEHKcsMuxFhQnscEyoSE8YDzZZ0CAichMTa0EWfNgXbcJCe9QE64uYXW2Do5CS3EMVzMl3WoUHJCdZ9J+OVfUlXCCJN4h4lwf7oQ482wog08YRgv4IkLkZKQ+IK+ELPQmuw7kcQYTZml4LA1KMhChGGNvtCyVXJCmkALg9rsHYrGWK6Xd3yibsiHCftpdeifr1NvM67KZQJQai/eUEgEZQPC7r4Lll5S4vitrxvvpawXjj/lOlz70r0w+6xsgxpo7z//OXjWC/+A42f/GuNhIE5IAICulOhK2UiMjZ6TGGGvKkpXsjhxN7QUd8ocPV/mKWLiaV9rbGmq1lh/Mg8GT9OjqIiQj7GvCIqusf54NMkGBcU9/dOrWFVD2MKTDU9apPKhiBDEAJzr55hepV4wAFhf9hFAYAAr8X6VYfRziASZJVgSTZs1FqU47ezhSoEKJAXhs8ULH6G7ialSnXrUJCD1Th4M7eipKydqmtVuIrGpe2dDIBGUDQEiZAu2xtLdNsVnd/vwpA8Xe9ucP+IfZt2O1+72r5i9fAHKvzyEMZ3p7pHtvgxf2eHHSOQkYbzgRNuKo3qCqp24J7YxaTcYY/W6abQUh7JOnEjsyYn4QXaxdbhetnFeQamXaxw1g8RqrbJRQXF1H4pUHT5PAwyBGJ24E5NUw8ndl3bg33+IUkPB2KtlHjHq8wBBb5OAYZQQUGV0ZVFTrPUkroRplHhiu3GQcqRW0glGWR66USiZ1duKURlpQytxY58KINw0/iaMHRJB2QCwM2dCPsu4YLtPYvtsarTGbp8N4oIPfhIfWPRS2KNmwi1bNt5LSkgYU4TOnU4tlK3TF8q2gtsxLTaoKaG00+NmjH3XtxOXIYjNmWqonSBmnDRaYz3hCCWeqJjUVYH6HBnxc2P6OlLWddBdICPk4GPs9TYzRc+JGFVUOPOfC4qv08dErRuOlMNEJYUgLJCMQJZBviZUWhMNs4XTMk/H5cjIxUGKIXW33pYdM1BWg7j/apvx3U91P8+QbJSEDYpk9dkQsBb7b7YILxjAhDfFri0sGbxgADhm3q14/Kg9gOfsNd5LSkgYUzDEB7JVoWwcZsP469pGbKNHQufGeIWkZuqswthso6UYUvNN9P2ib7TChhOpoOouqWd21JJPq9v1k7FUj6/Fps8f7nPCc4aSo2EzREILtKsUoWYwGsUcEwnqEldx9mEGT8N7UgtmC6+LIXbDkIo6MesPayOWvjZkT+6kuj9hwyEpKAnrhZNmLcExF34Ue//sFCw8frxXk5AwdijEee8J0PG5J2HeTr29uD4MsMcZul45CXN2qsyTWtdOWQtjW13niVQnd+MAUzR9JpE09Csq0nd/YAFrC9ISTKWUqDpCwYNiRJUf780Q4+0kdXWFSdUVCEwJiNMpyMigsfEc3ocgMHDeK1M6A0OieSjOgk1V9nFh9k4gea7Wlh1MxiOhTvo8oTMNAld5UEJ5LFV2NixG/ef8hRdeiP333x8zZ87ElltuiVe84hW45557Gs8REZx33nmYP38+pk2bhkMPPRR33333aC9lQmDZ8c/FH859Bp43feKksY42Bk0Lxo5depE7dF/cd8GBeOWOvx2zz0gYX0yG40YBBwfx4oWqI73GlkVFpe5B0Uh7P8wOVaCYYxNbYpnJn1hNlXoaBtqVqAb+9ZUdYodOqaUcU9ZOsqXfvMFTr/fdXutNVRTj+m6XVfJqpdpUnz9E1eGaatIPr3pIfZ8wNfw6jCqMrfHSmkk2Dgb0Pp56W/aQILZhDLLU/3gkKaKWFksQo5sSt+G5ZML6Y9QJyg033IDTTz8dv/zlL3HdddehLEscccQRWLFiRXzOxRdfjEsuuQSXXXYZbrnlFmy11VY4/PDDsXz58tFezviCCE8evQJ/OuEKHDFYjPdqxhRE4X9GHw8/bwB/OOlyvG/LO8fk/RPGHxP9uOGEwb68U0+NrXJPquyTOG9H6nN2bAwU05KFPx/7E7L43A6UVdcOHDSQrTZ1N8SyAxU5qUgHPJmpkYgGERGYwm89ge3xWm+mV3tt/3vWSFGIiA9rMcUIJGU4ghLyUVy1T0LgGgvggufEDxSsSju1Dh6gMsYGtakgmKK+D4f5fGler+bvSOziIYFOMDbapRSJSSInY4ZRL/F8//vfb9y+6qqrsOWWW+LXv/41XvCCF0BEcOmll+Lcc8/F0UcfDQC4+uqrMXfuXFxzzTU49dRTR3tJ44Knjnku+HWP4X07fnO8l7JB8L5nfRMf+u7hMFdtgRlf+eWaX5CQUMNEPm7EWHuID2bTgYBqgs18mcdGc6zOifEdPFzNigkqSuFMNMayj7Wvhv75hNiS/Mm9aos1BTWn69b8KOHkqyUfqbUZV2UdU0qjXLFuJllRAmW0i4ezWqnHANozHCL5AZCALSCGqt8tpOsIQwWJ9DuJD15DpsH48MbbsF+cM5EDZD4bxdinZwaJ+7OWHtufEUOxTxlNtYUFnBtftqJYumJLag621f4IJuJEXtYPY+7YXLp0KQBgs802AwDcd999WLx4MY444oj4nHa7jUMOOQQ33njjsO/R7XaxbNmyxjbR8eSOBjc962t41YyJv9bRwKtmLMNNz/oantxxapiAE8YXo3HcAEbn2BHMsbqhZoTVlmIn1FBQCrFRPVGvRDVvx/UHi7H+FJf+E6JrkpOG6TS2DIcyRNWlY/p8FMZVKkcov4QSD5UCU/AaN4oqicQyTnW77337umJM8L30lacac268p6b+/eOwPzYxI0b8vB1ej3pKbBlGcw3DEpKROngC8SCKRCQqKYTmfQnrhTE9m4gIzjrrLDz/+c/HnnvuCQBYvHgxAGDu3LmN586dOzc+1o8LL7wQs2fPjts222wzlstOSEgYR4zWcQMYnWMHI2SfVLH2hWR+KGAt2t4rJ13fStx1mU4r9vN2gvekbCTGqjEWIZRtGEQvSjjxF6iVc6RSVIKKUgpsT2C7Attl2K7A9Nhfd7BdB1O7NJ3VbH3P141hegzT9Z9T+K0Hve23ETt6nPeFjPB94XR/cOk9KE5VlEDyCm6Gsz0dBJIX1CdT248meGlqJZ6matJUScQghrepilLbMp38nMjK08OYEpR/+Zd/wW9/+1t88YtfHPIY9fkVRGTIfQHnnHMOli5dGrcHH3xwTNY7GrCbborisGdj5XYbOAZ+gmDldiWKw54Nu+mm472UMcEStwLvePSZWHbfJuO9lCmL0TpuAKN37HD+J7Tzba4AVEGJG9Xi16tpxSwmKibOR9pXk3Zrhk5/ScHYOdyveek74feXJlia150v55QcyzvE4kmNgBz7x0beqGR9nvOvq3kyTDiJ1w2ywygSVatuXfUZ+h2r8LSh+yXss7D/1kdFARAVkahAYZj11K8DzUyXsEz/b6+xnJqiEpUV/xqMVPrpq7eZtai/Ge2Bgp3C45XHrM34TW96E6699lr8z//8DxYsWBDv32qrrQDoL6J58+bF+5csWTLk11FAu91Gu90eq6WOKjrP3gGXfPoT2CkXAAPjvZwNjjv//j/wpxcTzjrlNOQ/+vV4L2fU8eVlu+M3r9oBuzx8B6buYWH8MJrHDWB0jh1OxBtktXsnJMc2E2PrJR4TvScazmZRONvwnjjn8ztq82LIEagXOk4qf0k9PCz4J+Iv/uilkOqXf+zS8WUcx4BrkguUrCdmXv2/YgIAY/REnBk1sBoC5Ua7WGB0MCCrN0WsADmpF8P5oFgDGAgYfi4PISbIhpKVeL+JZAK0UEXVkycjjkBkUJIFAGTGrtffdFh4shKIVCyLlRoiJ1FFoYaKUg+trZd2tONH1J+S+8sWe5IigBGQ0enNRAD5VmrqJyu1+pIlntKEpB+jrqCICP7lX/4FX//613H99ddj4cKFjccXLlyIrbbaCtddV82j6fV6uOGGG3DQQQeN9nI2OMQS5mclZpiNj5wAwAwzgPlZOWVnVRRiIctXgDud8V7KlMJEP2642knC+cOm6/sZHObCANW0XaDqLqnP22kOsxvhQwWxa6cx8K/u5RjGNFuFoTUVE/WTsJIT50CFU4WkcCNv4XHn9HVxk7iZgpstxsMoOnUFpWlKHaHlGPq82OHElaGY/fTip2eTXQuMlA9DFGcPVT4UqDrSr4yMdPgL91sB2TpBqTbTdx0AbO06ABhPUmxtLxgILAF2CjlzR11BOf3003HNNdfgm9/8JmbOnBnrw7Nnz8a0adNARDjzzDNxwQUXYOedd8bOO++MCy64AIODgzjhhBNGezkJCQmTABP5uMGeQTivnuh9nqSIiSUdAD7lVG+XUoWyuZpvgkOZJ4SJjeQ96fduDKOmaOmmUlDCr/5oYA0Eg/U6mL2C4vREzKwlmBEghgBj1DOSCSQzgO+kgTW6F/x0Y861+4aYABIQEwxEB/+F7h7fvRONqsHHMZwg4veLGFUvuFbecWyGKA1jCYmlnZpyYtRzErt36uRldSDxBIVBVmAswxiBJYE1AgKQGUZmOAbUGQjUli0wxLBg2EBeIA2iAgBmioTEjzpBufzyywEAhx56aOP+q666CieddBIA4Oyzz8aqVatw2mmn4YknnsABBxyAH/7wh5g5c+ZoL2eDwW4yG//7vl2x424PY7ZpjfdyxhWzTQvZ2Y/ijy87ALu+83/hnlw63ktKmOCY7MeNfvVkOAhq3SmCWqS9ZnWQo6qVOGSe1L0bUSnpVypqxs7QXVN6clLzmlDhABdIil6iXP3kQAKAzGqZRwTEBrDKJsgxRKxvJdayFBN5U6nvzEGNaBmB8emx5Kchq/dEX2Ocqk3sS0AxXZYJ4sclhw5gAOtlkl1nENVUk6GtxPVZRPEykzWSFf92MEaQWUZmHXLrYImR+S03DoaUrFhiWChpsWDkVPrHlLAYAGaM8qjGA6NOUGQt4pOJCOeddx7OO++80f74cQNNn463v+hbeMPshwHk472ccUWbcly327fwqfnz8c0L9wamCEFZ4lbgkd7s8V7GlMRkOm5Ek6yYWO6Jj2F4ktIo9wTzJ4AwabeaTVNTSoZpfe1v061KPYG0BAWF/XWvmATVpE5OHANludp9Hw3IVkAiEPEFBAOfV6LeFHLqQYGIdueQgDyhIFalhFgJGvV9r7gP/P5QYuP9LGE3Sm3f+f25NkbS0UIwxNbNsZVplqL3ROpkJfhT1sAXjGEYw7GkU20c1ZOcHHJyDQWlKvNwVFEsUoknIWGjwxK3Ai++6K3Y8ubloCd+P97LSZjKED9ttxYmVuWLNLtmKuUENXMsg3yGCYJyUjpQUSpJKUsIM+D0OtZU4skywFpQloECsXEMWIOQwkaGtPJDBmIEAGn5AgQx2mlFpCoKQDA1A22YfMz5hiMcaw0iSOZLOOGyrpz0GWTRT1AyNcpyW3S/GAH8biODWKYiaOcOAaqc+BJP25Ro2xKZUSVlwBQYoAI5lWiRQ8sTl5wYOQGWCAYGZoqQlERQEhLWAh0RzLm7A9x8ZxpomjC2GKa9eLgAsSqOHc1pu0F9YVZCwV668J4TYVVO4BykKEc2hQJaqgkT2IkiIYHxl8xVNH/NDAuRyosi1Ggvlr7vgtp9Ew0N4lFXTaj5WD9h0S9XKSrBTKuXzT9u2KV1g6yBjOg/0VKPwOgM7aie+DgW2FTiSUhISEgYU4QTef+E3SE+FH+99MmvrpZhUjJQlKDSAUUJ6RWqoPR6SlDKEsIC8DBeFGNBhlRpsRZotQDHIGtAXlkgp2zJlAwm75f1ztm6F4VISz+h5ViCMTjDhCQmEcEMm1Um2OgxsRpzz+H+teEFmZpjTSYwhmGtwBr1nqhJlmN5J/Peksw45MbFMk+LSgyQKikDpvDqCaNFhJwMMlhYMmteyyRAIigJCQkJExQNm8UwykrDe8LwyoXPN3FSeU5ElGiwi8QEzkHcakyywhAHgLRgIGUJMtoyTY6VpAQFhRnEvpzDALjpRelXgurfbyLzE4nyRt1f0izzoF7SCamya9HJo+bYQEgQFZOqg0eNr0E9scF7Ao55KLahoBAsaMqQEyARlISEhITJhXiibxpjq6TY0L3jojEWpYMUhVdRelraGU41aXyOUgcpehBntYxEpD0AzGqSLX3jtVMXLBnfxePXpeUfAerG4MkEQnMQYF0x6SclJjxXVp8w5huCjJEh3Tu5ccjIafcOMTJysXsnj9cllnbq/pOcDHIagwC7cUQiKAkJa8Czf30MOjdtju3vfRAb5wCDhMkAqvfgMqv5VbR7p66YiAuhJOsA4ai8kCG9BIA8089hhp6ldR0yRUyalWJCQ0hJPftkiKISSE0ukEx8R1LwnFQbgGG6dwSZcVraoWozxGhRiZxKT1jKhv/EgqZM/klAIigJCWtA8dPNseDDNyZykjBx4YkJBTMs4BUWjmRFXCAZxeqNscO+v0CcAzFDfAtzKB9JCG4T34pcW8+kR720Q3W1pG9gYH8XD0GJSZ2chKYnQqzd1cPmgjE2M76046/X/Sem0VJcJydTJ5ytjkRQEhISEqY6RImK1AnMOr+H+LC2oMBMrXJCHUIAZwT2ZR1VSmiEko6/zOBbrIeBFc2SyTiWdqzlaIy1tWh7A90yctF/EgjKgCkwYHq+1bhe3iHkZKdMe3FAIigJCSPgvuIp/KqzDfLlU+XnYMJGj9VknmyQ108mNBSR5nDAIWUd8uTEVK9pzOUhxBwYIiUo9Xk79ZbicB1A9J+YfkNsSJSthbOZKWaQBRJBSUgYEX9/66nY/l+XY8vHfpsmFyckbEyoKyh9Skm/96ShqvjJxZwBkvvJxTGgzZMPy7BWO3VamUNuGLl1aBmHli21pNPnP7G1zh0DRguqngwQY4AM2pRNOXICJIKSkDAiup0WyvsfGO9lJCQkbAj4MLZGQmxfZH2Mu6+1E9d9KILwXKm8JjXlJLYXh8GAscTDMYQt8907Wd/90YMS7vPTi6fS7J1+JIKSkJCQkJAAVOUZ6CUHtcRv9cwTrt0vdoTSlwGQ+anFRjSYzbKSEsPIDcdo+5Z1aJlyiHoSwtksOEbbWwhaxLF7Z6q1FwdMPU0oIWE98f2Vbezw36di8++2x3spCQmjAzKAMRpR/3R/cetPf32fyV5OqPtL7DAtxDVywv2psbYiMcF7Up9gLJmof9jH2je9JzoY0BqJJMWa2uTimnrST1J0K2N5xwA+PXbqmWMDkoKSkNCH65bugWec+zvw8uXjvZSEhFEFEUHIQCNi1/XFpppuPMkhnqdV/hFNwYXUyjmNDh1qdO/UhwLG7JNMlKTkUpu5Ay3rQEmKMdLo3AmpsSHOvj/7pE5MQu6JIUYO7d7RJZkp6T8BEkFJSEhImPyI/ghSEuE3sQZkNQUWWQZhBlmGCK9buzERKM+APNepxtYA1kLCROPweX6bqD/o2WoarmGdbWhYSQaZShVxLV18Y95OBsAAnPeXd2T470q+rdj68o717cWkybHWG2MtMVqmVIOsKXV6sSl9O3GBtikwQL1q9g4VGCCHVm32zlRVT4BU4kmYBMhWAT9dZbDErRjTz3HCuLlb4O6l83wyZkLCxIb0H8EJFWGwvqzjyQQCWbFPo0RD/rXhff37xfePJGUN6xtjUP+N/s0ghqbVJwwPCVqrkZN+74mE19VvA9WE4/DZRrS0UyvvWMNqjB0yd6e6r2o35ug7CR08wX/iP37Kzd7pR1JQEiY8Flz9v7j4B6/G0g8X+MUzvz5mn/OHooOz/u0tmH37oyhXrhyzz0lIWB9UJYZaC2xGOknYGrAARgRg/9u69AbK0gItgAzBGKOx90WpoWvDqSlqnlBikmegVg7kLVBmNeLeGCCzkMxAMgPODMQanVeT+YAzHxNfN5+OJvoj40OnTCAikgWPiIEpEdUGcQC4IhVUIynS8t08XikZVjnJpLbvVUmRtvgsFKl5T3QgYJap9ySzqpyEuTstUyIjRtuUUUEJ6kk1rdg11JMBcphuCANk0aZ89HfqBEIiKAkTHu7xvwGP/w1PrthjzD7j0ie2xzcffhZm3fU4ynvvH7PPSUhYF8hwKkDtpFqFghGE6kFhValHDIHEgIzxMfQCykTfgNV0IcMEsJHxBCXPvGJi1SRrbSwfiTHxM6IXw6s49SCz1X2Pp4sq5CyQE6jfw/jvZKDfj0jJCvlJzEPeKLQL++fZZitxHA5IfZ08NtznvSfBcxLkjRjGhqiaBO9JUE8MCXLjYhtxc/aORO9JFXUv0Xsylc2xAYmgJGz0cML47GUvwZZX3gbX7Y73chISFHUCIkElERU8LEFEwFZVAW1zNWBWXwUEkEwlCyqtelPaLZ3NYwzEWBA7VUEAja/vR3islfuSkQXluaomeQZkFvDqiSonBmzDbTQn/9qm2jMkiXU9dpE1jMwQ2LLfBwIQqyhUGoyYsljfvzY8jZrlHL/GoKTUlRP9LgJp+Xk7PkmWcm54T6xVImJMFW0f1JOWcVFB6TfF1r0nLTi0oMFsOYABslPaHBuQCErCRo23Pbo3vnLz/tjpzlWQRE4SJgL8L3UJRMMTlHrnSLUpURFDgEgkA8TkiQi0JMNePSAXPgLCpNxgdfH1hoAsAxnvO8ksJPOXxpMRE+bVUHNdjcvmCX9IbHxoy10HBBVCAFhjqinB4QuOAKmRvrhva69pkBLU1ldXTgI5icMAofN2DJSc+NJOnLvjW4urrh2OU4tj947pzz5pek/qc3cMprY5NiARlNECM3h99MopCBaa8GbTr/zqOdjljTeP9zISEiL0pKmlBqCfqFDM3WBLICcQ8QqKeJLAiKSBhCC5EhQCqi4fQ6qmrE3bcJZFQ6yES2NUPTEUPSgVSWkO1+NhiUvzZB++07rCGj3uWn/CF19WkTUci1Vx0n0FoOmRCSUfW90vtqacUI2c1FqKKfPeE09OrBVkmWt07oTU2IxcI/ckEhNTNss6cDVyUnXuTNVo+34kgjJK4CeexGfe/zJ8aG/Cr4+/BLPNtPFe0rhhKa/Cs794Fja/Q7DpE3eM2vtu+alpeNbPT8P5p30Wr5j+1Hq91yV/2wGfu+LvsNNvVo3S6hI2FhhiQBC7LADAkl56l0cc9la9pt/ISTGHAwJILvE6uYqYUGiDFX/CRM3T6kmLiPdPZAJxnsBkBBajhlgigNWPQiUrySiNEpTMDm+QDSBqEBN434nkFrBKfgIZ4tz4WTTDBJ/Z5jYkT6TWtsu5DzrzXo7gtakbYo1XSjJiODLRj8LBJOu9H+oLIQ1P8/sKjoASAAQMAjndz3XENcUsk0BMPFlp1ZSTjKuWYgPYzMEYJSe5dbBG0LI+fM04TYy1Dm1bIjMuthZrS3HNJGt6lVmWHAZIkMMgh90oyAmQCMqogTsdzP7CLzHtr/uhe9zEVg3GGl1hbPOjAq0f3DqqQ/ZaP7gVW9+8Kb517N7Yq/V97JjPWOf3cML4c7kK1z78TMz95K2QojeKK0zYmFGfQAsMJSveO1qdPGsdJ/p73re0ij9hemMnoSqHxO6d8LpY4oE+wXiTpwDIDOBEfaK+awUZAFepKeC1UFACMbFKStSPUnXoNMs7/eSEaqUcil0zMU7e7486UUHIFzFNshEu+1dc7+KB38da5vEGWePVEhJwRmpY9SWvWDsb8qaValIP44iG2EBOjM87qSknGmnPcd5O5smJDeUdb3ZVw2toMeYqhM23ElsIWqiuqzHWwE6RsLy1QSIoCZMKbukyPPyGnXHcfm/F1877ILbN1o2k3NIV/NvZZ2HW3X+DS+QkYS1gYPyo+zDuXifKWkjf4LYw2I2rHIuQceFPUALAWqMySGbAIr7ldehJRwxUFRECXJ+CYitVhUEgq4RIT8CqohgnICcwBhBHMIYg7JUTZ7T8ytAQtxEQ2m1VRaF4KdYo0fATf0NbMefhepgGjDjlt1nSQaNdd1hYUaNpJrFkErJEwsneGoYR482nAhGGs/qzSIQgxoHJRLOs5AxYggNgSlVSxKzm+/s16r5As5XY6maypnKS5yUsCfLMReVkICuQGcZg1kNGjGm2wDSrQWyDpoecnFdQCjXImmCQLdEmhzYBA0RoU75ReE8CxlwnuvDCC0FEOPPMM+N9IoLzzjsP8+fPx7Rp03DooYfi7rvvHuulbBDkywu865HD8I0V6/7rfirgGytm4F2PHIZ8eTE2H8AO/Nv/xZzbl+GdD70UX3lq9hpf8sOVOd726N5426N7472LXobZtz4C9/s/js36EkYFE/G4oT+m9WRVlXQ8cQm3+wO3asbN0GpKoRU2liP896Pwi10q82hQH4LqEMsNdWWirlaE4XYVcag6a2pb5hWR4B8ZYYN/Xnh9zDrJqGGQjcbe2pq5cZsaXTP171SFn3mfRyiPRXWlCjuzfftzyH6mEIoW9jNAtQ4bVZ0qFWS4GTzN0pSgUZ7qayUm6z+DJContuY5aSonYTCgq23eBBu7eEodCAgXpxhbkkZq7MZS3gHGmKDccsst+NSnPoVnPvOZjfsvvvhiXHLJJbjssstwyy23YKuttsLhhx+O5VNg9gn98i48eFiOcz5/4ngvZVxwzudPxIOH5aBf3jWmnyO/+V/89cXAv3/6NWt87v/91j/jzkNm4c5DZkFevhzlogfHdG0J64eJdtywpD4OS9Bf6l5yz6mskZFquFtGoTNDT0gto16EVuaQWUaWuVgCCEmj4de4tBjcEnBL4gm/kVoag8Fqk3b9Y2w1pt21CK5N4Jb6Qrilm2tbcNuC2xm4ZcEDmW7T8pG38JyWf13bwrUtXG7g2rpxbjwRomrAXkYakpZ5T0pWrXvY7xS+c4vjvgj7xpCe+DO//1re2xFadHO/f3Pr9294bu5grIOxoq2/OTf3c1vi5464tQXcZnCbdW2hpJMzTK7vbf1n5XmJ3Dq08xKtrMRAVqJtS0zLCgzYAoNZD9NsgRlZDzNsF4Om19gGqNcIaKt7T9q0cXlPAsasxPPUU0/hNa95DT796U/jfe97X7xfRHDppZfi3HPPxdFHHw0AuPrqqzF37lxcc801OPXUU8dqSRsG7OCWLYPZSKsHpge4ZcvG/oP8ft78zgK73PDa1T51i1s30JoS1hsT9bhhQT5aXHwJh71vQLectJQQfs3Xfy1bY5GBhqgpwbcg4n/hh86TuoIfjJ5SmWrjpTfQQhA9IMaFYDJvCCVREy3p+uH8+xv1p8jalgv6PCccVROqkaWRW4vrRKpqM5Yh3zVehjRWg6ie1FWTSplwYBAcM3KjKpZjArMvScGX02DUD2dDv7b//DWdAaNqVfOceJWr33NSV05yr5ookfJ5J0aNsS1TIjMcU2PrbcWt2sTili8jGoRI+42ntBMwZgTl9NNPx5FHHonDDjuscaC57777sHjxYhxxxBHxvna7jUMOOQQ33njjsAeabreLbi2jYlk62SR4tL5/CxZ+f7xXkTBaGM3jBjA6xw7NnJDYWNICo/AnjyDL1w2OTITcOLAQWAil0ewRSxnYn1hZCMYIxFThYgJoh0kf1KMhEO9TEVuRk9Dh4/JwH6n3hAGAqu4UgXauWAKxwJS+U2ZtQSFgTT0pnJHPCqkuhy+RUKWc2FrgmV0L74n3nDSzRJQIaNCZboAf6OcZDmcOjqUZ+1DqX5LBWm7yTEnyNQxMrBlhQ3w9jPpO+luJ88zBGo6+k7YtMWDVezLNFsiMwzTTUy+K79BRD0rX+06qgYA5uY3aexIwJgTlS1/6Em677TbccsstQx5bvHgxAGDu3LmN++fOnYtFixYN+34XXnghzj///NFf6BhiwY+XYy93Gg5+9W34xNa/HO/ljDlOe+i5+NlX98WCn07+Ml3C+GC0jxvA6Bw7DAg5WeREcL7zQvMpSuRk0SKLtikABrpGvVeFWDghZGLir/zc6snUWfbZJQRmUpMq1ZQPK0BLm27IUazDxy6ecH61agR1BiBWUkIO2k7MACAg1s4Z7QqqunmcIdAazs11hAF5wePC/WpJyD2hGgkZSUmpGWWHbduNHTuqMlnrPRxWlYncE4Dg42AQBlD4v5USE+OzUEpXtSbDCQCr3TziJaY1dTF5lUdbiFU1gS85jdRKHEp6A7ZAy6smQTkJLcWDtqsG2NrcnUHqokVOW43JYYAYAxup9yRg1L/xgw8+iDPOOAOf//znMTAwMOLzqE+uEpEh9wWcc845WLp0adwefHASeAhuvhPzL74R19+383ivZExRiMMj5VP4wT27Yf7FNwI33zneS0qYhBiL4wYwOscOG04QIK+iCFq+rFNNmOWGD0Ufq9pItatHYlePKgNcy0WBbzmWqsXVp5QOMcjWTvhxeF3wo9jKnMohat5WZRnOyXtCvIF2LTfJqtfGck5NHQmfxRmGTv6lfnIi8bH694xqhd8fleFV91Vj//n9mcdSml5Wg/hY23tDu69VtaMyzmoUPTJe7UY+38RkXtUx7N+LR2wlDmWdoKK0/CBA7dSpIu21a6cXO3dCeSeEsmlq7MbpPQkYdQXl17/+NZYsWYJnP/vZ8T7nHP7nf/4Hl112Ge655x4A+oto3rx58TlLliwZ8usooN1uo91uj/ZSE0YB/7V0W1xzzpF4xp+Wwo33YhImLcbiuAGM3rHDkkHuTxIDxPFXe6H9vRiQXBUUyuHIqKfAlHBCsQzRYwsDQWGsln9YT5wAYIQgpCUhlBi21MOZaOiaKBHQE62qI8QAmGBK9WzA6OMQgjivsDhVVki0zIN1UFCq/BU0FBNupK0iqiqxqycEs0XlR2pG374JwD6J1WTNOTahhTioE1pGqXwdxisiBoKez6cvnF4G309R2qikMBOIwuDENSgo3vtirJLJLOPog7Fe1elvJQ7KyTRbRIIyw3aRk4vKyaDpRuVkuumqgmK6OqmYSrQJmE4Gg9RCTutSi5taGHVa9qIXvQh33nkn7rjjjrjtt99+eM1rXoM77rgDO+ywA7baaitcd9118TW9Xg833HADDjrooNFezrijt3gQVzy5Nf7qVoz3UsYEjxSbYMb//BHu7nvGeykJkxiT4bhhYGI3T06VByV09MR4clPW5qpol0n4lV8/uVWTbr044ssHQUVpbNFcKqgbTuvEoFIuqI8w1Dp+bP322m+xpBNVGhpatjF102yloIStMRjQhKC52nc14hUL3xllqnbhoJyQN8jG7p2wf2ttu0FJCX4VW1NgggJirL/PutVvJjw3lHVCucmN2Ercqq2n1TcEcIDqUfalHwros0/IIYcqJ/XSzsaMUVdQZs6ciT333LNx3/Tp0zFnzpx4/5lnnokLLrgAO++8M3beeWdccMEFGBwcxAknnDDayxl37PrO3+Pay56Lv/33DLxj83QST0gYDpPhuNGmDBaELulwnIIcHAowGXQoBwzQEfWiFFTCGQMWQpczOKPtx7BAwaqoSOYgosmmzAR2BmR8MBsJJPd+FDKgUlUWVSLEh6555US0KYdYeUwgCqqsaBAcCSBBQfGvWWdQHymK2Se16cS+7BQVlJqSwiOFspGqJ+RDz0L5JMu0TNbKXPSetKzuxyxOAVbviYOGtXWdKlIl6wdlxDAugyFBYQTWGPWmWH18TTN76tH6QTEJxCeWdDwxqoewtW2JaaY3xHMSlJOgmoyknLQp22jm7awO45Ike/bZZ2PVqlU47bTT8MQTT+CAAw7AD3/4Q8ycOXM8ljOmcMuWwTiH//zZofjlngtxzY7fwAwzco19suAp7uCEP78Cd921HZ7RG9vMk4QEYGIcNwwMchB63ixrpVJPnJjoRQlTaI3vOGEhZIbBzjTmxwzZjEAHBUJbgo0fBAgt1YRUdob33hC0ZdYTDraIs2uqbH1tNfYVnzgfcJ1NshiOhAxvhK2beYdkn0TS0meKNb6cErp2qDlkL/MJvZnhqJzEtm74Ya0WKESzf0u2gE/abXwXTwrXRE4CyJeYiESVmUBWfMhaJE2+rby/lbiunGhb8fDKSZsQA9ly2nh9J3WQyOomRU1MLFu2DLNnz8ahOAoZ5eO9nLWGPG9vXHbNx7FLPn28l7Le+EOxAv9ywumgX9wx3ktJGCeUUuCn+CaWLl2KWbNmjfdy1grh2PHEH3bArJnrfgJwwlglPXTEYTkLVorFCsmwnAfQkRxPuunocI7lPICVro2V3MKycgClWCwrBtBji5VlC12XYVWRo1tmKJ1Bt8jATChLC2HSoX+FUS+KEMCA6RpQqT6TgDDozhT+OQ4Is3ziFu4LXpT1GJAVS0h1laRGTlDv4vGlKbYSy0+cAZJp+BlCSSsEn2UCYx1aLS3PTGsVsW03dMYMZj20jMP0rIu2KTEr6ygRhGAlt9DlDKtcjpItVrgWes6ixxkKZ1GKQeHU/+M8OVlbBSUE8uXWRdIZ5uq0rM7WCa3EM2w3thIPeu/JTNNBTiVmWb2cTj0Mmi5aYMw0BXICZnqf0zRqTWlysmw5Y9Nd7l2r40aaxbMBkS96DK/49FvR3v9vuH3/L433cp429rnlOHRv2QwLFy1CueanJyRMGVjfVeFCBgoYLdHuHSdGlRSiyosiFm1TAoyopIThcLl18URZMusMHSGd9CPQOTwENc0an9lBlSIQFRCCDgRk35osiEFuxN7jEvLJhNaboAwp39QD2AiaINvnL4m+lFziZOF6aYdMPfOEo0fHklRJvD7wrF1r1606ptS4bL2SYkknFYduH0MCy6pelaKlt3VRUMJ7BNVmwBYxkK9tywZBCWsb8NOJQytxUE2qzWHAZ520iNCmLCknfUgEZQOi/MtD2OZ9D+Gx/3sglj57Faw/0EwWxlyIw0rpgb6/Kba5/MZEThI2SmSwyCFoEaMQLfW0RJNkWz5Rtm6MDOFtGXH0opQ+ddaShrVlhuGoOmlqfqiGuAkrAwnDAoN/RAQwTHqyD+US6CRjCsFsgSCEmJWQPvt0USvbNObqZJURNrYR15pPxKhyUrUUSzXLxrf8UjDFApGc1Nt2W1ZP/plp7tswx8aJgTGCQiyMCAq2gNWjlCFBSQY9ymCFnzZBaZnSXzq/Do6XbVM0EmLbfqu3EleXIetEoiE2eU6GIhGUccC87z2Ev198JgCdnXHUO3+Mt82Z+MPrDr3z1TCf3ALzbn8okZOEjRZBRWnDoUcOA3DokIMDYYAKb5bNUZDFgCnQlQwOJiooLdtsyDe+7GB8aFjwozhYsJc7hAkoat0wHvUwN/EnfJ1QrJkxwRBL7NuSBU/PIBtQ95bEziJPPnxJJ6yRM0SZRzKvngRyknvPSeY7ZPx8Iutn7VTERL0dLU9SYiKr9QFnkQRUAXkMUnKSASXbqJoUbJVI+oRfXkuCUh9IGLqHqkslTYYEg6YHSxynE69NK/EAEQYpT8rJCEgEZRxQ3v8ABu9/AABgBgbwtRP3xj7T7scLp3UmVM97IQ4/WTWA5TwNAPDoXVtix//3y0ROEjZ6hOC2FlFUURwcjBZo4q/7gqz+uieDjBycIWTMYEPIhOH8dU2ZtRp5bwQiAja+SOFVBTHDKCnWm2Cdn9UDBBetRtl7E22cmIx1M8f2I5plbR9BqXXxhM8R7z0BgqpSU07irB0B1ePsaWhbsfEeEw294yHqVBg3EEhKm0oYI9rZg5Azo+MKjBiwL/EAQw20Q//OVReP8QFqhqSh5LRNGQP7cqMlnfqaRmolrhtiJ9JxfyIhEZRxBnc62OINK3DhXidhk8s/gee0J84/1Nt7jAv/5SQM/uExAMAuy/6YwtgSEqAqShs5HASOHAr/X8aACW3GGRwRmAiF0f+muybzoWIGpmYEscz+Pf0vdWdQkAaLOScogWoOj0BZglNTrLQYYELdVmJKApz3gCCYYwmj+QNdCUqNLAWfSVaxH2kLJLCh4DmpKSfko+KDcqIpsKqW2JjC6jBgy9i629+2O9Ou8l0ySk5y0VyaQiwsMZwx6HKJQqyqK0JKUFAZZdcGuf97NcpLkaAUsJBITAaop5H1poqvT63ETw+JoEwAlA89jMFWjlN+cyK2nr0UAHDcvJtx4qy/bvC1fHbZ5vjSI88BADy0dDYW3LME5X2LNvg6EhImOoKKkhPBisBCdLpxbDOuB7npyQyozLLsL4EMLePQA2B96UGEvUeCwUwQGLBlX18R71FRySRO5Y2dKX7GTK2dWKgmo4wCQotw3c8itk5YBI1pxTXFBMFrEkLTTMgUqQYCtkw1b6cqrwxVTQxYDaemBwf9OzgYGGE4EArOAAMYEeTiIklxXtpZU5nHhC4ecGWU7fO/DFAJQ1ypJKbwM3WKGF+fWomfHhJBmSAo71uErY62YKP/wZx3yatw4j98coOv47wfvgq7nPVrAMBW/ChKTppJQsJwqHf0tMkB0LwLICgoBjBAIV10JMegtehwHmPZw8kvngS9kkK1+0rSYDERgTMEYS06iBGIMUpUGJAcCOYSNoT6eCLqeSJjRzlRggBu1RST0KET4NNi4X0nZKqOHZu5mM6aGfWdZN53Mm2YyPhpttfXHaNqxXTTw4DpYTr1/L72AxlFO3pUQckjWVHlxMTLtUFoYwbQmL8Uyzi1kk4gJlXHjs7WmU4lcq+cbAytxKOFRFAmEthBvFa79U+BHfmNeveMEj84/KNjkp/yh2IFXnzdGTBP6T+Frf9HIGVymSQkrA00uM2iRQxG5UXRExfBMUXlJJR66r/ate2YPWmxcKY6abGl2vwYAyKd38OsrcgAa8mHtdwDBiDUJCICsFu3ycVrC+3eqVSSKnjNE5MwCdjPrwFhiHISJgFnNVNs6JAZsIXvjqkG7VVkQPexA6GQDCsAFJLFsDyQwYAp9LZhOBgU5MBi4KBlnnWB8aMNwmUca+AJioVgwPSQQ0s7LYRoe9/hk1qJnxYSQZmgGPz6r7DT1/V6tnA73HbwAizMRr/kc1tnAXZ//2OpjJOQ8DRgQGpylFKzUcAAAQXpyXGAChRGW1sLyWCNNH65MwiGLUpfyijFRFIiAIw/kTGpgsJs4JySE4YBSSi1+LKOAwCqvB8CCAvGJI6zj6DEacTWl3P8bZOxnzVUM8P6IYC5rSYAt62mr7Zs6WPs1eeh7brNKcA5lZp1IgYdMeigGdgZSm2ODHIpwTDoSRbLO0+PoPjpxeColuTkMGB6SlCoiKWeFhhtchjwU4mDcpI8J+uGRFAmAfjRx3DZucfgI4Oj/w87X8mY+eido/6+CQkbAywZWOicHqDEdMPoiKCgEjDQzA0Y9MTCiUEhVr0SvhsEADJSX0Thu0tKqibxOjawfqZP6SwcszfQGjhXNekIk26GlKhEZcUHvI0RQYlKSfCb1Mo4MbreVgFsxng/iVdO6obYAVs2lJNpVtt1Q0tx2xRebTJYyes2obrynIx8DA3G5ToR0fKOHzoYxhh4BcUSI4dDi1wkJtaraDkYLU9OLKBTqKEjCiCcSMpaIhGUSQBeuRLT//tXY/f+Y/bOCQkbBywRchjkwj5lVks9DpouCyBeDnjyAvhyDwNtWwJOzbIA0PKyRCAxVVnIwvlW5PBfrogGu4lPlA3BbAFjOsskkBPrFRMSGBvmCjGIEMs5xg/ZM4ajchIMsS0bpv/6kohVxUTn7rhoNgYAB4KT0R1xYlF1V9VVkjoxAaBkBNy4DH/X+ns1bhOBRWDrxqCEtUIiKAkJCQnriTblyGDhjMAKA1z6rh7x5k1VRXr+Mpcynshy44ASyEjLPBlbZMxeWTHoGQvHBsZlMSfEsZZ7SqsmWufUm0Lk7ZxC4FJVFTWLjMGX9oSEjMBk3giLSjGxVr0nmWEfXy9xEnCYaRNaiVu2xIAt0DYuKichAI1BWMmtMfgCimCADWbYugl2JFWlboo14T6ogbZSUioPimafMAbI+c4v7eLJkPwoq0MiKAkJCQmjAEsGuVhvlhUM+GyUQrQWExUUn5US/BRgIDM6SKc0za65kkM3j8RQsdD4CjAywLcl++RYUqIibEAxqG0MNRTS0LWgmASPST1sLZCTYScBe0Ns5tuyAyFwMLpJRe7GCjHMjapOnYKs/m0ElTlWwqV6WzTnxsTHHJXxveoqigPBgcHqYm4qLKFGBySiMgwSQUlISEgYJagJkgAu9Fc3HGB66IjVCHzO1ZsS/A3cjieswlgYJyiMRc4ZCnYoxaDrMpTeO2GJYdkiN4ySDZxQ9KmIEEo22uXDAucnIa/tzJmnA+3OEVhbeUwyP+gvC2UcCjknHIfttYz3bXjfSdvftiQoxQIC9HjDnZ4MKnIUSksdzmH9fUFZCZchnC2nckhJKLQZdyiP3T4DVMCKREWlI6qotMihTQYWhDZyzdZJRCUiEZSEhISEUYIlAwjQjtP6GOyVFAed08Os+ShgxKwUB4JhgTMGRrzvATmMJyaGLdgWKMmqeZaUlBTOwoauH1EFRX/dI05HHlMBhfwwvVrImvUKSsuXcXLrFRKvnGSeAADaxcRi0OXxVRCqqccMJoNCOA50BFCREONgRDuxtNRjo3G2bYxXVwgtsmBTxDZoHSDpM3LEoE1BUxE4cci9PyW0rSeiokgEJSEhIWEUoTH44dBawnklxYHUjBlOPEZLGdaHHxUhL0UsCk9E2kJ4CnoCZSH1czhGaSwsGx+dT0pavJriWOCY4UKo2xgrKAREYhJJSs1nkodUWFIzbCADLAQGoRQD5vEb8RFJiF9XGAZYL7cFhSVnF2fxBEUlhLcVYmM3T0EOPbExI8UZQi4WBWXIqUQPBgNwyMV3+0DAvrA1CGhWyjjtj4mERFASEhISRhmBpIRyTwcMhp6YnOnCCMMwA6YyzvYki3NkCrHIKY/+i8LYmJOi03kdSjbosWZ7hIm9jk2j9CNC4DFUUIxXUPpLORlxJCjB/AsAJVcR8ywmlq4mAjI/jDDz+zrcBvycJAgyoz6aQuyw8feWWEt0YVikZJG89KfNFlT6/BRNmy3IISeAUaANh1x0iODGrKYkgpKQkJAwBgjlnpgk6+Neg3k2lHsMtKxgwH5uDMOESGn25ER8G2xtKq9BM5nWCschg6amrow1DMkQ82vmSYrx1wNKNpGYsCdR+jXHrwU3dPGEkk5L1A+jY0d0H2eeqLR1vnTjdZlxKGHAhqq2cDJxy31bueojIQUYUVHT66EMqEZa+PKPtiabjVZNSQQlISEhYYxgyWAaWupJ4S4KEoBLDfKCqg3hV7YTg44UKCRDh/PGULrwCzyoKyVbFKIKihNCj5uKShnUE9CYkhTjlYXYTuxvA1B1RAw6zoeUecMug1C4MFmYxjanZS0R5h8ZEhS+DEXUnJdkSNBjVVdWuTx6VnKfnaJt0YzC2uhXaftAt45UEf0tP6cnKC712T25n3isaoqg4B5yUDRft2l0818mOhJBSUhISBhDqJLCyP2lGmgZDMIAVObXVtSajG+g6oI30AafihGpTLQSFBRvpK0pKoYqAjDWKkrjJI6qHXo4khQu62UoYGx9MmtCGC0Q2qKp9n3CZSAsgPcDxSnLBlwrBeXGocv693HQv18OB7CqKjB+H3hFJVftpaGmdCJlcwAEDgJDOvl6Y8tNSQQlISEhYYxRV1KM9NASRi5qkizEIRdVR1riMCAFVkobFoxeTTnJycWpvF2ToWCLtin1saik6GXJdszVE6BZstHrflqwVCqJ61uDCKFwRucK+ccmBEHxmS2Oq3lIATaoK94kG0YV1L0qpS1rSopedk0WQ+csMdqcIzclOlIpZAPUQ4scnDH+sotCHDq1oLeCHXIAzghysRvNTJ9EUBISEhI2AKKSAusH7IXgLvUfhF/Qhji2IhvRALcW6ZTeMHAw+E06nCMX5x/TluOCLXJShaafHKwN2BOMui+krsQEgyuA6CUpuFay8ZeFV1AcmyFG3TKUeDi0Qo8vQSHS8QE0zDoMAeKzXfS2RL+KoaCm6P7IDPtuK/2bMghMFDuwYIDCaXdP8KjAAIwSxs9tUrVFW5QB+EsHB59STICFpgNPdZKSCEpCQkLCBoIlg0FqqWoChxwlChKvoGjGRi46QC+XEoVkaPmW1aCkdDhHIVnDl6ItySW6nKHlhwuymKdlPi3YohRTkQ5UxKQUG5USEWqYXV2NoABAr7RKUPoIiNSIyYYIk1sTQthcSMHlPgWFSGBJ75PMxfuMb7EurYnExLCgZco4oTqrzRYyLCiNPrfq6vGqmB8QWZCDI4MeWbTIofD+FCYdoAh2cOTAJBg0mPIkJRGUhISEhA2MnKq+jNznpBTQcLceTJzj0yMHw4wBMuhI3iAqPcnQ5RwOVJWBjIslFgczYokntC+HUlCdjHRdBgah52wkOcF462qEpG5yLZyF61NDqlRb0xxe6MPjxD8OmQAlHgLECNgPOGwSFMD5lNywzqC6WCPx+weiUnJFWDIx6JGgEKOlGjHImX16bhk7vHITBkpqzH7IUgn5KcHLoh4f7QYzUiKH+Hb2qUlSxuRbPfTQQ/jHf/xHzJkzB4ODg9h7773x61//Oj4uIjjvvPMwf/58TJs2DYceeijuvvvusVhKQkLCJMHGdtzISb0EbcoxQBYDZNAmYIAYbXIYoBLTqYfpposBKjBI3Xh9gApMN10M+tttU2DQ9OLWNiUGTIFB28Og1dv9W5gb47wSUvhY/R5bdMocPdbr3TJD16nnpXDhtkXhM1cKZ1GUFqXTx8N9ZWnhnIErDdgZuNLClRbs/SfMBHEEcQZSjt/G4dKTqf61utLAOYOy7Pt+/nv3yize1y2r/dRxOTouR89Z9DjDKqf7tMtZvOxyhq4nml3OsZJb6HCOFdxGR3J0uKWXopk4XbHoiUFHgK4wulL6NNqpOZN+1BWUJ554As973vPwwhe+EN/73vew5ZZb4s9//jM22WST+JyLL74Yl1xyCT7zmc9gl112wfve9z4cfvjhuOeeezBz5szRXlJCQsIEx8Z63LCkvTcGFA20hTByYQwIowAhFwZLgY5k6MH/upbMqyklWFRdcWJQiPXKSbiuptoC2p7MQijEYpVroRCDVS7XicmsJ95QwnGs94X5PkFPCEoJc6XOBC9JJB3+yeJfx6XmwWCIWRZAeIzHT0GBER34LKqWNODVFSOkThAXFBT4khDDiQapFYZhSAkfAY3Quh47f2mRGUbLlMgpQ9uW0Z8SS3Y+6K0jOaabbpWjQxmcMRhAgQIOzGX8N5PDTkklZdQJykUXXYRtttkGV111Vbxv++23j9dFBJdeeinOPfdcHH300QCAq6++GnPnzsU111yDU089dbSXlLABYLfYArzd3HH7fPPgErhHl4zb5yesHzb240YgKrlYH87lYpkHPibfgTTMrQYtDklsUXZQIy7XxPG6hyTkqBRiULLmqZRi0PMDCUMZJygCAsRyDgCUzsTbsVQTCQpFUgJAvSUMIJCWfhKi3dZKXMZVACAlIo76ORRABDICcdoaLL4tWodEM8SbhcUIjB85QH5MQdgP7FkPm9ByHYL6SsBlMJAqqM0TjGCe7Ymeojui+Se5T7C1EPSg4X5db5zNYQHhKUVSRp2gXHvttXjxi1+MV7/61bjhhhuw9dZb47TTTsMpp5wCALjvvvuwePFiHHHEEfE17XYbhxxyCG688cZhDzTdbhfdbjfeXrZs2WgvO2E98cixO+Oqf/3IuH3+P13+Fsy/OBGUyYqxOG4Ak+/YMWhacD4rpSsFCmEYcPSnFJ54xJOUV0ycb+/tcl4z0ioZ6frwNn1MiUmXlZB0yryamOyVk2B2jSbX2kyfytzqfSWBqAAQX7JBfUChEFDSsAqKPg6QI1AxfgqK5AKxGH59UPIhmScx8CoLCcgQ2ArYmcZE56CgWMMo/ETnglQ5ya2DJaujCrxhlkHIWOcrGWJ0TRbbx1lMQ0FhGK+k6L/pAgxwiR45MHW1ZDiFlJRRJyj33nsvLr/8cpx11ll4xzvegZtvvhlvfvOb0W63ceKJJ2Lx4sUAgLlzm7+2586di0WLFg37nhdeeCHOP//80V5qwjqC9tkDf91v1rCPLXvuKuzdbm/gFVUo9luOx085cNjHNr/jKcgtd27gFSWsC8biuAFM3mOHzkAWFBAUAhQgFFCfiBIPLfGEbp5CsuhTCOSky1mNoJhISgq2saTTYwsXPBW18DTHhDIoKK4KUwsEhZ2JpEQfDAQllHP8FxFS5cQTkWHhCOSGf2hDQAyN2OskVnT9DkD1VbX32Ms+bAJ5qRQVotouEIIYrhQVU/+0El1kmjAMbTE3JFFRMhDAAB3O49wmA4bhHNYwgBIdHZaAFgkM3JRqQR51gsLM2G+//XDBBRcAAPbZZx/cfffduPzyy3HiiSfG51FfsU9EhtwXcM455+Css86Kt5ctW4ZtttlmtJeesAYsevls3PWGy4Z9bLz/Y/jf538O7nnD68S7fv507HDLBl5QwjphLI4bwOQ7djhhlHBYyQU6wugIsNKXZFZIHolIICcruIVCMqzkNjqSYaVrR1KihMTGrpxurYwTvCZdl8GxQafMIjEJikkZFJRaK3A8yZamKt3ExXsywrWzsweVBCqH/ztRSfBNLOMDAWSkM6EAkomqQIGhENS3wgZiJSoqyAjMqqiQERhjwJZhDMc2ZZdpCzlbT/SshuoZqvJTWAzapqw8PjEPpSr/sNG8FGf0vgIMw+WUa0EedYIyb9487L777o37dtttN3zta18DAGy11VYAgMWLF2PevHnxOUuWLBny6yig3W6jPY6/zjc6EGHxGQdi2a7No8aBe/1+Qv+DH2lthx76W/zoiuc07ptxb4b5H/oVwOP40y0hYiyOG8DkOnY435VRwKEjjJUC9MRghVdJVnAbBSw63PJtxhYd0esr/X2xM4TzqJgMR0xCW3G3zHybrHbW1FuDAzGJbcK1dmAp9QTZ8JX4Mg6VQz0l5AhmpDIOA8T+cgMO5hGdwwfjRm5z5roaFGA8ofG+GjHqrmUwQOQD3yR6VIgqooISyCx5D4t2T+Wmmvac9U13DgMfjR+HYP1ASTBgjAb6WcNwcMjBjRbkAQLs2DTqbjCMOkF53vOeh3vuuadx3x/+8Adst912AICFCxdiq622wnXXXYd99tkHANDr9XDDDTfgoosuGu3lJKwFKG95ydLfthZbvewB/Ga3b4/jqkYPn97mF8A2v2jcd8J9L8STV0wHe3+C9Hpo/hxM2JDY2I8bTnR6rRIUQUeAjldOYummpqCEMk5H8tix0/GkJJCTVS73BlgbO3Ucayy9Y9Mo6ZROH3POd+KwT44VApdeQamTEUdDCUrtsf6SDa2mjBPIiXFDic1YggzA0LC1kSYWGtCQJYkFYKXyrDD5TiD1ooAJsAwWC2EBGSUqxlA84wYTbckGbCkOWmThSFb0eQwmQm50nk+Hct1HPmWYYZBLCQeDlp/vZOD87KZy0s/uGXWC8pa3vAUHHXQQLrjgAhxzzDG4+eab8alPfQqf+tSnAKhEe+aZZ+KCCy7AzjvvjJ133hkXXHABBgcHccIJJ4z2chLWgGz7bfHYx9vYYZPHG/e/df7XAbTGZ1EbAOdu/V28/3svBWM6WAiLP7gjpn3z5vFe1kaLjfm4UVdOVgij68lJUE5Wchs9sVjO0yIRibkYNVNsyQaruIWuC5kbmr/RczZ25gQTbOF8C7HTjpzSGZ9ZovcLe3+JEKSslW7qEIB6Q09+pjcMQRGAnCcjfWqFt29Uj28giAEM++6cYdqLg5hh+r+3qcotACA5AyCgR/F14ghkBTBagmRbTUZmoyMAQtCbtimrUpL5HcC2Nk7AOBgnVdqssfHzC3IwYLTIpw/7bqCQOGuNb0GepFOQR52g7L///vh//+//4ZxzzsF73/teLFy4EJdeeile85rXxOecffbZWLVqFU477TQ88cQTOOCAA/DDH/5w0mYZTBbYWbOAeVs21JIVCzfBf+x2BZ47YPuePXXJCQDs0ZqGaxb+BICeIJ65+zOw3R92BgDQqi7KRQ8mRWUDYmM9bvQrJ4Uv63R8B07wmwRC0vMKSn+3Tpcz7c5xWSzt9DiL5ZyhiompMk6knmFCYO8lkeAl8aWeIWUbUSLSX5YZjmgoMfFGWRmmlOPLPCQbhqQ0CImgYZIVgpZqSJRo9XEwAaJnRstEhNhUTOSf4VuWxd9HDDbQ5FwAZYjO98cYMYzMz1/qcS1l2CsnhdH7upIBDHQhsdwTWpDDZQFGQeynZOv066cx8WBCgEQm31F42bJlmD17Ng7FUcgmKTMcDzzx2gPx3ndd2bhv0HRxYNs1orc3Rvy218HD5WwAwIcXHYHs5X8Dr1gxzqua2CilwE/xTSxduhSzZg3f3TXREI4dT/xhB8yaOb7Sd1BOulJihTAKAZZLho4nJCt9mugKVuPrcjct+k0CMQlBa3W/SVBOVpWqqvRc6NSpJggHQhLahZlNbBOW0lSGV0BP4Eyg3lAFxfSGdsCQQ1Wykeq5QUUZQlCkpqJwRWbGDKTqiRhU6kndSuNvi+17jAC22lUjtcMl55XaIgaQFtdeo8+nnAEjMJlaXslH5xsjyDIHS4J2XsZW5LYt0bIOA7ZARozpWQ+5cZjmU4IHrSYG5+Qw067SS7MqJgxPpwKDpsRMEgyQwQzThgFNiHLPsuWMTXe5d62OG2kWzxQGZRm6L9ob3U30z/zX5zD+brA7zDM3bnICAM9sDeCZLd0398+7DVe96uWwXT1KbnLbErg/3juey0uYYgjKSeFn8BQCdKTeRqwm2F5QUmqKSfCaNFuINXStXtYJJZ2Q/lqfIBwISr1dOGyxnMOopvs6DPWQCCmx6CMcSkIokhF9bqWODCEgtccQSkFjSFCCskHiRdI+gkKewOgNNAgKETWUVaGgCoX4XCi5C9zOAmANeiOgL/DNl3PYAIbhfAicoaqck5EDjE6NBgMZWRgRdDiPLcghzK2QDJZ04rWjUv8tkQb+sTeuTLYjfSIoUxhmcBDPuuAOXDD3RgBhQNlk+ye64fHGTR7CiRd8NN4+8MNnYquPJIKSMDqotxLXlRNtJVZ1ZKW0o9ekXzlZ5fKGEZaF0HFKVEpRQtJjWyknpScrzjQTX+vlnJD0GpQTAUzPRCJBDsN24YRyTj3jZAjRqBGYqKDUyE7jOQwYJ2OuoLClhnpSL/mI9STFHyrrzzG+ZCPOkxgrQFm1v8dKWHi/3L9PWZFADXkTiCWw8fknvuye+Uv2LcgA0LK6s1rG+cdMowXZkAbB5aTlIGsEufjhg6J/jJwLtCmbdEp5IihTFMtOeC6W7AecOfu/MGimtp9kLFDfZzNfvBj3b3YgdrryYZT3LRrHVSVMdvS3Enf7PSeelDQvMxScDenS6TYC17IYU18faOeYYutwWZpITIR9gFh9Fo43sJLvpqFSSzsAqlbgvrbbUMYh78mg2uMmkJGaYhJIS0NdAUAi8bkbQkExomUZIkCoVqbyYkj0j6BSVKhuwzHaBaQhbBJblmEAU8t7Yf8+kkPD3gLBMZ6NieaaiGi5pwRArt7F4wmMX4ypGXTCY7lXWTo+vC1k5FgICnGwcCh8iFshbsKUetYGiaBMJdQCqx5/+Ur8+QWfHcfFTB38/JlfxwO7P4XX/eQMZPc/UD0w+exbCeOIuiE2hLD1txKvlHY0xQbvSbfmN6myTrSU03F5NML2z88pnelTTox6TKIBlqoY+kYLcRWsFolC9IjQUPWDPRnxz4uKSJ2MhMc8WamrJPEzaibZMfOh+K8dSYjRhdUVFBZSUuKbc4DKj2J8SUhCeUgAGH0+ZzLUMFz4/WugpCSUfwQQGCBjOGdAnqD0+1mNL8tFolL7gMwbaAeM1Usq0JEcueigQUOMjpeBclFjUA71G04WHSURlCkCs/fuePS9DjPaPQDAhQu/Mb4LmmKYa9uY974/44Hl2wMAHrtpHrY978bxXVTCpMLatBIHUrKC2xq+xhlWOg1hW8UtFGyrFmKXoVt6c2yZwUnwmaivoXA2KifMBlwqQamXcfRsHU6kemo0hScowbTqqEk66opI3/11BaUiKDUywhjeLFsjNoG81N9rVBC+IvlumtptoLpO4qcb13wowtqVE0gLW6+g1E224t/TeTISQuCYlP+FH5BWYnKtRuOrklLCwviyjgAxPC7zBCV4VELpx7gcbCjG4HckV3+K5JqDwvAhbhRLPQYFBoFJo6IkgjKJYedsBpo2DQDw5C6z8J29P4R52YxxXtXURJtyfH77n8bbz5OjkW2zAPzkUvDy5eO3sIRJASeshljvOenVDLH9rcRx0B9n6Eplhi3YxrJOz3frNELXvGrCfthf8JuEDp1ofu1rGyZpxtAHQlIRlKGqSINAiFdQGEP8JCRSxdjXHmu+T2UwrT9GPPoSihglGsarJhTJiVdN6obYmoLCIqqUiJISg7qC4p8f5vWI7tMYAmd8CSx0+qAuxQBilKSIAdgZsNH4/BKA9fsgtCCXYmDYoCSLkhg5MQqxsMIxsC8YrA3UMAsAhRhYOF/JUyVvMqgoiaBMYvz+4h3wnuddCwDYIvsxtrSD47yijQef3+2zuOG6HfCRK/4BW12alJSEkdGVAk4EK9mhgM7WUfWkNSSEbbmbho6ozyQoJytcGwVbrChbOoHY5VE5CTN0uoVXUEob80xCGmycm1OYymfSo4a/pNEu7ImJKbyKUo5QtqmpKKqgSFNdieUcVOSDa16TUPIBAJHqMzyx6feprDcIECsQIlVHooKirCKoIfExT2ZAuk9AEks9YqnZqmx03fXXkyVwBsAKDGqdPUaNrpJ5JaUExFkwHMioksJMsF4pCUpHlTbrFRTS8LfMj+vIycHVCkRMBpYYAyB0omnWwfgwt8mgoiSCMglhd1qIlbtsjgN2/TNOmrWk9sjE/sc2lbAwn4GF+RKc/6wuNvu7/THt1nvh/vr4ml+YsNHBSWgnhk4m7vulO5xyEh4PyknpO3R6LvMzdYIJtkqHja3DtS6d0J0jIUbem2DJUUUOuLpeKR819YRr9/erKoAnFTK0/FMr5zRMsNGLIs3PlSYxCWRm1ODLMBSJRlBNagoOUc0kK9FEa1CpK2qW1SeFwcNxX9RISDQW11SZ+B6u9vmARuU7fRGTroOJGi3IRIKSlXSETh6dsWRhwSiMhWFBQQ49yWDAapqG/nsKptkCgpxkUqgoiaBMQtz3mnn42es/iNlmAKlteHxx9+GX4y//p8App78FA99OBCWhwlBTLHnlJI8m2BDGtrpW4rpy0nOaFhs6derKifNx9aszwgbzq+nVCEqjnEPNNmH2npR6iBr3+USAJnmJSog0S0VcvSZeD+TFr6PxutFuNw7Kh1dH9EP8RxNpOzBJQzkR47t0XFM5iYqKAcj65/nWY2E11bJXR8j7V2A06E3zV5QwCgNo+a9ZIOajuForsTEahQ9Au3FIkBlGj62fvSN66SRON7Y+I2UlO7ApYspsWxxyYnSFMUDsY/Yn7g/bRFAmEezOO2DRP2yFLQ56BJvb6eO9nARoO/ICAA+8kjF7h4Mw76o7kyclAYD+yg6/WB2CcmLiwL8hg/9qIWz1VuKgnPScRcG21kZcKSfOmUbYmriqnBM6dKJyUlJNxQBiUqzr84Zw31bWyQMaPpSKnFRlnkotqasnTWJSdfhUHTCBmBjHYUeuP/w5mMUMk3FCIIgSC0OAVe+IGAKxL/0YJSZBOTHei1KpRQK2Su7Cck3o9mFUE4hBMd5ev7u+Lwm03MOISko08wJwoiU5AWL4nnb0lCiMAVyGlilhRKL6ZoThoK3nDgQHQgGDQhi5/7cJmtgTjxNBmURYvtcW+NXpl2CGGRjvpSTUMGhauO8l/4kvPX9TfO7agxNBSVBTrDh0pURPJHbtdIYhJxpr3xrSShymEYdW4q7LoiG2cH4CcRjy50aIqo8nUAIVXj0pa0qIV1eaw/z6iIknLqaUIaQlmlz7lJK6WbYq/8hQlaT0J2up1BIlKFJTXNZfRhFSD4hxAhBBbKi1+McASKbzdyS0Agc1xaLq1hGKyokY7ahRYqOGVvWf1CYgx7yUmgHX6udEoy0h5qkA6o4N3T0cDLSiviLHRpdGNualZMyAKdFjPZ3n5DRpNhhowdrZA0FHHHIwDBgDoqucyIMEE0GZyCDCovMOhN1zKQBg583vn9D/mDZ2HDTtQVzy8TaW33wQtn3vTUg5KRsn+mfsrBSq+U4y9Z1wRU66wygnw7USB+WkV0uGLUurcfWhrFOY2K1DZa1Th31ZJxCJsmaAjc+pSEqToIgnKPXnSEOFaZZzKpVE31eq++Pz/HNK/XAlNBKfDw4ERUZFQSFPHMSQDvcrAwmhSByEDRrmWK+axNKN9UpHrdRTV1g48/uCBIYr4hJMtD6tXtUhEMT67+v/FkKmakH2rc0Q9RM5V01AZks+fsUTlDDlmB0MBIWxUUUJLcgDUqADTZjtgmFJ0PEExQlP2DJPIigTFGZgADR7FnZ/4R/x9Z2uqz2SPCcTFdtmM3DzPl/F4QMv8z+b+oeXJGwMCDN2Or6luPBJsT3YmPIZZuz0b0PKOv56UZutU4WvGSUnUivr1FuIHdVIQk31qCsjfWpIbBeuqyH15zlVIYbzlQSiYsqaUiJ1whGIjCcdIkp+RB8H/PuwJy2evKCWrPq04QkKjPGqhp6QhQAypIZVYb1tqer0MQIypLknwRBLFdEIOTJqqNXrWtpRXwiFEo8/0wazMRwQJybXjbPQElN4b2EAtjYFuTavx7GBIUEpao4txcCI/vtgQyg4gzMGPf/vLbQdhzk9jhwcxJetJiZJSQRlguKBt+yL1xz/Yxw7+9cAUrZJQsJkQFcKdKTESnZY6cs6y7mFHiyW8wAKybCCW+hIq6GeaDmnCmFbUyuxc0pOOCgnPRtPanUjbJV1Eko1FNUQSM0AG7NHvGLSMMRW9xtXXUIQyUggHVVJp7pfCUpdQZFYxkEgKDGaFbXHuHH/eoE09ISsaZR4yN8vRICl+JiQV1oMKaGxRhUTZzyJQVX68SZZ8vH5LF5dcVUpSPx9QE1JgSopbGSoF5gJcNoqTKIR+Mb4V2X63taTrNw4GKh52kDQpdyXfYCuV1BC9H3PlxkBYED0jzgghU+XTQQlYQ2wm89Bses26O61Eu/Y/B4kcjL5sGD6k/jLwc9Ea9HjKOvR+AlTGsF3wiIoUIWx9WBjeUcNsdr2yWJitwaLxtWz6OycNbYS9ysngmqeTlBOAslAjXx4MtLvM6nm5sjwZR7u858EElPr5mkoJf3ExN8eQkyYY6BbJCOxtMNVmWd9Eco7oh4UYoqkRYi0BCTGKymk7cbWl1h8aUUVFFZFxe9TJRv6vmSCGRlRQQr7xhlfUar9PSSoKVF9gjfT+vIP+7+pL/cwDJhF/w14BYXgxxqQKjaFGLSh/4YcmWiWDV6UAvrvMCeHnhivqjAM/AInGBJBmWB44oidceUFl2ArCwApeG0y4uMLfoxHPvs9vOSat2LhOYmgbAwILcUdceiIYAUbrJQMHcliK/EypwqKKikWK53G2ZdsYmmn4zL0Qmpsma25lbioyjrkfABbvxE2DP+T6uQZlBNTomGARYO0DE9KTCmgUhrlnHrpJhKSkitvSVBNmCuSUlNJKHTsyNDH4EahVGqtqiVeQYkbUN1nTaWoWAIZoyTFGLATaPia3kehXTkz2sGTVaZbyhAD28RqecjGIDgCrCow5FuPA2kx0FZlFgAZqhC3EKhnGWwIzhmYWjZKj7Xs3zN62XUMA21HDnN6OqRKSs4O1giMMAbIaTYKdJDgRPSiJIIyQWA33RSPvnpX/O05BXbJBybcP5SEtcegaWFH08KW+zyKR990EOZ/fzHcH+8d72UljCFiS7GI+k7QNMUG70nlNdFuHSUneQzcYu8hKL1iUldOhm8lpljWQWghDmqIg3bv+HJOM5MklHyG95mEFuC6emK8WVZLPN4/EpQQJ5GMRGIS1BHmaHYl5yqFJBCTqKR4pYSD98RpS24gKE9HSQkkxLGWd1zwofjjqyFdnzGx/RfGaEuvBciXZhpBbSGp15CGxpNRohZD3Xz3jwBMmiLrQrpsTTkhn5lCjvwfyD8nGnj9Z7L4WH0fwGec/ntgBpEqaRrkZ5EJRxUlthtDVDURF9uN2at7FgInDgwB08TzoiSCMkEg28zFf7z943jegLd9J0x6/PyZX0d3rwKH/O1NmJ0IypSG5p34tFhQNMV2fChbvWNnpWt7xSSLAwC79dk6tdJOWWsjHtJKXFaGWCoqb0lUQkKnTs1fEmPsS1RqSO123X9SKSjSR2QEptDyCxXsyVBVjhm+jFMr2zhu3uccIKIEgb2rtHZdwvOfLozxJRurDlYiZQeGomISFRajBEas0XVao0oK+/dgJR9gA1iCEaPmWqsG2RjU5r0pAGk4m/Wkg/3He6MsEWKYmyPNXzGinhUWKEmxqEo9fsaScwalJ1mF1cuSDXrIkBFHP0rVxaP/FnMu0SKHHlyVLgtVUhgMM8GaMBJBGWdQlmHRuc+BfdZS7JyvApAC2BISJhP6BwHGIYCcVy3F0uprJ/YtxU6vd1yGUqqUWAHgwiwd7zuJZZ2yVtbxyokpqNmd430ogXwM6dCpE46QcxLaiWNQWqWYVLclEpNAVPrLNvF66YYqJfWyjYiSD2F/P3u1wCsZzgHsCQrg22HWEUENsF418Z08gYxoa68BShcNtLBW92tmtfXYGJC/DN06kiOOEgheIMBojopXXxjqbYmExOewhPsBbznJwt9HBwwO+/vUh7SJ9cZZYlVRgCFelNKrI4YztLlQ34mxyMWBYdATi5xsFd4miMFthiYWRUkEZRxBeQtm9ky84Mjb8ckFNyGRk6kJ1yKYwUHwypXjvZSEUUbwnrAICpFojHUwYBhf1qlKO0NbioNyYitzrDfFMps4lVjYVGWdoJwIYkknqCNKQKr76gFsMSW2ro4EElMjJ1ElCf6SSEwCqQkbAyWDmLUleDhfSV1BYW6WbcIlCyTcBvQ6oM+tKypPF6T7jYySDwDVJRHIKyvC3hjCrG3IIpGYiFiAGCQWYpQQxLYcGP//7Ms+/q29v4SIIkHR2+LJSPW8OLuHCLGnp2a01Sf7vz+xtkGLf0hIPUqifpjSTzvOySfJ+jTZQqw3ZYd02Sph1o3qTIHRQyIo44hF5+yHw19+C87Y4idI3TpTE23KceK/fRffeM2zMHAKobxv0XgvKWEUobN2Cj9rB+iGtFhf0gmDAIOiEks7roVVLo+m2FWlpsX2nEbZF7UwNqlNJ44D//pm6kQlpGwqJ43yzTBJsI0Sj1NSYkqpSEzNZ0Kl79Jx7DcBFapyoHTDExJmSLzthhASiZ4UrshIwGgFHYoA4vStPfkRACDjM1B8ucfaSFjEWiDLoqpCWQZYA4nkJfOEy4Iy0dZkNlr+gdEuH6NKiYGf5SPeoyJQkia+G8jznEhiPD8z7EmMJUhL233C8MdglgWAgizEEgrfhtzjDJlRL0p9kGCOUkmKJ88seumEwCQagU+MiZS1lQjKOKKzXQ//Mf8WJHIytfGmTRdhr4EHcfHgq8d7KQmjDAbDQaL3pJp5osZYJzREQSmlmk5csm2oJuo90V/EGsamZZ5gkozBYDzMJjS0PbimrgwNYatvlaISyYnv1Amm10BKUFdMWMszVLrVExNWtWREYsIbKNSwTnoCaTFWzbAs6kHxzyNAI+etBZUlIFaVEyPajgwLeO2BYo698R1NFPepzvTRRwMJQfDFepULfVtoOQ7Pb/79PblhApEBC+tuFurbDBjkCYnedp6Q1OfzAD4LL7UZJyQkJEwNOGF0pES3kRgbZuy0ovekI1ns2im8z6Trh/91nLYUB+WkW2Qq2TuDsvRlntJ364SpxMPABM+J88FqZU1BKTz5KBGD1sKJMc7X8eoJOagB1gV/iUQDLJXB3ArtxnEMKsqooMA5SFlWBtdQzimKaIBVgrKeJZvRBrumcFOUWg7KMiUn1kLyTJWUPNf7PMkia0GWIcYAA3o6pZK8htLMFhE/g0eEtJnICZiqtF9fVQKIwNkIXpSSIGLAoQ0IDM58l5cQshpBUcM1+RKQieUdDqUd0awUhio1hfehZLATppMnEZRxgN1lRzz2vC2x0/Z/Ge+lJGwgbGFX4IG/n4M5C5+Dge/cMrEO0AnrDCeMEhrK5mrek+A5cSDvQakC2VQpsf6EoL9oJZxA2NRMsRSj7EPXThz8F/JO2KslaP4CDx6T/lj7KhE2tAzD+0QAkqqsQyXHsk5/OSf4TYIBNnbjOFZi4hxQlpXRtUZMGiWcif5vX1jNsYFseTUlmGophL1JlQBLAMSJBrr5kDVyop0+pe/8IfEBceJbleE7n7zC0vd31Mf9813wppBv8wk9yIj/hpqXBoDzlwqOaklFVLimpDgROJpYf5tEUMYBjxw+Fze/42PIaeLU+hLGFnu0puE3b74M/3T/i/C361qQbne8l5SwHgi5J/3ek5B50okdO0MHAXadBrD12KLH3nMy3HRiF0yxJqaKktOpxKaoFJPhUmHVjyJRTYl+kxGMsPqYNJQT0ysrn0noyqm3CTsGylK7bMpSSUpRxM6cuvF1UqHPsyJkQLlXSwA104qAjAG5DNLWl5Hz5R6rPhQi3ZdiCcb47h2LyiwblBMXyEj1GFhLRKEFGQA4814UJojUslFkRGFNX+fTiquyjpZ6qvk8modSkBLuUocCTQgVZdRXUJYl3vnOd2LhwoWYNm0adthhB7z3ve8F1/rYRQTnnXce5s+fj2nTpuHQQw/F3XffPdpLmXDItt8Wf/7ggdjkFQ8lcrIRwpJBZtIAweEw2Y4b7CPCg/ekBx9pD1vr3Ml8G2fwn5joPelxNjTC3qsnYToxl8arJ2gaY4ua18QRTC2cLUSn13NPTF8LsZZxREs/3hSrl+zLOwxTep9J6cs3RQnyG4oS6BWQQjcUPUjPb2UJqXlNJj3Eqz9F6b9jAekV+v1LVYzIl7eU4DmvQun+a7Rmx61JKOPfrN875HTY4LqAhaJCF8clxK4yanihOHaa+ZZj0TKPm0B/t1FXUC666CJcccUVuPrqq7HHHnvg1ltvxete9zrMnj0bZ5xxBgDg4osvxiWXXILPfOYz2GWXXfC+970Phx9+OO655x7MnDlztJc0IUBZht42c/DdYz6EXfLUTpyQUMdkOm44YTgRfzCvck/ivJ1a507BYe4OoWSLomaKDR6BhjRf79hx1IyxL2lIW3G8LrWhfuGxQFRCRH0gJ2V10jTRCBtm5eiJNWzgYXwmvl04lnaY9cTt3IYzu25I1BUVMiDmUGxRA6xjbQ8unRpn/RRmjdb3U4658gex95+I8UMF2ZeMWKs2jcunwRXqRlnnyz0lGyXPlIGpjJ4URyELxeh8QhFwcPROAIw6Qbnppptw1FFH4cgjjwQAbL/99vjiF7+IW2+9FYD+Crr00ktx7rnn4uijjwYAXH311Zg7dy6uueYanHrqqaO9pHGHmT4d93ziGfi73X+HBTYf7+UkJEw4TKbjBvv0zZ4ICn9wD4pJJ5phlZh0JYtx9pWCYmPHTuGspsXGlmKjNg0OvhM/X8f5lmKp/boeZiqxKYMZthbCViv7RLWk8L/ui74OnZJVBXCiSglzvJReEVuFpVB/hl5O4nLOOkLKAuKMRt9DW4lBflggaekFROoNMmps1twT0ZbiQCoNaSAeEWD03xSYwlupB2WNi/H+EzYgAgrfdpz5f1uGRNuMSfzQQBmioljJhuShOAgYMiGajUe9xPP85z8fP/7xj/GHP/wBAPCb3/wGP//5z/HSl74UAHDfffdh8eLFOOKII+Jr2u02DjnkENx4443Dvme328WyZcsa22QCtVr4p71/hU9s/UsMmtZ4LychYcJhLI4bwNgcO9hHbDGgLZuob1UoliootfJOfVqx9wVITTmJ6km9ndgbJUcqBzRKA32zc+Im8KFj1WbqZQdXlXNCiQKlA/kN7JWSUss7UpSRpEypcs7aIJR8XFCRXHXdt1cHI3Ej+p8lmpmHbSuubWu3Dm+QRej29p08Xp1TE7aWelw0zapBth/cdx9PoL/lqCsob3vb27B06VLsuuuusNbCOYf3v//9OP744wEAixcvBgDMnTu38bq5c+di0aLhQ6wuvPBCnH/++aO91ISEhAmCsThuAKN/7HDCfigg+5q9qSkmqpo0Sjw+MbbnE2PDpOLCWT8MkGIgGzsDLvvSYvugKglVxtei77KslJMqhE0aj8VpxI5hek5n5hROT6z+ZEvOKyfOqc+kVGIi/nJSdOOMFUQgRQ+hN1nzUrLKRGuqqcjkc1U0OZZhrJZSlDiuu8cEgG8595H34of8geFEu7ucJ8GWlKwY2KiccJ1IezINwHeaaWDbREqVHXUF5ctf/jI+//nP45prrsFtt92Gq6++Gh/60Idw9dVXN55H1PyPT7xENhzOOeccLF26NG4PPvjgaC977PCcvfD43++Khe3HxnslCRMAu05/FEuP3ge0zx7jvZQJhbE4bgBjc+xQCdw31tQVlFrWRHWdqoN/yKfwhthgko0zXYCGcqIx8xQ7eIb84ub+rUZO3NDHIEDTrClKTpyLuSZwXjXxBlDhqlMnthJvzOSkBuF64JxEX04MsBMZ2gIOVLUbqV2v3T/s8+L4gvq/A4plnqi8rQU4/hv1bccTMaHNY9QVlLe+9a14+9vfjuOOOw4AsNdee2HRokW48MIL8drXvhZbbbUVAP1FNG/evPi6JUuWDPl1FNBut9Fut0d7qRsE973F4I6DP5pKOwkAgLfN+T3O/OCd2OPb/4Jd3jjeq5k4GIvjBjD6xw5tL2b0RPzEYvWfhBbO0BHRTI7V8k6Ps2pKcZhU3PCeUIwyB2s7cYyzj4SkyswIZR3j6upIuE98UJs0Iuyppp5QyVE5oW7RbB32xATOqQG2KCFlkYhJHewgBSBROdFLKi2ESONZxU90DugnIeG+ftISDc4aOWsA7ewCgAyQHJo3QxTLPHWiEkuHgYTEy6YmEZNkfefPRMOoKygrV66EMc23tdbGdsGFCxdiq622wnXXXRcf7/V6uOGGG3DQQQeN9nLGHcZyIicJEZaM/nsw6UBfx2Q4buhgQHWgFII+QhLaikMIFjXVkyGeAPIDAWv+k2iMHebD/cmq0Z5a8zM0c1CqKcShpBOCwyrfiVdPAikJl0XVPhtaaqXX27h8JusC0X0nI+0bfz8FiUyafzvyKktdEauTz/p9GyNGXUF52ctehve///3Ydtttsccee+D222/HJZdcgpNPPhmASrRnnnkmLrjgAuy8887YeeedccEFF2BwcBAnnHDCaC9nfEHqyk5ISFg9JsNxo4RDIdq9oyRF6/jDTSuO5R2fGNs/J8Wx/vINxCQmxobU2D7QMCetRvkmGmWrvBOdp+NfExJiyypore45icpJUUTjp/R6+iu9LDfI/p2UEIE4BplQ+jJBztCkWa0D6t8n/CgZwSjbIDB+Xo+GuRGQbZwMZdQJysc+9jG8613vwmmnnYYlS5Zg/vz5OPXUU/Hud787Pufss8/GqlWrcNppp+GJJ57AAQccgB/+8IdTKgPlqWOeC37dY3jfjt8c76UkJEx4TIbjhhOJ/pNeraTTi9tQolJwKPGYhuQO9HkH6qUdp+2ppqhUkyFhXqGFOKbC1tqJuQpj65+t0yjtlHXPSanKSQgkc1zNzUlYM7znREJJp0ZSokfFDxCMSbK+ZGec/nm1FVknIIOU62zsIBlRm5q4WLZsGWbPno1DcRQympi5In855yDc/aZPjPcyEiYoFn77FOzyhlvGexnrhVIK/BTfxNKlSzFr1qzxXs5aIRw7nvjDDpg1c93OACu5h66UeJIZK8ViObewjAewUtp40g2iwzmW+stl5QBWcQurXI4VZQs9l2FF2ULhLDplhl5p0SszFIXG23PPqoLSM0pKerXE2JBl0quyT0Leie35Dp2eV00KRL+J6amKYnqVemICOSnUCEu9AiidqiWhrNPpJr/JOoCyTLdp04AsAw20gTyDtHNIK4PkFtzOIBnBtS3YErhF4JzAluBaBLEA5wBb+McAzvQ+sYBrC8QA3BJIJpBcgJwBK7BtB2MYrZZDbh3aeYmBrETblpiedzFgS8zMO5hmC8ywXcy0HQyaHmbbFRgwBebYpzBABTYxXcw0DoNEmG0GxiztfNlyxqa73LtWx400iychISFhLcBgOGg4W+jcYVQdOw5miHqik4qzauYOG5274/0nwsO3E9dBtUnFDfWkpqaYvtKOKUOJp2+2TkiILXxEu28l1uj2MvlNngaERSc7a8Je1cXjWH0+xKCMARiQFRD5v6mBDhdk6DDBhCFIIlJCQkLCWiCUd4DQXmwaAW0cMyUoXnLdIBu2UO6JwWyeD4zACeoBXlTzL4TW4SGtx2GAoKvKDTEp1kkMFIOPrBfmaPZEKus8fXBV2lkdgjm2/76EoUgKSkJCQsIa4ITBooPUQktxyDsBankoXkkpfShWICfi56KE4YDO+Q6e/onFq0H0ojQ8KGEY4NAunWiKDcPrQlknDPxj9qZYrrUSJ0NswsRBIiijjGy7bfDAMdugfeDj472UhISEUYC2F4ufU4JaGqeWeDhMi/XtxXXUVRSg3xhbKShxYnEYCuiq7JN6TkYViS4NNaXKzUDVUhyeE9qKpamehPh6cRxj6xPWHWQICBv1bQaaVUKklbwwxcCQ79SheF/CUCSCMspYuetc/PjNH8SWNk0sTkiYCtBwNodCBIXUpxdrQFtPrCcpFNuLgSocK75P3+3YvSO1icUhnK1Eo3tn2CRSf71Khm1eh58BE8s6pY+wdzVyEuLskyl2vaDR9kY3azw58STF+uuWIJbiFGMx4U/vyUrCECSCkpCQkLAahHg2L06gF+aYxMTYLJZ86gbZ/vyTp/HBMMEgWy/tcO02Y3glJQwHdLUBgMFjUh/8F+PrEzl52iADGNNQUaRPTQlERIxXVUwfUTHawaMEBlFt2diRTLIJCQkJa0AwyLoYvFZ17tS7d+K8HTEx4r4//2Rt4WfADUmP7Sci1W1/naWpujCiURYsaooNU4jDZcL6IQwHDMmckZiE0k5VypH6dVPfKsISyz4bOUlJCkpCQkLCauBEvEFWyzbBfxISZEOkfTV7xwe0sZKU0GLcKy04TC72SbJri+BHMa4eZd+XKBtC2mKirE+MrYeFRXJSIylJPVkvkPUKCpk+1UTVEgTVJJRyasSkfhuBqFjNQBHrr9uN9++TCEpCwgbEX8qn8N/L90RrSfpPb7JBqypDh641x9ZXs3bCJhIGuuljIrX8k+BDWRP649Dr7cY19aS6Lg31JG5c5XTIWrTEJqwBXjGh4DkxNZJiqsfrPpN6QmxUWFBTTIJqQoAYTZ6FqW3roKyYNfQv2wk+5CcdJRMSNiD+ffHh+Mur5mCHv/12TV2lCRMEIaCtkXmCuinW+CGAJpZ/wgwexyYOBtThgAbOaRYKlwQph5+9s0bU5rYQh5k7tWnFLM15O/5SmL1xVss7CesBIpC1QJ4D1oKMKilia0bZ6Edplmykj4jUt2a3DyAZwLmomtJiT1IEMAAZqZqBSIZsAdZft8RD7gOUqJgJSFYSQRklmIEB/O3V++DxvQVtStaehOGxyuVwSx6DdLvjvZSEUUY9oA2o1JZwW1UU+PZiVApKmL9TK9uE7p56SNuIGO7xPvWEYolHyYmIaAJqwtNH6NoBKvWkZo4VY6L/RLzCUpV5gldlde/fvNkgNYGcGAEZbpAS4zddVnXd+p9EhhimTlT6/gHZCWR8SQRllGA23QSvO+davHGThwBMG+/lJCQkjBGqxFg1yAJVyaeflFSvaeagIFRXQjdO4efsFORTYIc3xdbLOyHnBKiuV+Udv9WTY50DWMlJSowdJRhSDwoFgqLqiVjvO7GmZn7VmTtiqKruPR0uYATIGGQZZAXGCKxlWBJYr6gYEmSG/3977x9tWVWdiX5zrr33OXUvRUlRpIqrYNBAaKVe2anXGumh4C9oRgPJMBGN6SGdwRutrWG8ivASjckAR6fVYXcw6Uaa92x74I/hwCQdiBo7ClFBQohQQBqQ8COgUEKFaIr6Rd1z9l5rvj/mWmuvfe4tqCrurXPOvesbY3N+7HNv7bvvZe1vf/Ob3/QExYHhiQsEBgIDpxu1pMWQTFzXzKQdz/SC+QXrfRkZGRkvhDDldlEjbNK1EySZNh9Fn1Ds5PGv/XMJCopznc9nHAF8eYeMAcI2GtQWW4/h1ROMtBZTNMWm3TyHHNzmxRhmJSmFcTDsUBoLQw6F30pyKNiCSWC8etISF31tInEBmAg8ISpKJigZGRkZk4REQYnzdbya0hKVrlE2qvRRUUFrjA1wgjDMLpd3XiTIkw9j2vbikfyTViVp808Wby9G16fS+Xde4DCS0o5hr4JE9aQlI0ExKckmCooSlvAcAAzRRJV4MkHJyMjImER0jLAAN5JMNJZEWUlbi9OWYiStxWEgYKKgZBwZ2IDKAlRVoKIAFQVQFJBClRQJibGGNT02lnRaFaXbWky+nRjtYwGIkSMqAXFCUkpyKNmiYIceNyjJoqSm3WBRkqorZRB7JogWTM6RZGSsYAykxp8/18d9z8yN+1AypggdhWQRP0ogMLHUI9LxoWARpUTbizNJOSIQgdiXd4i0W8eYbrx9bDXuZp90O3W6zKPTYhyUlvCI8Pzgqlf0znqfSfScRN+J65R3jC/3mKS8w1CDrKHJUVCySTYj4yjgngHjqvf+KuYe2IEmd/BkvAi05Z52crGqK91Jxq051htkrYvpsRlHBqoqJSe9HlAUoMIAXjlBWWjnTuE3b5B1hSoqrlgYbz/abhzLQixtYFshkEo0E8VvOvpHmWobXrs4gYmeE09EqhH1pPQ+lYoIJTEKGJgJ6UTNBCUj4yhgXkr0du5Ds/Mfxn0oGSsItJgQkgPYlh5+GCAZoyUdw9q9UxS+rONVkzgMkGIyrHpTEFURWaCmJI8e8bknHUKjgW3d/JO0rZg6nTv6B5J266SIXhQIGAQGTww5ATJBycjIyMjIeF6Ebh2qfChbWSo5KQtIqYRFShO9J2IYzgQl5eDdO85QjLQ/JG+qEVDhwIUDGwdjvDrCoc3YtaUdX8qJ6kkgKYlBVl9PZnkHyAQlIyMjY7oQ774JYkSD34w3K8SOEY6dJZ0QsTAzBrnMc8gg7daJLcXeFIvCqDG28MSk8JknwSSbEpIFJMWrJCOvD4esEIX2YgvDbe5Jwa7t4hnxn5RkW/9JaC0mgSFtL540TI6Wk5GxQlGLxVBMlt0zDhuLdXGkIV/pZFyEDpFYXugOr4vtsExtAmrG82O0tMM+0t6w79oJKolpvScpUUnIiTuYkpIMCdTPygu3F0PzT8iHs5XG+jZjh4IsGBLzTwp2GA1oC/knIQMlKCiT1MEDZAUlI2NZUYvF6V/+AE68XbD2ye+P+3AypgmRdAAQwBUEhmiUCZMaKWPQl14IQ9uqzoWxnZkwIbuD6PAmKa9KhDk7XjkhX85RU2zRKieliYZYV2hbsSuoLe0UvozjSUhoJU631quiW7tP4CoB/GcQo+0Xn7eTqic906BnQluxRdlpMbaoyKKCRQXnyclktRcHZIKSkbGMcHA4YTsw+yd/k4cDZhwaRsyTo62nMvJcHAEkUVGhtq2jfeREOeHJuxBNHIgXkBOwaVuKubuFzBPnY+5bhYQ6v7P290ZtWcf/mmTkdy4GQFBUwha6djwxIXhjLLCgtTi8DuWckppY3mkVFC3vGFL/yaQkyAZkgpKRkZExSSDAFQIOQwcLnyzL0CuYV07IwXtOBG1LR3uhS9NNyXnlhFs/hfhU2YwERG0rsTfEgo0qJ6GV2LD3n2g5x5WtguIK0tdeOdHWYlW/onoSCAuPqCmF/l4XHhPUHGsEXHhDrPHzd1i3wj92M1BcVFB6XEcFpU+1V1AcSjiUAErQRLUXBxz20dx666244IILMDc3ByLCjTfe2NkvIrjyyisxNzeHNWvW4Oyzz8YDDzzQ+cxgMMCll16KDRs2YHZ2FhdeeCF27Njxon6QcUMGA/yXB9+EK/7x1aglG9AygP+571hc/INzsebHzbgPZexYqeuGGdHFXmhkfZTlk+wLhNyLcEcd7rZDSym6nw1Iw77C8/geJ6+zD+XQwCbO1yHfPtxmnXhTrFdPJG7BZ7LQcxKVlJFZO50k2U72SWKWjX8PYXYBEGYbxBIP0HmM83WSR+ONsgCiktJVT0SVE6KJIyfAERCU/fv3Y8uWLbj66qsX3f/JT34SV111Fa6++mrceeed2LRpE972trdh79698TPbtm3DDTfcgOuvvx633XYb9u3bh/PPPx92igOE7I9/gpf9yqP4zkf+JXa5+XEfTsYE4De//m48++b9qL65fdyHMnaspHWDF8mUMH7B1/fbXIr2a6TlFpQEbflNSg3jcj3RcC4v7cvIhQxYWBZA+hxQ1YR8Z4+htgQRYtlLfeSqbMtAqxlEbXx9r6chbFWpSkpVAb0K0q8gVQlUJaQqIaUBQhhboWUdZ9pANt0AV/rnpfejlKSPRVdJSbt5nP87kMot8J5w6NwpLApjURUWpbGo2KIgh8pYlBzC17r+E4bzZR6HCtarJ4IeMUqYMf8SFsdhl3jOO+88nHfeeYvuExH8wR/8AT7ykY/g7W9/OwDgc5/7HDZu3IgvfelLeO9734vdu3fjs5/9LL7whS/grW99KwDgi1/8Ik466STcfPPNOPfcc1/EjzNeyGAArrPTIENBVv8mMqZ73VDzoI1tmfpe+/95SlS4ky0h8c423uGGFFASULhApcP+DgeLeFX0jl1LOnqnDw0UE1HvhDOAdaCy0K5kYzRddjWWepIOHbAv7RApkQshbEkrMUJKLDNQaElHklk7WFQt0VPe6eBJyzrxc4lSkoIFMD49lpXUMjut+nA7c0fLPDZOMA7Ti9PunYpsq6KQi+3Fk5h/ErCkms7jjz+OnTt34pxzzonv9Xo9nHXWWbj99tsBANu3b0dd153PzM3N4YwzzoifGcVgMMCePXs6W0ZGxsrAcq0bwNKtHemE11DWGQ27MiPyepTRw5TZEKLFAmIHMgIyTi9Ah4ukNKB5KGlZoS07aLIpt8bOwsSSBXklhcxk3j0vK3yXDpVFZ/AfqlLPT1m2IWyhWyduCTkplATG1uLEGOtM4jtJAtk6XhTfrRPLeqPwChsbibknzBK9J8H0WrBDxV49Ybto9w4nZtnKqycllIBPYgcPsMQEZefOnQCAjRs3dt7fuHFj3Ldz505UVYXjjjvuoJ8Zxcc//nGsW7cubieddNJSHnZGxpLiin98Nf75770fr/iTA+M+lKnAcq0bwNKsHaFGr0q8RGISuiHSC4GOtG8nyFb+Ue9y/YXFOCUph3EMi5oqR3wNUiR36UV7sUTM6fBGz6qME3hRlu1E3gm9i15SsPpL2JdywiP1Kr9peSeSk7KAVEUkJ67STSrW58EUa3z5xiCSkwVqSYG21OPn7MRyXnjtS31StNH20XPi/3YKdihMmxxbsGafBAWl4gY9btBPjLGVby0ObcYludi9U5KZuO6dgGWhTTTyhy4iC94bxfN95sMf/jB2794dtyeffHLJjjUjY6lQi8XD9X589Ydn4Keu/RvQX//tuA9pqrDU6wawtGuHLuiShFv5iwTC68SoCEFBSZpnuOONGRaH+Y93zJSUmCqTsC8iT2K8B4VD7Lr3oiQmT8SMD1a1ICSlrnC0bcMlqFTFpEPYPInTtuFQwmm7dILnxCXKCXyXTvr76LSBMy0glC3ZTMzRRomJmEBO/NhqQqKeuFguLCI58QSYJKonJvWgxC2QaP1bDd07jMk0yAJL3Ga8adMmAHq3c+KJJ8b3n3nmmXh3tGnTJgyHQ+zatatzN/TMM8/gzDPPXPT79no99Hq9pTzUjIwlx18emMHHf+PfY9PD/wTrptfwfbSxXOsGsDRrB0NNhCUsLAQVHGp/V1pSA0uEkiwsE3qsHVsDLlAIoxCGYQfnDv8OVRhAIRAhwPpcDECD2oxeu8QADgQyqu64gsB+VqAIaz6KmJh9QlC7CzkXA9vIWn1urRIXa1eOLyUEroWQuqrypMw/Gn9uQjIskWaeELUhbEUawtYaYkMonivbVuL4mITmpUmxsbxTSKu2FJ6c9JKJxUY3Lpweup+7Y0hQeBWl9IbYyjSo2KLiBpVX8nqejPSpRp9r9Hmoz6lGnyz65FD66cWTSk6AJVZQTjnlFGzatAk33XRTfG84HOKWW26Ji8jWrVtRlmXnM08//TTuv//+511opgXl3hq/+/RbceP+Y8Z9KBlHCVYcrn32pfjEY+fhmLuegH3o0XEf0lRhGtaNEGSlM0ukVVAgHVUleFIMHeLFvdM27L0ISfdOGubVCfuiJOgrvVMf3VITJ3k/StgMg5jbzh7vSYlqSmhNnjbEGUTt/Jzot/FKSSQnIRE2lL8Ko4pJaoBliiFsmmfS7dZJlZM0NTYqJcnvsRvWlignacS9/zoy4nP1XMdcnYaxBQVFM080xr7kVEFpOhOLg3piCChpcr0nAYetoOzbtw+PPtouwI8//jjuvfderF+/HieffDK2bduGj33sYzj11FNx6qmn4mMf+xhmZmbw7ne/GwCwbt06XHLJJbjssstw/PHHY/369bj88suxefPm6M6fZtAd9+PJt87iw7/xHvzie68Z9+FkHAXskwE+/9ELsO7rD6BJ2mIzWkzzulGSgRVCCYKFaHtmUtOvxeh8k9h+HHIn2otJeJ8W2cCtrC/k4Jj0+lqHTBMAktyF+5tsVU70e4tv1AmKihIfBpEDCSs5CfEr4nuRnNNI/KCmOKclkFo7e8haiHVeTZnwULfQkROC6PyAPzArGSFPyEK+SVn4rpw2GVZCS3acpdMaYVUVSfwmI8QkVVCCx0QWCWcbnbvjihHlhAEqHciocmIKq76TQof8VYVt1RNjo/ekxxrG1uMGPWr8cw1k63O9QD3pE6GEQY8mO6v1sI/urrvuwpve9Kb4+oMf/CAA4OKLL8Z1112H3/zN38SBAwfw/ve/H7t27cLrXvc6fPOb38TatWvj13zqU59CURS46KKLcODAAbzlLW/BddddB7MSaqDOwu7ZAx6O+0AyjiaKeQeXyclBsRLWDSaK3Txtq2YSeiWhrbNN9NSv8y3HaPNQKARusfpotC7jCUkqWvjQLoiSFs81EJJkJZIXgoNoe7H4O3IL7eKxvhxB+hriLZHCvsRDQCEg5/SIiUDWQJpGu4waApy0RAUYP1mJMf6elJAnImFAYlCGWJWUMIMIRdGWc3xXk3Y5KREBUbcjJzyPnpO0W6o1xMYZOhzKOCMm2Y6i4kllkmETlRPf2RWUk2CoTk3W0XfivSfBlM3egxLUEwOXeE9ac2xJ6j0pafKvtyQy7r+0w8eePXuwbt06nI1fQEHluA9nUez48Jl44NKsoKwG7HYHcMEH/m+s+bPvjftQjioaqfEd/Bl2796NY489dtyHc0gIa8euh1+BY9cenry9z81jXiz+yQHzYvCs62Ov62PeVfiJPQYDV2K3XYN5V2JP08cBW+GALbGv7mHoDPYNe6gdY1AXqK1B0xjYhuEcw9WsRGLIIEugIYFrAjkfc+9UUSELcANwDVADcC1g659bgBsBNwBZgRmKPg4cyAp4aEFO30Pj1IdSW81FqRvAOcA6oGkgzgFW90mj+/RRAHHj9aiM+kpSw69/D15BoWAIjvOIfGhdVFDYG129z4TgjcTQuTrBdFwkJR1DMWgtLemEco4rE+UkqClB/Sql/VzhvSiVV9AqVU64dDHOviiUePTKRgPZfChbaSxmiiEqtpgtBuhxg2OLefS5xjFmHjM8xCwPsJYPoE81XmKeQ58s1lKDWSbMkMEaqsbiP9mz1+G40x47pHVjsvWdKcbL/nIvNtv34w3vuBvXvPSOcR9OxjLhtfe8A/PfOgEn/e8fIQfar3wYUFLTdyhhUZNDRQ0c0SJ5KBqa5UAojRqnrXEQ8Rc6RyAJ3RrhTlyACnBQdSNcQsIdd/SjGEAcwZFoyYf8h0Rfi9HXYvQ2XUqG2BCVDv8FaIcHioAanYBMnqig8AFv1vn5PaIlIWv9c09SxEGS50tCXljv8IlbtSQqIaF8E58Hz0mroASfjebEpO+rATYlKJ2STmzV5gUt3B0jLLemV1CrpCxKTrwRNnTvOANND45dO0nHDqBZJ0ZLOYYFlWnb1ktj0Tc1KrZa0vFb2rFTedVkwdwdgibH0uTN3VkMmaAsF753H+a+B3zr/9wMZIKy4jCQGv9kB9h3+wk46fdvz+RkFUANhS4ShjRVVuebeHIigpIshlSgDGQlTJaNYW0OLAQibmfzUDCXkF60/OocSjSjBksEEkIUyUHXMAvAiJo8/ZETC9B48uC03YdIICjaEo8jVR6YlJjAX1wtK5lyouqK+EdA1Re/T0eRvQiS4ss3ndIN0LZHAx1fSfxMUEoSMiJeNYnvxTEAJpZ1xLDvxlEFxRU0En43Yn7tlHLQfm7UX2ISZSW0EwdjbDFCTuLhad5JaCk2LChM13dScaOvWf0nC9uJbWwn1nJPg5IcKnKofJmymNBo+1FkgpKRcQR4x6MXYN9/eClOeXRHJierBIYIJVjr+IlR1oH9o5oTrbDu4wZWCBXrRXzoDBiCmg2cEJzTu2QAYNELpxMCGgB2YfeMKwRMBIifcOyv2+QAOFVvVLwgiAica7NlyBLIipZ4jIAb/5oIZB3IEGAZKBhkRUs8DXtVxSgxsYGEODXU+nJPICYkLvGq6OeOCKEluvCXp6CQeCOs7jOt3ySQkpSQdEo6/pEC0aOuahI6c4qkpMMtOVnMCOvKxGtiusSls4UW4tQUW3j1JJCT0qtaRSAmPi2WlJyU7NAzTSzrFOSwxtRYY2oUbLHGaCDbjBl4I6xvKY6txQ36ZNGbMvUEyARl2THcOYNrn30pfnntw9hgZsd9OBkvErvsc/jjfT+D+x48Gafd/L1MTlYRtCnTd+iElk1yYHHx0UA0VVYKlGLRkEHJFk4IBTs4oaikNMTtkGH9B+LdNBxFxSQaKJ03WJqWvPioE71Ld2hbkgleOfEKCgli/QBe3Agloch0RIkROcCSftSJz03xJSgRwDHIOa+acFvq8WSFmGLp50gsjjF4z+eRdEo7YV8wwAZVJDXIhrIOURL1H9QTaInHBPNr2zYsBl5dSUkJtUoItapIp4MnJSiJwtUJYgv7wnupcsJh1k5XPUlbik1sJxb0TKOpsbxQMWGSNjEWyfsADLREOemtxSkyQVlmnP47D+IrV/88/ulPjsFvb3ho3IeT8SLxpb0/iz9/x5n4Zz96CDmKbXWBfedDn7RjpiSHPmo4UgUFDMxLDTigpgaWGU4IA1fAsraEwgC1D/GTwkKEQCRwjuCslmBElFBI6UsrxGqCFe9RIa+k+KYa9apAL8ZOdFIQ6fcgR57EaEuyGmoJwgIOCooIyLCSECeqqDjpKCgiPv3NqygU1BTxkf3JawR/CroNSYeLGL8f1BAaISRpOSdMb45ZL/CqCTx5odjFFFJ2QzlHklyTroKCDknpdOqULTlxZVvyCSU4NdmmhlhVUVzPtUS0UDWLCgfyE4rDpGIt5ziUvrRTeUKyxtSouMExZhCVk7Vm3j8e0HA2rjHLg6St2KJPQJ8YPSonNtZ+MWSCssywe/aArcV//+7ZuOOMU/ClV96IY7g/7sPKOEw854b4N4/9a9zz4E/jnz35EGweWLnqYIjhxMKAdNAaORiR2MZphWMeSpzV41s/o4JiGQU5WO89WbD5GFgxACz83b7KHWLRiiDU3dp2Y732iahvQqsagtDJ7KClIIrPleQwoEzHQhWLaHhNHh1ibooEJSOQFOa2rONJz4vCYl6S2FqcqCSBjFCrlGiZxysoByEmOg4AkZykXpJWJVnoOxlVTjqKSkJQhFtDrHpOEE2yIPHtxPpLCcMjF2sp1oGAwWytKkrwNS0WYx/yTuL7YSigT42d5Fj7xZAJylGA278fp37gbzD/L1+Dp75kcdr0/H1kePzYDbH7ypNx2rfuzMrJKoaqKKyBbSLok4UFoU81wEBfSlVQ2MAKwzJj4ArAQB8BNMJwIDSOYVggohcpQEtA2ltMKsxbtEqKMKjxUfbBTOvLP+LXFBHt6olig89IIQclN57kCJFXSQAIaXePFZBBbEUmVr8LGacVIucgDt4oq9JNICLiXDTdIvhTXiQWVUkCSQlqiUlVlST5Ne5Dm7obyAv8OQvlHK+Y2PJgCkpLXmKpJx3yF2Lti7acE+BCrH3qOTGiYWysJMV45SQMAuyEsbFvLfbm2DIJZOv7MLYZHqCkBrP+cWEoG9AnM1Xek4BMUI4mpi9yJgPaSrzvjhNwyt8/mT0nqxyGdC6PhcQ71ErUMKvm2HY2j3pRjM7ncYhKSuj2KY2F9eFsTUhzFdKeIPGkBNCgtBG0bcb+deGfi3o41LohgesoQQH5jiF9pOBzEa2gKJERzV1hUq+qg6oQ4gmSdYAgtihHwiIMWInkZUlWumBsNQk54XRfIDHwCgqhnX+T+E28UgLqkozUZ5KSkJBx0npKWiMsFpCWlqiMelEARGISyUko67CATVc5CS3FRVLW6Xu/iU4oTjp4EvVEpxanxGT6SzsBmaAcTQjwrKswkBq9CQ2Yy9DZOgdEo4AtBIObT8BJn8qtxBmKAgYlBBU51KKlnkps9KI44gUtn45ISzvei9IQe6IiEJ8Oaj1BAQCBekLC64jE7CqcthD7RHxfZtDP6oWXtZ7jjbE+cZYSVcV5T67134g8ISFWwuMC8UAkKxQyVJwoYYlpuBLJy4vGAoKi/z6g6kcaqrZo+ca/Rodk0EieTDcRNgSoxVwT7pKObhknJSftVGJXSGtGHm0lDkmxnpwURTsEsDRa0imTMDYlJzamxS7eUjxKThpPTgR9YpS+c2cakQnKUUTx4A/w/1z6ATx5LuGxX/p/x304GQfBtbtfji/+h3+NYl7vA192bw5hy2gRVJQeLIZk0YfF/EipZ15K1GTQ5xoDKWDBUUGpTLdIyFbbjtmRbxFWs6sEPwp7D0oVMkbU6EqEGG8fxI6glmippn0zllwcQIbAFupZafTaSRagAtqO7ARkvfclEhSogdYlhMV/P0iy3zqQLM3FMCogSdkmKCiRoITY+aCipGqJST+H1nNyEIISA9cK/XeCktJ+r0QdiV+z+FTi+DMcpJU4zNhJlZPKWBjfUhxKOjPFECU5zBYD30o8RI8azJgBepy0FCfkZIYbzJCgT4QZLqeqa2cUmaAcRdhnd6P/te9hw4bX43+eeyxe03sKryzz1ONJQS0W3z7Qxx/v2IqXfOU+uP37ASCTk4wFYGjgVUUUVRQLG0Pbwt1tTUbLP8QoyMIyoXA6ELAQB+ufG1YNhHyImw29xyG8DSpUEDTVFOEmXQBBq6QAaFUDf01XLqGdOwR/lw8lMGK8+AH/JqQ1r1gK/6gOGgztwxY6WUjgTb3+A95gK+FgXywC6SioJRrRKOv/2YK7U4LJp75SUspJ1JS208YrMaNek9iBk5Rs0u8TCAolqkkkPyNTiRF+fzhIK3FriC09OSnYRXISlJPQThxLOz41tqIGFdloyI6zdiCooil2etUTIBOUseD4P/7f+B+3vhmP/Md1ePisz437cDI8Hq6H+I+/8e9xzN070HhykpGxGAwxeihhIbBkUXvrdJ9Dm3EBSwRHhNpHtg+4AJPACcfJxwBAVmCd3uUWrDH4xmiuiCOGg4M4Amq9+AsJiBholLCkSgr5Lh4EVcT/M+S9FuR9KmxVDRFPQsR6ZcT5+T/Oqy4CkFCrpFjqGGDj9w/G2iUwx6ZYTCFJB+xp6utI6Sb6ThIyQaMEpf0+zpd+wkBFCXN2ypGyDqE1wnJ6DK1ykk4lhldPiNBpJTbGxRj70rionKwpahTsYoy9hrENUZLFMUFB4SF6vo14xrcSz0YVRX0nYdZOj8qpJidAJihjgdu/H+6x/ejdcyYunPtX+L2X34j/o8qtx+PER//xVfjKE2dg40M/QfOjp8Z9OBlTgKCilEQwInE+T7ijNb4V1KBtCQVas2xDDgXZqKA4n4kSovAd+4ySUNbgcPEnwIi2CQeC4JUUeGUDDnHacXoxjp4Tb5hlAJBQCaI2bp38a4F6Trx/BaS+mEBMtK1ZtCuIPJlZKpLCXS9J9JQgUTXiZ9CWeEKnTeziQUcBSUs+ADpdO3FfJ1gt+fpUaUH4eh9bH5QTT07IOD3f1LYSjyonoZW4COqJV056vmOno5yQ9WWdOqYYx1C2ZFKxikA8labYUWSCMkbM/ae/RnPtWvzXm96Cz5z0V+M+nFULKw5fueYsbPjM92BdbiLOODSkHT09sgAs+qQFQVVQ1MBZywDzUmLGGMy7UluJoSShEYYTvTgBQOO6fgHHggZG7+jhIFaVEymgc3bEm0ThFQBI6z0BeTLifSYMr4QEdUWNo+SUBJFDVE/i50RLOHGfL+eQ5yCqyFCivniStERIk12j8oGEXHS6clqlJC29IPma0XZh/R4tKen6VeAH/knHfCwEuCrxmYRpxF45odK1RligS0x8C3HaSryYcqIDADXOvs81ZliVlBjAxjVmaKChbFRraGDwnVCJHhVT11K8GDJBGSdE4J57Dn/zR6/FKzafjlvf/Id4WZE9KUcDf7RvHT70jXeBvMT9M3+7H8jkJOMwwVCSUpGL6bIWvrOCCdZRVE5CqacWg0IsCjIoyMGx5l0AiEQF8PYPApwjiPNBZHBKSgDNQfElBziKpCGEucWLqi/5hIiVGPDmoG3Czqsf0QTbqi9teSeYY6GERNqDjCZbaUs+S4WYYRIUkpSgJP6S1CfS8YvE973nJG0XppGvSaLqQS05iTkziZIl5agRFlrSYSg5YehAyGS2DrNbtJV4VDkJ5CTNOgnKSSAn6aOSE4s+0dTN2nkhZIIyZkjT4MTfvx18xul49A3HYqOpAWDqEv+mAVb0IgIAf/QP/wKnXX4vZDAY81FlTDNC/H0pjWajQGX9mnRoYJ9q1FzAiMO8lNrh40o0xGh8fDkADIOCYrT9GABECBZaqnBhkCAB6jjRtUEKKNGwEn2pXPsyTyE+JZbU5CrwRlZPPrySEiclO/KExRMV30GMRFkJ/hYgqCuemFB335IhKduMko3RsgtGCEdsE07fT9SRBYQmJS7cJSfio+qB9nV7jFADbOGNsMbPzzHiO3W03KcEZWErcd/UcQBgGsQWiEo/tBFzQk5G8k7iIECYqfedpMgEZUJATzyF3/7tfwfb0//5mnf8BNu3/tGYj2rloBaL07/8AWy4V8/vmn9s0BveNeajyph2GGIYAD0qADSYZYd5EdTUxAF/NRUwcNqCDDXSOiFYMNYYwpAEjTAa0tk9ltsbE+uH9Tlf+nGWYf03Fm+UhW9BDuTAEXR2T+ONoeyJhPUmWt9iHMo+nfKPHSEiSZtx+LoAktZ0S6YtBy05EpUk9Y60+8JGHZLSCVYLX5OWb4KS5KHmWFnYtWNESzqpF8iTkmiIJYnKSTDBBiNs6jUp42MbwtY3qoIEQ2wo6/S4xlpu5+xUZGNZR82xmncyQxrG1qcCBVYOOQEyQZkY2D17sPbLd8TXP/jZ1+OvXu1wRjXAOl4zxiObfjzR7MMj9TrM3SaY+dM7XvgLMjIOE4YIJRilOJ8yq6WePteYlxIWWuqx8CFubOO0Y0c6YwUMVMZiaKHGSX9VLZjQAOAQkiattCGeuETeEImFzu7xWWvtfwiIRtp0S7+J2kzaMo4l34qcsJNQNuK2rCNISj9LhNZrsrA0Ez/DBynzLPI1rTF24fdxvlTT8ZsEb8po+zCLL+n4+UmEWNYJ+SY6hVhQGAtDbVnHkHbupCFsaStxP5Z0gvm1iWbYPteoYKNyUnpyUhKjwMop7QRkgjKh+Jn/+hh+74//DU74bzvw+ZffOu7DmWqc9Y3fwOl/uBfH/PD7WOISeUYGAKBHJQoYWBYYcXCuTc+Z5QGMONRSxDk9KUKZp/EGi4KsdvKQoPAExPjOGGYdKuiI4VzSghzCTSx1pyAHW5UPcxOGxq2Hsk0ozYTXjI6/RFuS9VFsezWPn/ElIV5m+9aoSiIjBCWNoE/NrClJiZ8nLGwXDmDAJf4SV2qHjrbGeFJCiLkmbNpBjxyG/XkDbGECEdHwNcMOFVswtVknaQjbaCtxn+qonKxlnVSsLcW2o5wcw70VawnIBGVC0ez8B+CZH+O7f7sVl/hblNNm/wGXr39oRf4hLiV+bPfj9545C3trbd1ed18Jd//fjfmoMlY6DDGMEEoQShKU4lCDffuxdOb0OGL0qEHt/QIlW8ABlb/SV2Jjt08wzgYlxRj1p5DocL9oeCV4oiJxCnLwa5AVJRijFRgDhO7keGH3u0KZJ4S6pQiCDEM9IUs0feegaOfsLK6gRJMrLyQvEob5LVBdFiEoafkHCD27sZxDIRmW0JITb4Y1xulsQ6+clCYZ+mcsCq+UBFNsz5ujR1uJ41RirjttxBVCK7FDL1FOVio5ATJBmWw4i5+99F48ZfSP78FfeAs+8Pv34RjKmSnPh288dzIeftfJkCc1z2RT871lXj4zMhRrqAKjQY0alhyABpYHYHEYioFhgQPDwMWwttq7LwekyzGTi0bZSE4SJWXo/y3HAmtVTnDiO33IKx0NICUQB5Q66qiHXHslIsTZB+9Jcp2LygpjgQE2vJbQvWNHmc/SQkYUksWIRSeELWDE7Dr6PVO1JH6fyrUEKHhNCvWYcKEEhckP/OOuEZZIUBUavFYZi57R4X4zxRBMgjXeEFtyUFA0vn40hG1hK/HQqybtAMAZLldkWSdFJigTDqmHEPXWYd1De7D565dGmfF3Xv81XLJu53gPcMz49LMn4T//9bmd94qflDj1x38HNz8/pqPKWM0wRChFVRTnpx072NYk6+f1wAGOGXDQiccAXHIVdYYAW6BkLfmEjJTCV4gsIU5AhiUIGM5oZH7ovgmTh4Wls9qL+G8ARMOskHQVFqeqBRpglOGnHhURgJaXn7SeklTdGPk3o1LC7cG2HTnolHn089LtxgnfM3wuMcOqAVY7ceA7dGIbMakJtjBKLNvQtVYtCepJKOv0uI6qSY8alLFjZ2ErcfCcaDtxmE5cgMErmpwAmaBMFdy938dp/659fc1X34hLVnmnz3/7uzfitEsWduPkRJOMccB4V2qPBBY1XAxxA4Zcqz8FHMcPW2jsvQPBkKbJqrJS+Uh8TYxtnHRakJkA6xwax1ruIYa18O3HTttzgzfFEwxJBqg7IfVRQMs34icQp3P+yIov41AUYuI+aYlJeL6s8GTELVKuCej4ShYhL6NkREw3zyT+O35+Dgot2WiHjldMEp8JAQuICZPEck5sIWZtIS7Y4hgzAJNoUqwv58yYQQxhK8ku2ko8kxhie1SsmCC2F0ImKFOMtdeuwz//6fcDAPae4nDnO6/CcWZmzEe1fPiZ7/xbrP2r7s+34bF6TEeTkbE4dE5PAaeuVVgfe28dYR6aMMvswKJFl6EUcMKoxcAaBidR8Zo0S50WZCZB7fS1sQZOgCEKUMhPobbkAwEcsaohIvroCFI6fQ5AgtIyctVnKHlZUAZBm4sCpF1Cy4gg9hSIg/kW+0ycpTNCUFzlDa4JFrQLB7CaYGNUPbdqSVG4TuCa8aWd4DVhtASlMn7gn0+HLfxjICFpfH1FDWZ4cNBW4rSssxqUk4DD/ilvvfVWXHDBBZibmwMR4cYbb4z76rrGb/3Wb2Hz5s2YnZ3F3Nwc3vOe9+Cpp7qzTQaDAS699FJs2LABs7OzuPDCC7Fjx44X/cOsNvT+/E781Kdvx099+nb89NeGuHu4Fg/X+zvbQKbvAm7F4fF634KfZe1tM/HnDVvvf9057sPNOASstnXD+ETPkhglAaVvPe77VtE+qQFSH9u76b4P6dILly8NUDvltvThXvrcofAD54rEqMneE8He1ElGQIXTC3QSya7hYxJDyKSQ7paWVRbZQgR8iIRf1i1OF5Z2avDo5g2tbvTnKPTnjj9rsUhnTrJpTL1G1Ud/iWnTYAsfuFb630EwwZZs0SsaTYU1+rvrsb4ObcQpKWkTYoddxYS7IWyjrcQrKYjthXDYCsr+/fuxZcsW/Nqv/Rp+6Zd+qbPvueeew913343f/d3fxZYtW7Br1y5s27YNF154Ie66q5Xht23bhq9+9au4/vrrcfzxx+Oyyy7D+eefj+3bt8OY1XPylxLl9kfwyV/9VaRj15uZAhf+4V9i23E/GN+BHQH+rh7gkt+5HMc+dqDz/ol//2gu3UwpVuO6UcBghhjgIWpxgGtQ6+hhvfMWB4ZDKb4EJLocl6RZGWyl04IcfChDV6iq4hgDW7STkB1jSCYGu1krXjSR1kTrSNcIH2EPq4qK63XbdEho0e6dgGCytUaWPPvkYEi9J66UBWWc8BmppONDARDD1IJ/L3yftHwTyzlAp22YvVLCJCiMksKen0Bcep9JUEyYJPpMijD0j1rlZK2Z94bYgVdRNCXWkMMsDVFSk8zWWR2txM8HEjny6iER4YYbbsAv/uIvHvQzd955J1772tfihz/8IU4++WTs3r0bJ5xwAr7whS/gne98JwDgqaeewkknnYSvf/3rOPfccw/6vQL27NmDdevW4Wz8AgoqX/DzqxU8O4sdX3w53v6Kv124D4JfO+57OHlMs3/2uXn8f8++CrvtwhC6h/ZtxJ7/63jYBx8Zw5FlHCoaqfEd/Bl2796NY4899pC/blzrBtCuHbsefgWOXXt0Fvvn3BA1LPY6i1qAvVKgFsZ+qfCc62EoBnvdGtRisNfq43OuwrwrMXAFDtgKtTAO2BKNMxg6g8YZOBAONKWSFmtgHaO26kmxjmEdwzmKm3i/irMhqI0gDcWyTwcCUE2xjDMKHvrclXGAfMnmoATFLdwXyjnBVwIsbBcmdf6Gic7dwDXXCV3rFTptuDIWBdloimVysX24INtRw1qC0kSCElQ0JSZD32LcxtfPeuVkDVUrhpzs2etw3GmPHdK6sewelN27d4OI8JKXvAQAsH37dtR1jXPOOSd+Zm5uDmeccQZuv/32RReawWCAQTIzZc+ePct92CsCbv9+vOw9T+LO4qcW7KN+DztuPG5sU5Rvn1+Lb/7bM8F//6OFO8XC7n706B9UxsRgKdYNYDLWjh4VKGEAHmLglZT5kI/CfkYP0D66UluNISip7eAxJLBCGLoCtScqgE5AZq+SFGxgRTt7aqthblYoKirOEZj9XB/RGT8IfpUAq+RDSkAWM5cIgGD0HQcIqvgcrHMokpFuXzRxopaEt5N2YWYXyQmRqLfEz84x7EBAzDbp+RJOv6hRcQOTKCc9rtUsmxCTGTOAgWCtOdCdSkxh4F/aqSNx8F+Ir18p5ORwsawEZX5+Hh/60Ifw7ne/OzKlnTt3oqoqHHfccZ3Pbty4ETt3Lt4y+/GPfxwf/ehHl/NQVyzc3r2Lvk9FgVtv/hc4/dRXHOUjUgx29fGqHU+g2bVrLP9+xuRiqdYNYDLWDu3scUpSCLBkAWjnTumLlrXPQHHE2t3jGH2uAaeze2oYWHAMbwtwQmA/f8V6omIdx5ZkS2pmDaVRZj/Xx+eniGE1z4bSsPhOoBCGNspPJMlPGdc1M20FHq0vBdMrIZpcAXh1xCfxdogIIjEJZZxIUBITbGmUKIaWb/WVOO8zsTHXxEB9J+WI56RPLWkJiklLUJpY0lG/CcXBf6uZnADLSFDqusa73vUuOOdwzTXXvODnRQR0kGb6D3/4w/jgBz8YX+/ZswcnnXTSkh3raoQ0DX76I3891mNoXvgjGasMS7luAJOzdoTOnhIGjgbwufQAgBoOlhmlGDhw7O4x0JwM4yrUZLR7RwxKshiQi8FuDTuwdXDMGDoDSwzDjMITFetLPo3Vso8YB2ZVV3wAimapwFd+WCCWsWj131KM0x9r+mFoBQ7JrukuT1LY7wuekpBfQiRxZAD5/Wm7MIUgNmhQ3qjXJMzQUY+JjUP+AjHpUQPjlZSWoAy1nJO0EM/wABUc1nIdiQkDmKHSG6wnz1d1tLEsBKWua1x00UV4/PHH8a1vfatTZ9q0aROGwyF27drVuRt65plncOaZZy76/Xq9Hnq93nIcakZGxoRgqdcNYLLWjqCkzHAJlgZGHOAs5iGYxbBtQUYJMMBwqL2BNgwbrMVgnko/q0fTaBtnUFCBRhiFs2icQSOM2hqUhlBbg8ZfdJ04WEeajs/iBxCGDepRAUPYdbNNQigbsXbLOFk8jORoIczCKXydKTmUYHaNnUxJ2SYQk4KdJyKIhCQErIXXTAITOqg4GGBtzDHRMk9rgJ3h4QJiEjq0os8kZJ0kXTo9AioizJABgzM5SbDk2lFYZB555BHcfPPNOP744zv7t27dirIscdNNN8X3nn76adx///3Pu9BkZGSsXKyWdcP4VtE+FX5mD9D381XSFuTYbsp6p92nti21bVVt4oWyxw3WmBoVW5+/0UQjZ2l8+2tohzWhPdnCGKfD7QqLonBtm7LfF7pYOLQr+xZcGAEKN7aNChdbqDm2AIc2az1249uEw89njIt5JVWhW+Fbtit/jkofT1+yPoahfpWx6Bs912tM7c+1nvfwe9LfVdsyPMuD+Lvr80grcTTDtn6THpXqV8rkJOKwFZR9+/bh0UdbA+Pjjz+Oe++9F+vXr8fc3Bx++Zd/GXfffTe+9rWvwVob68Pr169HVVVYt24dLrnkElx22WU4/vjjsX79elx++eXYvHkz3vrWty7dT5aRkTExyOtGi5A2ewwzWFrjbE0E4/QuvRLdhmJQUoNaCpROFRQDQZ8N5l2JkiwsGBUXaJxBxQ0aZ1BL2448tKqulGxhRcs+BWs7svWBbyIEJ0BjDYxxUVEJEPHdQOy7gYCxKijpBOHgI2n3tapI6L4JIXbBZ1J6paRgNSOH56GEExQUhsRSDvtck9HhfiEJdoaHqLy3pPRD/kILcccIC4d+ElsfwtcyMVmIwyYod911F970pjfF16G+e/HFF+PKK6/EV77yFQDAa17zms7Xffvb38bZZ58NAPjUpz6Foihw0UUX4cCBA3jLW96C6667biKzDDIyMl488rrRhSG1tpaixtkhWUA0zC11ZzGCH0Xi/J6eN88G/dv5+oaBA4uBIQH7Dh/nk2hZHJgMmiSlVoSiD9YlPhQXSEtyvBKG2gAQ8uWfcRGUpBWYkw6cuBtQcuHJSSAlgE4aDnH0jHYfk6Ag2ynjsE8ADsP9Qtswk3SSYBeUc3y+SQkbpxH3qVFiSRYlJJITnUacycnB8KJyUMaFnIOSkTF+HGkOyjgxjhyU54MVBwfBc6Jhbs+JoBZgXrwKAsa8lKjFYL/roZYC+12FWgr1o/h9tdPXA1fAQlWSgSujkmKFojclKCrOm2cdKD4PploZIR/RZCutZ2WcSFuB2Se7ju7vkBGvhgRlJWSWhPk5QSkJ7cImfQ8uKiZ9rsFICUoTn8/yAAyHY3ke7FWTCqF8Z8EA+gSUROiTQc+bYVcbJioHJSMjIyNjcQTjbJ8KGFg4WD+LWB8ZohOOCaj80ME+E4yIN9lq8uwA/jVUUal963FQVGrXVVbYsaor7GIqbehgKUQJS9rSHEokLL5NGVjQ8ny0ENSQtBW4SBSUqJb49w2lKomLxCSUc4z/fKqUhJZgJtFuKm4j6g0keoFGSzol/GNSzqnIoUeAQaualGTABw1yyQjIBCUjIyNjjNByj5pnGQ1qsqjEYSgOtWiZohaGYYdaiuhNqaVQD4q0HT49rlVRoQIlW9TO6Gs2GLpCw8QSRSWoJ41jNGLQJJ4UAHG/E0JNsqAcNC4s1goclJB0vxIR21FKQp5JICT6nouEpMfaJsyQ+NjnOqoiTBLbhlVVGcJAfNuwxQzX0WdS+g6dHjEMCD0qV2Vk/ZEiE5SMjIyMCYDx4VwG5GPZLAwEDpowCqdeFRaHUgxqsTDiYIVhRDNShr70U5OFcQ6WGAPR5Fnj9ZmSHGphOLZKPkAY2AIONhIUJxy9LeG9wqstYfbPuJGaXFMVJZCUYHZNlZKgoJTxsTXEhhlIIcdE9zkYCEpqIjExJLFtOETVc5yl4zrdOdqpFRQTzuTkMJEJSkZGRsaEQD0JpqOmwFnUEFjyxk4I5qHqCTun6bMOWloQn5XiSq+8GBhxC4LeCjEdIsIQLQ2RiaQFULWkIS0HBd+K4/ErKEBCULzBNRKUpGQTyIhOE9bXADqdOEElCc9b42vTlnioicSk7crRUk6FEMam/8YMSVRNVntU/YtFJigZGRkZE4Y0edZwjVocmB0cHOaF0BeLWhgVWx/ipq3IofTTpxrzUsJKa7LtcR3NtA4EK6zPhVCQVbJiSGf3SOszqYWjwTb4TsblPwkIJKT1lUjssAlIyzdhX+l9PFEt8Z6SoKbE9N5YzmkVFEOu05mj5MShR9aTF0EJYIYNShj0fNJvJidHjqkkKKHxqEE93rjljIxVjAY1ACweiT6hCMe6Z9+4Jt0dLggDIdQCDERgRTAvQAMt0wxElY+hAE4aNFKiFotaCjRw6jURh0YMrBhYaWCj94ThPEGBmPhchOOy6oQgzoBAIDEgr66MW0GRaJR1EHIgODi2rZkXgCPnY/AdHDcACZwv21gImK2fiyQgatCQqI5EDYgcaihRcXAgsrBwAFmALIQsBP7fJqt6EwGWCEKCHhGaeIqm5W/t6CD8v3co68ZUEpS9fgDebfj6mI8kIyNj7969WLdu3bgP45AQ1o6X/9wPxnsgGRmrHIeybkxlDopzDg899BBe9apX4cknn5yaDIZpQhiqls/v8mAlnF8Rwd69ezE3Nwfm6ZCx89qx/FgJf9uTjGk/v4ezbkylgsLMeOlLXwoAOPbYY6fylzQtyOd3eTHt53dalJOAvHYcPeTzu7yY5vN7qOvGdNz2ZGRkZGRkZKwqZIKSkZGRkZGRMXGYWoLS6/VwxRVXoNfrjftQViTy+V1e5PM7PuRzv7zI53d5sZrO71SaZDMyMjIyMjJWNqZWQcnIyMjIyMhYucgEJSMjIyMjI2PikAlKRkZGRkZGxsQhE5SMjIyMjIyMiUMmKBkZGRkZGRkTh6klKNdccw1OOeUU9Pt9bN26Fd/97nfHfUhThyuvvBJE1Nk2bdoU94sIrrzySszNzWHNmjU4++yz8cADD4zxiCcft956Ky644ALMzc2BiHDjjTd29h/KOR0MBrj00kuxYcMGzM7O4sILL8SOHTuO4k+xcpHXjaVBXjuWFnndWBxTSVC+/OUvY9u2bfjIRz6Ce+65B294wxtw3nnn4Yknnhj3oU0dXv3qV+Ppp5+O23333Rf3ffKTn8RVV12Fq6++GnfeeSc2bdqEt73tbXHgWsZC7N+/H1u2bMHVV1+96P5DOafbtm3DDTfcgOuvvx633XYb9u3bh/PPPx/W2qP1Y6xI5HVjaZHXjqVDXjcOAplCvPa1r5X3ve99nfdOP/10+dCHPjSmI5pOXHHFFbJly5ZF9znnZNOmTfKJT3wivjc/Py/r1q2Ta6+99igd4XQDgNxwww3x9aGc02effVbKspTrr78+fuZHP/qRMLP8xV/8xVE79pWIvG4sHfLasXzI60aLqVNQhsMhtm/fjnPOOafz/jnnnIPbb799TEc1vXjkkUcwNzeHU045Be9617vw2GOPAQAef/xx7Ny5s3Oee70ezjrrrHyejxCHck63b9+Ouq47n5mbm8MZZ5yRz/uLQF43lh557Tg6WM3rxtQRlB//+Mew1mLjxo2d9zdu3IidO3eO6aimE6973evw+c9/Ht/4xjfwmc98Bjt37sSZZ56Jn/zkJ/Fc5vO8dDiUc7pz505UVYXjjjvuoJ/JOHzkdWNpkdeOo4fVvG4U4z6AIwURdV6LyIL3Mp4f5513Xny+efNmvP71r8crX/lKfO5zn8PP//zPA8jneTlwJOc0n/elQf57XhrktePoYzWuG1OnoGzYsAHGmAWs8JlnnlnAMDMOD7Ozs9i8eTMeeeSR6MjP53npcCjndNOmTRgOh9i1a9dBP5Nx+MjrxvIirx3Lh9W8bkwdQamqClu3bsVNN93Uef+mm27CmWeeOaajWhkYDAZ48MEHceKJJ+KUU07Bpk2bOud5OBzilltuyef5CHEo53Tr1q0oy7Lzmaeffhr3339/Pu8vAnndWF7ktWP5sKrXjfH5c48c119/vZRlKZ/97Gfl+9//vmzbtk1mZ2flBz/4wbgPbapw2WWXyXe+8x157LHH5I477pDzzz9f1q5dG8/jJz7xCVm3bp386Z/+qdx3333yK7/yK3LiiSfKnj17xnzkk4u9e/fKPffcI/fcc48AkKuuukruuece+eEPfygih3ZO3/e+98nLXvYyufnmm+Xuu++WN7/5zbJlyxZpmmZcP9aKQF43lg557Vha5HVjcUwlQRER+fSnPy0vf/nLpaoq+bmf+zm55ZZbxn1IU4d3vvOdcuKJJ0pZljI3Nydvf/vb5YEHHoj7nXNyxRVXyKZNm6TX68kb3/hGue+++8Z4xJOPb3/72wJgwXbxxReLyKGd0wMHDsiv//qvy/r162XNmjVy/vnnyxNPPDGGn2blIa8bS4O8diwt8rqxOEhEZDzaTUZGRkZGRkbG4pg6D0pGRkZGRkbGykcmKBkZGRkZGRkTh0xQMjIyMjIyMiYOmaBkZGRkZGRkTBwyQcnIyMjIyMiYOGSCkpGRkZGRkTFxyAQlIyMjIyMjY+KQCUpGRkZGRkbGxCETlIyMjIyMjIyJQyYoGRkZGRkZGROHTFAyMjIyMjIyJg7/P7uI7cODNEoVAAAAAElFTkSuQmCC", 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) --------------------------------------------------------------------------------