From 5e31f2150f2e3e4f726046f2e956632ff4f36759 Mon Sep 17 00:00:00 2001 From: Benny <775410794@qq.com> Date: Tue, 26 Nov 2019 18:45:05 +0800 Subject: [PATCH] update framework --- README.md | 107 ++++---- data_utils/ModelNetDataLoader.py | 146 +++++++---- data_utils/S3DISDataLoader.py | 386 +++++++++++++++++++++++++---- data_utils/ShapeNetDataLoader.py | 58 ++--- model/pointnet.py | 284 --------------------- models/pointnet.py | 141 +++++++++++ {model => models}/pointnet2.py | 8 +- models/pointnet2_cls_msg.py | 51 ++++ models/pointnet2_cls_ssg.py | 50 ++++ models/pointnet2_part_seg_msg.py | 59 +++++ models/pointnet2_part_seg_ssg.py | 59 +++++ models/pointnet2_sem_seg.py | 63 +++++ models/pointnet_cls.py | 40 +++ models/pointnet_part_seg.py | 86 +++++++ models/pointnet_sem_seg.py | 54 ++++ {model => models}/pointnet_util.py | 0 provider.py | 251 +++++++++++++++++++ test_cls.py | 105 ++++++++ test_partseg.py | 157 ++++++++++++ train_clf.py | 159 ------------ train_cls.py | 209 ++++++++++++++++ train_partseg.py | 325 ++++++++++++++++-------- train_semseg.py | 354 +++++++++++++++++--------- utils.py | 229 ----------------- visualizer/example.jpg | Bin 19459 -> 0 bytes visualizer/pic2.png | Bin 0 -> 220561 bytes visualizer/show3d_balls.py | 201 +++++++++------ 27 files changed, 2420 insertions(+), 1162 deletions(-) delete mode 100644 model/pointnet.py create mode 100644 models/pointnet.py rename {model => models}/pointnet2.py (95%) create mode 100644 models/pointnet2_cls_msg.py create mode 100644 models/pointnet2_cls_ssg.py create mode 100644 models/pointnet2_part_seg_msg.py create mode 100644 models/pointnet2_part_seg_ssg.py create mode 100644 models/pointnet2_sem_seg.py create mode 100644 models/pointnet_cls.py create mode 100644 models/pointnet_part_seg.py create mode 100644 models/pointnet_sem_seg.py rename {model => models}/pointnet_util.py (100%) create mode 100644 provider.py create mode 100644 test_cls.py create mode 100644 test_partseg.py delete mode 100644 train_clf.py create mode 100644 train_cls.py delete mode 100644 utils.py delete mode 100644 visualizer/example.jpg create mode 100644 visualizer/pic2.png diff --git a/README.md b/README.md index 72abc7042..0a545ccd5 100644 --- a/README.md +++ b/README.md @@ -2,72 +2,81 @@ This repo is implementation for [PointNet](http://openaccess.thecvf.com/content_cvpr_2017/papers/Qi_PointNet_Deep_Learning_CVPR_2017_paper.pdf) and [PointNet++](http://papers.nips.cc/paper/7095-pointnet-deep-hierarchical-feature-learning-on-point-sets-in-a-metric-space.pdf) in pytorch. -## Data Preparation -* Download **ModelNet** [here](http://modelnet.cs.princeton.edu/ModelNet40.zip) for classification and **ShapeNet** [here](https://shapenet.cs.stanford.edu/media/shapenetcore_partanno_segmentation_benchmark_v0_normal.zip) for part segmentation. Uncompress the downloaded data in this directory. `./data/ModelNet` and `./data/ShapeNet`. -* Run `download_data.sh` and download prepared **S3DIS** dataset for sematic segmantation and save it in `./data/indoor3d_sem_seg_hdf5_data/` +## Update +* 2019/11/26: (1). Fixed some errors in previous codes and added data augmentation tricks. Now classification can achieve 92.5\%! (2). Added testing codes, including classification and segmentation, and semantic segmentation with visualization. (3). Organized all models into `./models` files for easy use. + ## Classification -### PointNet -* python train_clf.py --model_name pointnet -### PointNet++ -* python train_clf.py --model_name pointnet2 +### Data Preparation +Download alignment **ModelNet** [here](https://shapenet.cs.stanford.edu/media/modelnet40_normal_resampled.zip) and save in `data/modelnet40_normal_resampled/` + +### Run +``` +## Check model in ./models folder +## E.g. pointnet2_msg +python train_cls.py --model pointnet2_cls_msg --normal --log_dir pointnet2_cls_msg +python test_cls.py --normal --log_dir pointnet2_cls_msg +``` + ### Performance | Model | Accuracy | |--|--| | PointNet (Official) | 89.2| -| PointNet (Pytorch) | **89.4**| -| PointNet++ (Official) | **91.9** | -| PointNet++ (Pytorch) | 91.8 | - -* Training Pointnet with 0.001 learning rate in SGD, 24 batchsize and 141 epochs. -* Training Pointnet++ with 0.001 learning rate in SGD, 12 batchsize and 45 epochs. +| PointNet++ (Official) | 91.9 | +|--|--| +| PointNet (Pytorch without normal) | 90.6| +| PointNet (Pytorch with normal) | 91.4| +| PointNet2_ssg (Pytorch without normal) | 92.2| +| PointNet2_ssg (Pytorch with normal) | **92.4**| ## Part Segmentation -### PointNet -* python train_partseg.py --model_name pointnet -### PointNet++ -* python train_partseg.py --model_name pointnet2 +### Data Preparation +Download alignment **ShapeNet** [here](https://shapenet.cs.stanford.edu/media/shapenetcore_partanno_segmentation_benchmark_v0_normal.zip) and save in `data/shapenetcore_partanno_segmentation_benchmark_v0_normal/` +### Run +``` +## Check model in ./models folder +## E.g. pointnet2_msg +python train_partseg.py --model pointnet2_part_seg_msg --normal +python test_partseg.py --normal --log_dir pointnet2_part_seg_msg +``` ### Performance -| Model | Inctance avg | Class avg |aero | bag | cap |car |chair |ear phone |guitar | knife | lamp |laptop | motor |mug | pistol |rocket | skate board | table | -|--|--|--|--|--|--|--|--|--|--|--|--|--|--|--|--|--|--|--| -|PointNet (Official) |**83.7**|**80.4** |83.4| 78.7| 82.5| 74.9| 89.6 |73| 91.5| 85.9 |80.8| 95.3| 65.2 |93| 81.2| 57.9| 72.8| 80.6| -|PointNet (Pytorch)| 82.4 |78.4| 81.1 |77.8 |83.7 |74.3 |83.3| 65.7| 90.5 |85.1| 78.1 |94.5 |63.7 |91.7 |80.5|56.2 |73.7 |67.5| -|PointNet++ (Official)|**85.1** |**81.9** |82.4|79 |87.7 |77.3| 90.8| 71.8| 91| 85.9| 83.7| 95.3 |71.6| 94.1 |81.3| 58.7| 76.4| 82.6| -|PointNet++ (Pytorch)| 84.1| 81.6 |82.6| 85.7| 89.3 |78.1|86.8| 68.9 |91.6| 88.9| 83.9 |96.8 |70.1 |95.7 |82.8| 59.8 |76.3 |71.1| - -* Training both Pointnet and Pointnet++ with 0.001 learning rate in Adam, 16 batchsize, about 130 epochs and 0.5 learning rate decay every 20/30 epochs. -* **Class avg** is the mean IoU averaged across all object categories, and **inctance avg** is the mean IoU across all objects. -* In official version PointNet, author use 2048 point cloud in training and 3000 point cloud with norm in testing. In official version PointNet++, author use 2048 point cloud with its norm (Bx2048x6) in both training and testing. - +| Model | Inctance avg | Class avg +|--|--|--| +|PointNet (Official) |**83.7**|**80.4** +|PointNet (Pytorch)| - |-| +|PointNet++ (Official)|**85.1** |**81.9** +|PointNet++ (Pytorch)| -| - ## Semantic Segmentation -### PointNet -* python train_semseg.py --model_name pointnet -### PointNet++ -* python train_semseg.py --model_name pointnet2 +### Run +``` +## Check model in ./models folder +## E.g. pointnet2_ssg +python train_semseg.py --model pointnet2_sem_seg --normal +python test_semseg.py --normal --log_dir pointnet2_sem_seg +``` ### Performance (test on Area_5) -|Model | Mean IOU | ceiling | floor | wall | beam | column | window | door | chair| tabel| bookcase| sofa | board | clutter | -|--|--|--|--|--|--|--|--|--|--|--|--|--|--|--| -| PointNet (Official) | 41.09|88.8|**97.33**|69.8|0.05|3.92|**46.26**|10.76|**52.61**|**58.93**|**40.28**|5.85|26.38|33.22| -| PointNet (Pytorch) | **44.43**|**91.1**|96.8|**72.1**|**5.82**|**14.7**|36.03|**37.1**|49.36|50.17|35.99|**14.26**|**33.9**|**40.23**| -| PointNet++ (Official) |N/A | | | | | | | | -| PointNet++ (Pytorch) | **52.28**|91.7|95.9|74.6|0.1|18.9|43.3|31.1|73.1|65.8|51.1|27.5|43.8|53.8| -* Training Pointnet with 0.001 learning rate in Adam, 24 batchsize and 84 epochs. -* Training Pointnet++ with 0.001 learning rate in Adam, 12 batchsize and 67 epochs. +|Model | Mean IOU | +|--|--| +| PointNet (Official) | 41.09| +| PointNet (Pytorch) | -| +| PointNet++ (Official) |N/A | +| PointNet++ (Pytorch) | -| + + ## Visualization ### Using show3d_balls.py -`cd visualizer`
-`bash build.sh #build C++ code for visualization` +``` +cd visualizer +bash build.sh #build C++ code for visualization +## run one example +python show3d_balls.py +``` ![](/visualizer/pic.png) -### Using pc_utils.py -![](/visualizer/example.jpg) - -## TODO +### Using MeshLab +![](/pic/pic2.png) -- [x] PointNet and PointNet++ -- [x] Experiment -- [x] Visualization Tool ## Reference By [halimacc/pointnet3](https://github.com/halimacc/pointnet3)
diff --git a/data_utils/ModelNetDataLoader.py b/data_utils/ModelNetDataLoader.py index ccbc19d2f..de67e3136 100644 --- a/data_utils/ModelNetDataLoader.py +++ b/data_utils/ModelNetDataLoader.py @@ -1,67 +1,103 @@ import numpy as np import warnings -import h5py +import os from torch.utils.data import Dataset warnings.filterwarnings('ignore') -def load_h5(h5_filename): - f = h5py.File(h5_filename) - data = f['data'][:] - label = f['label'][:] - seg = [] - return (data, label, seg) - -def load_data(dir,classification = False): - data_train0, label_train0,Seglabel_train0 = load_h5(dir + 'ply_data_train0.h5') - data_train1, label_train1,Seglabel_train1 = load_h5(dir + 'ply_data_train1.h5') - data_train2, label_train2,Seglabel_train2 = load_h5(dir + 'ply_data_train2.h5') - data_train3, label_train3,Seglabel_train3 = load_h5(dir + 'ply_data_train3.h5') - data_train4, label_train4,Seglabel_train4 = load_h5(dir + 'ply_data_train4.h5') - data_test0, label_test0,Seglabel_test0 = load_h5(dir + 'ply_data_test0.h5') - data_test1, label_test1,Seglabel_test1 = load_h5(dir + 'ply_data_test1.h5') - train_data = np.concatenate([data_train0,data_train1,data_train2,data_train3,data_train4]) - train_label = np.concatenate([label_train0,label_train1,label_train2,label_train3,label_train4]) - train_Seglabel = np.concatenate([Seglabel_train0,Seglabel_train1,Seglabel_train2,Seglabel_train3,Seglabel_train4]) - test_data = np.concatenate([data_test0,data_test1]) - test_label = np.concatenate([label_test0,label_test1]) - test_Seglabel = np.concatenate([Seglabel_test0,Seglabel_test1]) - - if classification: - return train_data, train_label, test_data, test_label - else: - return train_data, train_Seglabel, test_data, test_Seglabel + + +def pc_normalize(pc): + centroid = np.mean(pc, axis=0) + pc = pc - centroid + m = np.max(np.sqrt(np.sum(pc**2, axis=1))) + pc = pc / m + return pc + +def farthest_point_sample(point, npoint): + """ + Input: + xyz: pointcloud data, [N, D] + npoint: number of samples + Return: + centroids: sampled pointcloud index, [npoint, D] + """ + N, D = point.shape + xyz = point[:,:3] + centroids = np.zeros((npoint,)) + distance = np.ones((N,)) * 1e10 + farthest = np.random.randint(0, N) + for i in range(npoint): + centroids[i] = farthest + centroid = xyz[farthest, :] + dist = np.sum((xyz - centroid) ** 2, -1) + mask = dist < distance + distance[mask] = dist[mask] + farthest = np.argmax(distance, -1) + point = point[centroids.astype(np.int32)] + return point class ModelNetDataLoader(Dataset): - def __init__(self, data, labels, rotation = None): - self.data = data - self.labels = labels - self.rotation = rotation + def __init__(self, root, npoint=1024, split='train', uniform=False, normal_channel=True, cache_size=15000): + self.root = root + self.npoints = npoint + self.uniform = uniform + self.catfile = os.path.join(self.root, 'modelnet40_shape_names.txt') + + self.cat = [line.rstrip() for line in open(self.catfile)] + self.classes = dict(zip(self.cat, range(len(self.cat)))) + self.normal_channel = normal_channel + + shape_ids = {} + shape_ids['train'] = [line.rstrip() for line in open(os.path.join(self.root, 'modelnet40_train.txt'))] + shape_ids['test'] = [line.rstrip() for line in open(os.path.join(self.root, 'modelnet40_test.txt'))] + + assert (split == 'train' or split == 'test') + shape_names = ['_'.join(x.split('_')[0:-1]) for x in shape_ids[split]] + # list of (shape_name, shape_txt_file_path) tuple + self.datapath = [(shape_names[i], os.path.join(self.root, shape_names[i], shape_ids[split][i]) + '.txt') for i + in range(len(shape_ids[split]))] + print('The size of %s data is %d'%(split,len(self.datapath))) + + self.cache_size = cache_size # how many data points to cache in memory + self.cache = {} # from index to (point_set, cls) tuple def __len__(self): - return len(self.data) - - def rotate_point_cloud_by_angle(self, data, rotation_angle): - """ - Rotate the point cloud along up direction with certain angle. - :param batch_data: Nx3 array, original batch of point clouds - :param rotation_angle: range of rotation - :return: Nx3 array, rotated batch of point clouds - """ - cosval = np.cos(rotation_angle) - sinval = np.sin(rotation_angle) - rotation_matrix = np.array([[cosval, 0, sinval], - [0, 1, 0], - [-sinval, 0, cosval]]) - rotated_data = np.dot(data, rotation_matrix) - - return rotated_data + return len(self.datapath) + + def _get_item(self, index): + if index in self.cache: + point_set, cls = self.cache[index] + else: + fn = self.datapath[index] + cls = self.classes[self.datapath[index][0]] + cls = np.array([cls]).astype(np.int32) + point_set = np.loadtxt(fn[1], delimiter=',').astype(np.float32) + if self.uniform: + point_set = farthest_point_sample(point_set, self.npoints) + else: + point_set = point_set[0:self.npoints,:] + + point_set[:, 0:3] = pc_normalize(point_set[:, 0:3]) + + if not self.normal_channel: + point_set = point_set[:, 0:3] + + if len(self.cache) < self.cache_size: + self.cache[index] = (point_set, cls) + + return point_set, cls def __getitem__(self, index): - if self.rotation is not None: - pointcloud = self.data[index] - angle = np.random.randint(self.rotation[0], self.rotation[1]) * np.pi / 180 - pointcloud = self.rotate_point_cloud_by_angle(pointcloud, angle) + return self._get_item(index) - return pointcloud, self.labels[index] - else: - return self.data[index], self.labels[index] \ No newline at end of file + + + +if __name__ == '__main__': + import torch + + data = ModelNetDataLoader('/data/modelnet40_normal_resampled/',split='train', uniform=False, normal_channel=True,) + DataLoader = torch.utils.data.DataLoader(data, batch_size=12, shuffle=True) + for point,label in DataLoader: + print(point.shape) + print(label.shape) \ No newline at end of file diff --git a/data_utils/S3DISDataLoader.py b/data_utils/S3DISDataLoader.py index 2a0dc4647..06de12b28 100644 --- a/data_utils/S3DISDataLoader.py +++ b/data_utils/S3DISDataLoader.py @@ -1,61 +1,337 @@ -# *_*coding:utf-8 *_* import os -from torch.utils.data import Dataset +import sys import numpy as np -import h5py - -classes = ['ceiling','floor','wall','beam','column','window','door','table','chair','sofa','bookcase','board','clutter'] -class2label = {cls: i for i,cls in enumerate(classes)} - -def getDataFiles(list_filename): - return [line.rstrip() for line in open(list_filename)] - -def load_h5(h5_filename): - f = h5py.File(h5_filename) - data = f['data'][:] - label = f['label'][:] - return (data, label) - -def loadDataFile(filename): - return load_h5(filename) - -def recognize_all_data(test_area = 5): - ALL_FILES = getDataFiles('./data/indoor3d_sem_seg_hdf5_data/all_files.txt') - room_filelist = [line.rstrip() for line in open('./data/indoor3d_sem_seg_hdf5_data/room_filelist.txt')] - data_batch_list = [] - label_batch_list = [] - for h5_filename in ALL_FILES: - data_batch, label_batch = loadDataFile('./data/' + h5_filename) - data_batch_list.append(data_batch) - label_batch_list.append(label_batch) - data_batches = np.concatenate(data_batch_list, 0) - label_batches = np.concatenate(label_batch_list, 0) - - test_area = 'Area_' + str(test_area) - train_idxs = [] - test_idxs = [] - for i, room_name in enumerate(room_filelist): - if test_area in room_name: - test_idxs.append(i) - else: - train_idxs.append(i) - - train_data = data_batches[train_idxs, ...] - train_label = label_batches[train_idxs] - test_data = data_batches[test_idxs, ...] - test_label = label_batches[test_idxs] - print('train_data',train_data.shape,'train_label' ,train_label.shape) - print('test_data',test_data.shape,'test_label', test_label.shape) - return train_data,train_label,test_data,test_label - - -class S3DISDataLoader(Dataset): - def __init__(self, data, labels): - self.data = data - self.labels = labels +from torch.utils.data import Dataset + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +ROOT_DIR = os.path.dirname(BASE_DIR) +sys.path.append(BASE_DIR) +sys.path.append(ROOT_DIR) + +# root = '../data/S3DIS/stanford_indoor3d/' + +class S3DISDataset(Dataset): + def __init__(self, root, block_points=8192, split='train', test_area=5, with_rgb=True, use_weight=True, block_size=1.5, padding=0.001): + self.npoints = block_points + self.block_size = block_size + self.padding = padding + self.root = root + self.with_rgb = with_rgb + self.split = split + assert split in ['train','test'] + if self.split == 'train': + self.file_list = [d for d in os.listdir(root) if d.find('Area_%d'%test_area) is -1] + else: + self.file_list = [d for d in os.listdir(root) if d.find('Area_%d'%test_area) is not -1] + self.scene_points_list = [] + self.semantic_labels_list = [] + for file in self.file_list: + data = np.load(root+file) + self.scene_points_list.append(data[:,:6]) + self.semantic_labels_list.append(data[:,6]) + assert len(self.scene_points_list)==len(self.semantic_labels_list) + print('Number of scene: ',len(self.scene_points_list)) + if split=='train' and use_weight: + labelweights = np.zeros(13) + for seg in self.semantic_labels_list: + tmp,_ = np.histogram(seg,range(14)) + labelweights += tmp + labelweights = labelweights.astype(np.float32) + labelweights = labelweights/np.sum(labelweights) + self.labelweights = np.power(np.amax(labelweights) / labelweights, 1/3.0) + else: + self.labelweights = np.ones(13) + + print(self.labelweights) + + def __getitem__(self, index): + if self.with_rgb: + point_set = self.scene_points_list[index] + point_set[:,3:] = 2 * point_set[:,3:] / 255.0 - 1 + else: + point_set = self.scene_points_list[index][:, 0:3] + semantic_seg = self.semantic_labels_list[index].astype(np.int32) + coordmax = np.max(point_set[:, 0:3], axis=0) + coordmin = np.min(point_set[:, 0:3], axis=0) + for i in range(10): + curcenter = point_set[np.random.choice(len(semantic_seg), 1)[0], 0:3] + curmin = curcenter - [self.block_size/2, self.block_size/2, 1.5] + curmax = curcenter + [self.block_size/2, self.block_size/2, 1.5] + curmin[2] = coordmin[2] + curmax[2] = coordmax[2] + curchoice = np.sum((point_set[:, 0:3] >= (curmin - 0.2)) * (point_set[:, 0:3] <= (curmax + 0.2)), + axis=1) == 3 + cur_point_set = point_set[curchoice, 0:3] + cur_point_full = point_set[curchoice, :] + cur_semantic_seg = semantic_seg[curchoice] + if len(cur_semantic_seg) == 0: + continue + + mask = np.sum((cur_point_set >= (curmin - self.padding)) * (cur_point_set <= (curmax + self.padding)), axis=1) == 3 + if sum(mask) / float(len(mask)) < 0.01: + continue + vidx = np.ceil((cur_point_set[mask, :] - curmin) / (curmax - curmin) * [31.0, 31.0, 62.0]) + vidx = np.unique(vidx[:, 0] * 31.0 * 62.0 + vidx[:, 1] * 62.0 + vidx[:, 2]) + isvalid = len(vidx) / 31.0 / 31.0 / 62.0 >= 0.02 + if isvalid: + break + choice = np.random.choice(len(cur_semantic_seg), self.npoints, replace=True) + point_set = cur_point_full[choice, :] + semantic_seg = cur_semantic_seg[choice] + mask = mask[choice] + sample_weight = self.labelweights[semantic_seg] + sample_weight *= mask + return point_set, semantic_seg, sample_weight + + def __len__(self): + return len(self.scene_points_list) + + +class S3DISDatasetWholeScene(): + def __init__(self, root, block_points=8192, split='val', test_area=5, with_rgb = True, use_weight = True, block_size=1.5, stride=1.5, padding=0.001): + self.npoints = block_points + self.block_size = block_size + self.padding = padding + self.stride = stride + self.root = root + self.with_rgb = with_rgb + self.split = split + assert split in ['train', 'test'] + if self.split == 'train': + self.file_list = [d for d in os.listdir(root) if d.find('Area_%d' % test_area) is -1] + else: + self.file_list = [d for d in os.listdir(root) if d.find('Area_%d' % test_area) is not -1] + self.scene_points_list = [] + self.semantic_labels_list = [] + for file in self.file_list: + data = np.load(root + file) + self.scene_points_list.append(data[:, :6]) + self.semantic_labels_list.append(data[:, 6]) + assert len(self.scene_points_list) == len(self.semantic_labels_list) + print('Number of scene: ', len(self.scene_points_list)) + if split == 'train' and use_weight: + labelweights = np.zeros(13) + for seg in self.semantic_labels_list: + tmp, _ = np.histogram(seg, range(14)) + labelweights += tmp + labelweights = labelweights.astype(np.float32) + labelweights = labelweights / np.sum(labelweights) + self.labelweights = np.power(np.amax(labelweights) / labelweights, 1 / 3.0) + else: + self.labelweights = np.ones(13) + + print(self.labelweights) + + def __getitem__(self, index): + if self.with_rgb: + point_set_ini = self.scene_points_list[index] + point_set_ini[:, 3:] = 2 * point_set_ini[:, 3:] / 255.0 - 1 + else: + point_set_ini = self.scene_points_list[index][:, 0:3] + semantic_seg_ini = self.semantic_labels_list[index].astype(np.int32) + coordmax = np.max(point_set_ini[:, 0:3],axis=0) + coordmin = np.min(point_set_ini[:, 0:3],axis=0) + nsubvolume_x = np.ceil((coordmax[0]-coordmin[0])/self.block_size).astype(np.int32) + nsubvolume_y = np.ceil((coordmax[1]-coordmin[1])/self.block_size).astype(np.int32) + point_sets = list() + semantic_segs = list() + sample_weights = list() + for i in range(nsubvolume_x): + for j in range(nsubvolume_y): + curmin = coordmin+[i*self.block_size,j*self.block_size,0] + curmax = coordmin+[(i+1)*self.block_size,(j+1)*self.block_size,coordmax[2]-coordmin[2]] + curchoice = np.sum((point_set_ini[:, 0:3]>=(curmin-0.2))*(point_set_ini[:, 0:3]<=(curmax+0.2)),axis=1)==3 + cur_point_set = point_set_ini[curchoice,0:3] + cur_point_full = point_set_ini[curchoice,:] + cur_semantic_seg = semantic_seg_ini[curchoice] + if len(cur_semantic_seg)==0: + continue + mask = np.sum((cur_point_set >= (curmin - self.padding)) * (cur_point_set <= (curmax + self.padding)), + axis=1) == 3 + choice = np.random.choice(len(cur_semantic_seg), self.npoints, replace=True) + point_set = cur_point_full[choice,:] # Nx3/6 + semantic_seg = cur_semantic_seg[choice] # N + mask = mask[choice] + + if sum(mask)/float(len(mask))<0.01: + continue + + sample_weight = self.labelweights[semantic_seg] + sample_weight *= mask # N + point_sets.append(np.expand_dims(point_set,0)) # 1xNx3 + semantic_segs.append(np.expand_dims(semantic_seg,0)) # 1xN + sample_weights.append(np.expand_dims(sample_weight,0)) # 1xN + point_sets = np.concatenate(tuple(point_sets),axis=0) + semantic_segs = np.concatenate(tuple(semantic_segs),axis=0) + sample_weights = np.concatenate(tuple(sample_weights),axis=0) + return point_sets, semantic_segs, sample_weights def __len__(self): - return len(self.data) + return len(self.scene_points_list) + + +class ScannetDatasetWholeScene_evaluation(): + # prepare to give prediction on each points + def __init__(self, root, block_points=8192, split='test', test_area=5, with_rgb = True, use_weight = True, stride=0.5, block_size=1.5, padding=0.001): + self.block_points = block_points + self.block_size = block_size + self.padding = padding + self.root = root + self.with_rgb = with_rgb + self.split = split + self.stride = stride + self.scene_points_num = [] + assert split in ['train', 'test'] + if self.split == 'train': + self.file_list = [d for d in os.listdir(root) if d.find('Area_%d' % test_area) is -1] + else: + self.file_list = [d for d in os.listdir(root) if d.find('Area_%d' % test_area) is not -1] + self.scene_points_list = [] + self.semantic_labels_list = [] + for file in self.file_list: + data = np.load(root + file) + self.scene_points_list.append(data[:, :6]) + self.semantic_labels_list.append(data[:, 6]) + assert len(self.scene_points_list) == len(self.semantic_labels_list) + print('Number of scene: ', len(self.scene_points_list)) + if split == 'train' and use_weight: + labelweights = np.zeros(13) + for seg in self.semantic_labels_list: + tmp, _ = np.histogram(seg, range(14)) + self.scene_points_num.append(seg.shape[0]) + labelweights += tmp + labelweights = labelweights.astype(np.float32) + labelweights = labelweights / np.sum(labelweights) + self.labelweights = np.power(np.amax(labelweights) / labelweights, 1 / 3.0) + else: + self.labelweights = np.ones(13) + for seg in self.semantic_labels_list: + self.scene_points_num.append(seg.shape[0]) + + print(self.labelweights) + + def chunks(self, l, n): + """Yield successive n-sized chunks from l.""" + for i in range(0, len(l), n): + yield l[i:i + n] + + def split_data(self, data, idx): + new_data = [] + for i in range(len(idx)): + new_data += [np.expand_dims(data[idx[i]], axis=0)] + return new_data + + def nearest_dist(self, block_center, block_center_list): + num_blocks = len(block_center_list) + dist = np.zeros(num_blocks) + for i in range(num_blocks): + dist[i] = np.linalg.norm(block_center_list[i] - block_center, ord=2) # i->j + return np.argsort(dist)[0] def __getitem__(self, index): - return self.data[index], self.labels[index] + delta = self.stride + if self.with_rgb: + point_set_ini = self.scene_points_list[index] + point_set_ini[:, 3:] = 2 * point_set_ini[:, 3:] / 255.0 - 1 + else: + point_set_ini = self.scene_points_list[index][:, 0:3] + semantic_seg_ini = self.semantic_labels_list[index].astype(np.int32) + coordmax = np.max(point_set_ini[:, 0:3], axis=0) + coordmin = np.min(point_set_ini[:, 0:3], axis=0) + nsubvolume_x = np.ceil((coordmax[0] - coordmin[0]) / delta).astype(np.int32) + nsubvolume_y = np.ceil((coordmax[1] - coordmin[1]) / delta).astype(np.int32) + point_sets = [] + semantic_segs = [] + sample_weights = [] + point_idxs = [] + block_center = [] + for i in range(nsubvolume_x): + for j in range(nsubvolume_y): + curmin = coordmin + [i * delta, j * delta, 0] + curmax = curmin + [self.block_size, self.block_size, coordmax[2] - coordmin[2]] + curchoice = np.sum( + (point_set_ini[:, 0:3] >= (curmin - 0.2)) * (point_set_ini[:, 0:3] <= (curmax + 0.2)), axis=1) == 3 + curchoice_idx = np.where(curchoice)[0] + cur_point_set = point_set_ini[curchoice, :] + cur_semantic_seg = semantic_seg_ini[curchoice] + if len(cur_semantic_seg) == 0: + continue + mask = np.sum((cur_point_set[:, 0:3] >= (curmin - self.padding)) * (cur_point_set[:, 0:3] <= (curmax + self.padding)), + axis=1) == 3 + sample_weight = self.labelweights[cur_semantic_seg] + sample_weight *= mask # N + point_sets.append(cur_point_set) # 1xNx3/6 + semantic_segs.append(cur_semantic_seg) # 1xN + sample_weights.append(sample_weight) # 1xN + point_idxs.append(curchoice_idx) # 1xN + block_center.append((curmin[0:2] + curmax[0:2]) / 2.0) + + # merge small blocks + num_blocks = len(point_sets) + block_idx = 0 + while block_idx < num_blocks: + if point_sets[block_idx].shape[0] > self.block_points/2: + block_idx += 1 + continue + + small_block_data = point_sets[block_idx].copy() + small_block_seg = semantic_segs[block_idx].copy() + small_block_smpw = sample_weights[block_idx].copy() + small_block_idxs = point_idxs[block_idx].copy() + small_block_center = block_center[block_idx].copy() + point_sets.pop(block_idx) + semantic_segs.pop(block_idx) + sample_weights.pop(block_idx) + point_idxs.pop(block_idx) + block_center.pop(block_idx) + nearest_block_idx = self.nearest_dist(small_block_center, block_center) + point_sets[nearest_block_idx] = np.concatenate((point_sets[nearest_block_idx], small_block_data), axis=0) + semantic_segs[nearest_block_idx] = np.concatenate((semantic_segs[nearest_block_idx], small_block_seg), + axis=0) + sample_weights[nearest_block_idx] = np.concatenate((sample_weights[nearest_block_idx], small_block_smpw), + axis=0) + point_idxs[nearest_block_idx] = np.concatenate((point_idxs[nearest_block_idx], small_block_idxs), axis=0) + num_blocks = len(point_sets) + + # divide large blocks + num_blocks = len(point_sets) + div_blocks = [] + div_blocks_seg = [] + div_blocks_smpw = [] + div_blocks_idxs = [] + div_blocks_center = [] + for block_idx in range(num_blocks): + cur_num_pts = point_sets[block_idx].shape[0] + + point_idx_block = np.array([x for x in range(cur_num_pts)]) + if point_idx_block.shape[0] % self.block_points != 0: + makeup_num = self.block_points - point_idx_block.shape[0] % self.block_points + np.random.shuffle(point_idx_block) + point_idx_block = np.concatenate((point_idx_block, point_idx_block[0:makeup_num].copy())) + + np.random.shuffle(point_idx_block) + + sub_blocks = list(self.chunks(point_idx_block, self.block_points)) + + div_blocks += self.split_data(point_sets[block_idx], sub_blocks) + div_blocks_seg += self.split_data(semantic_segs[block_idx], sub_blocks) + div_blocks_smpw += self.split_data(sample_weights[block_idx], sub_blocks) + div_blocks_idxs += self.split_data(point_idxs[block_idx], sub_blocks) + div_blocks_center += [block_center[block_idx].copy() for i in range(len(sub_blocks))] + div_blocks = np.concatenate(tuple(div_blocks), axis=0) + div_blocks_seg = np.concatenate(tuple(div_blocks_seg), axis=0) + div_blocks_smpw = np.concatenate(tuple(div_blocks_smpw), axis=0) + div_blocks_idxs = np.concatenate(tuple(div_blocks_idxs), axis=0) + return div_blocks, div_blocks_seg, div_blocks_smpw, div_blocks_idxs + + def __len__(self): + return len(self.scene_points_list) + +if __name__ == '__main__': + data = S3DISDatasetWholeScene(split='test') + for i in range(10): + point_set, semantic_seg, sample_weight= data[i] + print(point_set.shape) + print(semantic_seg.shape) + print(sample_weight.shape) + diff --git a/data_utils/ShapeNetDataLoader.py b/data_utils/ShapeNetDataLoader.py index c11bb3ef8..d7457c4ec 100644 --- a/data_utils/ShapeNetDataLoader.py +++ b/data_utils/ShapeNetDataLoader.py @@ -13,33 +13,24 @@ def pc_normalize(pc): pc = pc / m return pc -def jitter_point_cloud(batch_data, sigma=0.01, clip=0.05): - """ Randomly jitter points. jittering is per point. - Input: - BxNx3 array, original batch of point clouds - Return: - BxNx3 array, jittered batch of point clouds - """ - N, C = batch_data.shape - assert(clip > 0) - jittered_data = np.clip(sigma * np.random.randn(N, C), -1*clip, clip) - jittered_data += batch_data - return jittered_data - class PartNormalDataset(Dataset): - def __init__(self, npoints=2500, split='train', normalize=True, jitter=False): + def __init__(self,root = './data/shapenetcore_partanno_segmentation_benchmark_v0_normal', npoints=2500, split='train', class_choice=None, normal_channel=False): self.npoints = npoints - self.root = './data/shapenetcore_partanno_segmentation_benchmark_v0_normal' + self.root = root self.catfile = os.path.join(self.root, 'synsetoffset2category.txt') self.cat = {} - self.normalize = normalize - self.jitter = jitter + self.normal_channel = normal_channel + with open(self.catfile, 'r') as f: for line in f: ls = line.strip().split() self.cat[ls[0]] = ls[1] self.cat = {k: v for k, v in self.cat.items()} + self.classes_original = dict(zip(self.cat, range(len(self.cat)))) + + if not class_choice is None: + self.cat = {k:v for k,v in self.cat.items() if k in class_choice} # print(self.cat) self.meta = {} @@ -77,7 +68,10 @@ def __init__(self, npoints=2500, split='train', normalize=True, jitter=False): for fn in self.meta[item]: self.datapath.append((item, fn)) - self.classes = dict(zip(self.cat, range(len(self.cat)))) + self.classes = {} + for i in self.cat.keys(): + self.classes[i] = self.classes_original[i] + # Mapping from category ('Chair') to a list of int [10,11,12,13] as segmentation labels self.seg_classes = {'Earphone': [16, 17, 18], 'Motorbike': [30, 31, 32, 33, 34, 35], 'Rocket': [41, 42, 43], 'Car': [8, 9, 10, 11], 'Laptop': [28, 29], 'Cap': [6, 7], 'Skateboard': [44, 45, 46], @@ -85,40 +79,40 @@ def __init__(self, npoints=2500, split='train', normalize=True, jitter=False): 'Table': [47, 48, 49], 'Airplane': [0, 1, 2, 3], 'Pistol': [38, 39, 40], 'Chair': [12, 13, 14, 15], 'Knife': [22, 23]} - for cat in sorted(self.seg_classes.keys()): - print(cat, self.seg_classes[cat]) + # for cat in sorted(self.seg_classes.keys()): + # print(cat, self.seg_classes[cat]) self.cache = {} # from index to (point_set, cls, seg) tuple self.cache_size = 20000 + def __getitem__(self, index): if index in self.cache: - point_set, normal, seg, cls = self.cache[index] + ppoint_set, cls, seg = self.cache[index] else: fn = self.datapath[index] cat = self.datapath[index][0] cls = self.classes[cat] cls = np.array([cls]).astype(np.int32) data = np.loadtxt(fn[1]).astype(np.float32) - point_set = data[:, 0:3] - normal = data[:, 3:6] + if not self.normal_channel: + point_set = data[:, 0:3] + else: + point_set = data[:, 0:6] seg = data[:, -1].astype(np.int32) if len(self.cache) < self.cache_size: - self.cache[index] = (point_set, normal, seg, cls) - if self.normalize: - point_set = pc_normalize(point_set) - if self.jitter: - jitter_point_cloud(point_set) + self.cache[index] = (point_set, cls, seg) + point_set[:, 0:3] = pc_normalize(point_set[:, 0:3]) + choice = np.random.choice(len(seg), self.npoints, replace=True) # resample point_set = point_set[choice, :] seg = seg[choice] - normal = normal[choice, :] + return point_set, cls, seg + def __len__(self): + return len(self.datapath) - return point_set,cls, seg, normal - def __len__(self): - return len(self.datapath) \ No newline at end of file diff --git a/model/pointnet.py b/model/pointnet.py deleted file mode 100644 index 6f13dff51..000000000 --- a/model/pointnet.py +++ /dev/null @@ -1,284 +0,0 @@ -import torch -import torch.nn as nn -import torch.nn.parallel -import torch.utils.data -from torch.autograd import Variable -import numpy as np -import torch.nn.functional as F - - -class STN3d(nn.Module): - def __init__(self): - super(STN3d, self).__init__() - self.conv1 = torch.nn.Conv1d(3, 64, 1) - self.conv2 = torch.nn.Conv1d(64, 128, 1) - self.conv3 = torch.nn.Conv1d(128, 1024, 1) - self.fc1 = nn.Linear(1024, 512) - self.fc2 = nn.Linear(512, 256) - self.fc3 = nn.Linear(256, 9) - self.relu = nn.ReLU() - - self.bn1 = nn.BatchNorm1d(64) - self.bn2 = nn.BatchNorm1d(128) - self.bn3 = nn.BatchNorm1d(1024) - self.bn4 = nn.BatchNorm1d(512) - self.bn5 = nn.BatchNorm1d(256) - - def forward(self, x): - batchsize = x.size()[0] - x = F.relu(self.bn1(self.conv1(x))) - x = F.relu(self.bn2(self.conv2(x))) - x = F.relu(self.bn3(self.conv3(x))) - x = torch.max(x, 2, keepdim=True)[0] - x = x.view(-1, 1024) - - x = F.relu(self.bn4(self.fc1(x))) - x = F.relu(self.bn5(self.fc2(x))) - x = self.fc3(x) - - iden = Variable(torch.from_numpy(np.array([1, 0, 0, 0, 1, 0, 0, 0, 1]).astype(np.float32))).view(1, 9).repeat( - batchsize, 1) - if x.is_cuda: - iden = iden.cuda() - x = x + iden - x = x.view(-1, 3, 3) - return x - - -class STNkd(nn.Module): - def __init__(self, k=64): - super(STNkd, self).__init__() - self.conv1 = torch.nn.Conv1d(k, 64, 1) - self.conv2 = torch.nn.Conv1d(64, 128, 1) - self.conv3 = torch.nn.Conv1d(128, 1024, 1) - self.fc1 = nn.Linear(1024, 512) - self.fc2 = nn.Linear(512, 256) - self.fc3 = nn.Linear(256, k * k) - self.relu = nn.ReLU() - - self.bn1 = nn.BatchNorm1d(64) - self.bn2 = nn.BatchNorm1d(128) - self.bn3 = nn.BatchNorm1d(1024) - self.bn4 = nn.BatchNorm1d(512) - self.bn5 = nn.BatchNorm1d(256) - - self.k = k - - def forward(self, x): - batchsize = x.size()[0] - x = F.relu(self.bn1(self.conv1(x))) - x = F.relu(self.bn2(self.conv2(x))) - x = F.relu(self.bn3(self.conv3(x))) - x = torch.max(x, 2, keepdim=True)[0] - x = x.view(-1, 1024) - - x = F.relu(self.bn4(self.fc1(x))) - x = F.relu(self.bn5(self.fc2(x))) - x = self.fc3(x) - - iden = Variable(torch.from_numpy(np.eye(self.k).flatten().astype(np.float32))).view(1, self.k * self.k).repeat( - batchsize, 1) - if x.is_cuda: - iden = iden.cuda() - x = x + iden - x = x.view(-1, self.k, self.k) - return x - - -class PointNetEncoder(nn.Module): - def __init__(self, global_feat=True, feature_transform=False, semseg = False): - super(PointNetEncoder, self).__init__() - self.stn = STN3d() if not semseg else STNkd(k=9) - self.conv1 = torch.nn.Conv1d(3, 64, 1) if not semseg else torch.nn.Conv1d(9, 64, 1) - self.conv2 = torch.nn.Conv1d(64, 128, 1) - self.conv3 = torch.nn.Conv1d(128, 1024, 1) - self.bn1 = nn.BatchNorm1d(64) - self.bn2 = nn.BatchNorm1d(128) - self.bn3 = nn.BatchNorm1d(1024) - self.global_feat = global_feat - self.feature_transform = feature_transform - if self.feature_transform: - self.fstn = STNkd(k=64) - - def forward(self, x): - n_pts = x.size()[2] - trans = self.stn(x) - x = x.transpose(2, 1) - x = torch.bmm(x, trans) - x = x.transpose(2, 1) - x = F.relu(self.bn1(self.conv1(x))) - - if self.feature_transform: - trans_feat = self.fstn(x) - x = x.transpose(2, 1) - x = torch.bmm(x, trans_feat) - x = x.transpose(2, 1) - else: - trans_feat = None - - pointfeat = x - x = F.relu(self.bn2(self.conv2(x))) - x = self.bn3(self.conv3(x)) - x = torch.max(x, 2, keepdim=True)[0] - x = x.view(-1, 1024) - if self.global_feat: - return x, trans, trans_feat - else: - x = x.view(-1, 1024, 1).repeat(1, 1, n_pts) - return torch.cat([x, pointfeat], 1), trans, trans_feat - - -class PointNetCls(nn.Module): - def __init__(self, k=2, feature_transform=False): - super(PointNetCls, self).__init__() - self.feature_transform = feature_transform - self.feat = PointNetEncoder(global_feat=True, feature_transform=feature_transform) - self.fc1 = nn.Linear(1024, 512) - self.fc2 = nn.Linear(512, 256) - self.fc3 = nn.Linear(256, k) - self.dropout = nn.Dropout(p=0.3) - self.bn1 = nn.BatchNorm1d(512) - self.bn2 = nn.BatchNorm1d(256) - self.relu = nn.ReLU() - - def forward(self, x): - x, trans, trans_feat = self.feat(x) - x = F.relu(self.bn1(self.fc1(x))) - x = F.relu(self.bn2(self.dropout(self.fc2(x)))) - x = self.fc3(x) - return F.log_softmax(x, dim=1), trans_feat - - -class PointNetDenseCls(nn.Module): - def __init__(self, cat_num=16,part_num=50): - super(PointNetDenseCls, self).__init__() - self.cat_num = cat_num - self.part_num = part_num - self.stn = STN3d() - self.conv1 = torch.nn.Conv1d(3, 64, 1) - self.conv2 = torch.nn.Conv1d(64, 128, 1) - self.conv3 = torch.nn.Conv1d(128, 128, 1) - self.conv4 = torch.nn.Conv1d(128, 512, 1) - self.conv5 = torch.nn.Conv1d(512, 2048, 1) - self.bn1 = nn.BatchNorm1d(64) - self.bn2 = nn.BatchNorm1d(128) - self.bn3 = nn.BatchNorm1d(128) - self.bn4 = nn.BatchNorm1d(512) - self.bn5 = nn.BatchNorm1d(2048) - self.fstn = STNkd(k=128) - # classification network - self.fc1 = nn.Linear(2048, 256) - self.fc2 = nn.Linear(256, 256) - self.fc3 = nn.Linear(256, cat_num) - self.dropout = nn.Dropout(p=0.3) - self.bnc1 = nn.BatchNorm1d(256) - self.bnc2 = nn.BatchNorm1d(256) - # segmentation network - self.convs1 = torch.nn.Conv1d(4944, 256, 1) - self.convs2 = torch.nn.Conv1d(256, 256, 1) - self.convs3 = torch.nn.Conv1d(256, 128, 1) - self.convs4 = torch.nn.Conv1d(128, part_num, 1) - self.bns1 = nn.BatchNorm1d(256) - self.bns2 = nn.BatchNorm1d(256) - self.bns3 = nn.BatchNorm1d(128) - - def forward(self, point_cloud,label): - batchsize,_ , n_pts = point_cloud.size() - # point_cloud_transformed - trans = self.stn(point_cloud) - point_cloud = point_cloud.transpose(2, 1) - point_cloud_transformed = torch.bmm(point_cloud, trans) - point_cloud_transformed = point_cloud_transformed.transpose(2, 1) - # MLP - out1 = F.relu(self.bn1(self.conv1(point_cloud_transformed))) - out2 = F.relu(self.bn2(self.conv2(out1))) - out3 = F.relu(self.bn3(self.conv3(out2))) - # net_transformed - trans_feat = self.fstn(out3) - x = out3.transpose(2, 1) - net_transformed = torch.bmm(x, trans_feat) - net_transformed = net_transformed.transpose(2, 1) - # MLP - out4 = F.relu(self.bn4(self.conv4(net_transformed))) - out5 = self.bn5(self.conv5(out4)) - out_max = torch.max(out5, 2, keepdim=True)[0] - out_max = out_max.view(-1, 2048) - # classification network - net = F.relu(self.bnc1(self.fc1(out_max))) - net = F.relu(self.bnc2(self.dropout(self.fc2(net)))) - net = self.fc3(net) # [B,16] - # segmentation network - out_max = torch.cat([out_max,label],1) - expand = out_max.view(-1, 2048+16, 1).repeat(1, 1, n_pts) - concat = torch.cat([expand, out1, out2, out3, out4, out5], 1) - net2 = F.relu(self.bns1(self.convs1(concat))) - net2 = F.relu(self.bns2(self.convs2(net2))) - net2 = F.relu(self.bns3(self.convs3(net2))) - net2 = self.convs4(net2) - net2 = net2.transpose(2, 1).contiguous() - net2 = F.log_softmax(net2.view(-1, self.part_num), dim=-1) - net2 = net2.view(batchsize, n_pts, self.part_num) # [B, N 50] - - return net, net2, trans_feat - - -def feature_transform_reguliarzer(trans): - d = trans.size()[1] - I = torch.eye(d)[None, :, :] - if trans.is_cuda: - I = I.cuda() - loss = torch.mean(torch.norm(torch.bmm(trans, trans.transpose(2, 1) - I), dim=(1, 2))) - return loss - -class PointNetLoss(torch.nn.Module): - def __init__(self, weight=1,mat_diff_loss_scale=0.001): - super(PointNetLoss, self).__init__() - self.mat_diff_loss_scale = mat_diff_loss_scale - self.weight = weight - - def forward(self, labels_pred, label, seg_pred,seg, trans_feat): - seg_loss = F.nll_loss(seg_pred, seg) - mat_diff_loss = feature_transform_reguliarzer(trans_feat) - label_loss = F.nll_loss(labels_pred, label) - - loss = self.weight * seg_loss + (1-self.weight) * label_loss + mat_diff_loss * self.mat_diff_loss_scale - return loss, seg_loss, label_loss - - -class PointNetSeg(nn.Module): - def __init__(self,num_class,feature_transform=False, semseg = False): - super(PointNetSeg, self).__init__() - self.k = num_class - self.feat = PointNetEncoder(global_feat=False,feature_transform=feature_transform, semseg = semseg) - self.conv1 = torch.nn.Conv1d(1088, 512, 1) - self.conv2 = torch.nn.Conv1d(512, 256, 1) - self.conv3 = torch.nn.Conv1d(256, 128, 1) - self.conv4 = torch.nn.Conv1d(128, self.k, 1) - self.bn1 = nn.BatchNorm1d(512) - self.bn1_1 = nn.BatchNorm1d(1024) - self.bn2 = nn.BatchNorm1d(256) - self.bn3 = nn.BatchNorm1d(128) - - def forward(self, x): - batchsize = x.size()[0] - n_pts = x.size()[2] - x, trans, trans_feat = self.feat(x) - x = F.relu(self.bn1(self.conv1(x))) - x = F.relu(self.bn2(self.conv2(x))) - x = F.relu(self.bn3(self.conv3(x))) - x = self.conv4(x) - x = x.transpose(2,1).contiguous() - x = F.log_softmax(x.view(-1,self.k), dim=-1) - x = x.view(batchsize, n_pts, self.k) - return x, trans_feat - - - -if __name__ == '__main__': - point = torch.randn(8,3,1024) - label = torch.randn(8,16) - model = PointNetDenseCls() - net, net2, trans_feat = model(point,label) - print('net',net.shape) - print('net2',net2.shape) - print('trans_feat',trans_feat.shape) diff --git a/models/pointnet.py b/models/pointnet.py new file mode 100644 index 000000000..c5c36f786 --- /dev/null +++ b/models/pointnet.py @@ -0,0 +1,141 @@ +import torch +import torch.nn as nn +import torch.nn.parallel +import torch.utils.data +from torch.autograd import Variable +import numpy as np +import torch.nn.functional as F + + +class STN3d(nn.Module): + def __init__(self, channel): + super(STN3d, self).__init__() + self.conv1 = torch.nn.Conv1d(channel, 64, 1) + self.conv2 = torch.nn.Conv1d(64, 128, 1) + self.conv3 = torch.nn.Conv1d(128, 1024, 1) + self.fc1 = nn.Linear(1024, 512) + self.fc2 = nn.Linear(512, 256) + self.fc3 = nn.Linear(256, 9) + self.relu = nn.ReLU() + + self.bn1 = nn.BatchNorm1d(64) + self.bn2 = nn.BatchNorm1d(128) + self.bn3 = nn.BatchNorm1d(1024) + self.bn4 = nn.BatchNorm1d(512) + self.bn5 = nn.BatchNorm1d(256) + + def forward(self, x): + batchsize = x.size()[0] + x = F.relu(self.bn1(self.conv1(x))) + x = F.relu(self.bn2(self.conv2(x))) + x = F.relu(self.bn3(self.conv3(x))) + x = torch.max(x, 2, keepdim=True)[0] + x = x.view(-1, 1024) + + x = F.relu(self.bn4(self.fc1(x))) + x = F.relu(self.bn5(self.fc2(x))) + x = self.fc3(x) + + iden = Variable(torch.from_numpy(np.array([1, 0, 0, 0, 1, 0, 0, 0, 1]).astype(np.float32))).view(1, 9).repeat( + batchsize, 1) + if x.is_cuda: + iden = iden.cuda() + x = x + iden + x = x.view(-1, 3, 3) + return x + + +class STNkd(nn.Module): + def __init__(self, k=64): + super(STNkd, self).__init__() + self.conv1 = torch.nn.Conv1d(k, 64, 1) + self.conv2 = torch.nn.Conv1d(64, 128, 1) + self.conv3 = torch.nn.Conv1d(128, 1024, 1) + self.fc1 = nn.Linear(1024, 512) + self.fc2 = nn.Linear(512, 256) + self.fc3 = nn.Linear(256, k * k) + self.relu = nn.ReLU() + + self.bn1 = nn.BatchNorm1d(64) + self.bn2 = nn.BatchNorm1d(128) + self.bn3 = nn.BatchNorm1d(1024) + self.bn4 = nn.BatchNorm1d(512) + self.bn5 = nn.BatchNorm1d(256) + + self.k = k + + def forward(self, x): + batchsize = x.size()[0] + x = F.relu(self.bn1(self.conv1(x))) + x = F.relu(self.bn2(self.conv2(x))) + x = F.relu(self.bn3(self.conv3(x))) + x = torch.max(x, 2, keepdim=True)[0] + x = x.view(-1, 1024) + + x = F.relu(self.bn4(self.fc1(x))) + x = F.relu(self.bn5(self.fc2(x))) + x = self.fc3(x) + + iden = Variable(torch.from_numpy(np.eye(self.k).flatten().astype(np.float32))).view(1, self.k * self.k).repeat( + batchsize, 1) + if x.is_cuda: + iden = iden.cuda() + x = x + iden + x = x.view(-1, self.k, self.k) + return x + + +class PointNetEncoder(nn.Module): + def __init__(self, global_feat=True, feature_transform=False, channel=3): + super(PointNetEncoder, self).__init__() + self.stn = STN3d(channel) + self.conv1 = torch.nn.Conv1d(channel, 64, 1) + self.conv2 = torch.nn.Conv1d(64, 128, 1) + self.conv3 = torch.nn.Conv1d(128, 1024, 1) + self.bn1 = nn.BatchNorm1d(64) + self.bn2 = nn.BatchNorm1d(128) + self.bn3 = nn.BatchNorm1d(1024) + self.global_feat = global_feat + self.feature_transform = feature_transform + if self.feature_transform: + self.fstn = STNkd(k=64) + + def forward(self, x): + B, D, N = x.size() + trans = self.stn(x) + x = x.transpose(2, 1) + if D >3 : + x, feature = x.split(3,dim=2) + x = torch.bmm(x, trans) + if D > 3: + x = torch.cat([x,feature],dim=2) + x = x.transpose(2, 1) + x = F.relu(self.bn1(self.conv1(x))) + + if self.feature_transform: + trans_feat = self.fstn(x) + x = x.transpose(2, 1) + x = torch.bmm(x, trans_feat) + x = x.transpose(2, 1) + else: + trans_feat = None + + pointfeat = x + x = F.relu(self.bn2(self.conv2(x))) + x = self.bn3(self.conv3(x)) + x = torch.max(x, 2, keepdim=True)[0] + x = x.view(-1, 1024) + if self.global_feat: + return x, trans, trans_feat + else: + x = x.view(-1, 1024, 1).repeat(1, 1, N) + return torch.cat([x, pointfeat], 1), trans, trans_feat + + +def feature_transform_reguliarzer(trans): + d = trans.size()[1] + I = torch.eye(d)[None, :, :] + if trans.is_cuda: + I = I.cuda() + loss = torch.mean(torch.norm(torch.bmm(trans, trans.transpose(2, 1) - I), dim=(1, 2))) + return loss diff --git a/model/pointnet2.py b/models/pointnet2.py similarity index 95% rename from model/pointnet2.py rename to models/pointnet2.py index 21f268cb2..9efcfec21 100644 --- a/model/pointnet2.py +++ b/models/pointnet2.py @@ -2,16 +2,14 @@ import torch import numpy as np import torch.nn.functional as F -from model.pointnet_util import PointNetSetAbstractionMsg,PointNetSetAbstraction,PointNetFeaturePropagation +from pointnet_util import PointNetSetAbstractionMsg, PointNetSetAbstraction,PointNetFeaturePropagation class PointNet2ClsMsg(nn.Module): def __init__(self): super(PointNet2ClsMsg, self).__init__() - self.sa1 = PointNetSetAbstractionMsg(512, [0.1, 0.2, 0.4], [16, 32, 128], 0, - [[32, 32, 64], [64, 64, 128], [64, 96, 128]]) - self.sa2 = PointNetSetAbstractionMsg(128, [0.2, 0.4, 0.8], [32, 64, 128], 320, - [[64, 64, 128], [128, 128, 256], [128, 128, 256]]) + self.sa1 = PointNetSetAbstractionMsg(512, [0.1, 0.2, 0.4], [16, 32, 128], 0,[[32, 32, 64], [64, 64, 128], [64, 96, 128]]) + self.sa2 = PointNetSetAbstractionMsg(128, [0.2, 0.4, 0.8], [32, 64, 128], 320,[[64, 64, 128], [128, 128, 256], [128, 128, 256]]) self.sa3 = PointNetSetAbstraction(None, None, None, 640 + 3, [256, 512, 1024], True) self.fc1 = nn.Linear(1024, 512) self.bn1 = nn.BatchNorm1d(512) diff --git a/models/pointnet2_cls_msg.py b/models/pointnet2_cls_msg.py new file mode 100644 index 000000000..d1032350d --- /dev/null +++ b/models/pointnet2_cls_msg.py @@ -0,0 +1,51 @@ +import torch.nn as nn +import torch.nn.functional as F +from pointnet_util import PointNetSetAbstractionMsg, PointNetSetAbstraction + + +class get_model(nn.Module): + def __init__(self,num_class,normal_channel=True): + super(get_model, self).__init__() + in_channel = 3 if normal_channel else 0 + self.normal_channel = normal_channel + self.sa1 = PointNetSetAbstractionMsg(512, [0.1, 0.2, 0.4], [16, 32, 128], in_channel,[[32, 32, 64], [64, 64, 128], [64, 96, 128]]) + self.sa2 = PointNetSetAbstractionMsg(128, [0.2, 0.4, 0.8], [32, 64, 128], 320,[[64, 64, 128], [128, 128, 256], [128, 128, 256]]) + self.sa3 = PointNetSetAbstraction(None, None, None, 640 + 3, [256, 512, 1024], True) + self.fc1 = nn.Linear(1024, 512) + self.bn1 = nn.BatchNorm1d(512) + self.drop1 = nn.Dropout(0.4) + self.fc2 = nn.Linear(512, 256) + self.bn2 = nn.BatchNorm1d(256) + self.drop2 = nn.Dropout(0.5) + self.fc3 = nn.Linear(256, num_class) + + def forward(self, xyz): + B, _, _ = xyz.shape + if self.normal_channel: + norm = xyz[:, 3:, :] + xyz = xyz[:, :3, :] + else: + norm = None + l1_xyz, l1_points = self.sa1(xyz, norm) + l2_xyz, l2_points = self.sa2(l1_xyz, l1_points) + l3_xyz, l3_points = self.sa3(l2_xyz, l2_points) + x = l3_points.view(B, 1024) + x = self.drop1(F.relu(self.bn1(self.fc1(x)))) + x = self.drop2(F.relu(self.bn2(self.fc2(x)))) + x = self.fc3(x) + x = F.log_softmax(x, -1) + + + return x,l3_points + + +class get_loss(nn.Module): + def __init__(self): + super(get_loss, self).__init__() + + def forward(self, pred, target, trans_feat): + total_loss = F.nll_loss(pred, target) + + return total_loss + + diff --git a/models/pointnet2_cls_ssg.py b/models/pointnet2_cls_ssg.py new file mode 100644 index 000000000..f344312ea --- /dev/null +++ b/models/pointnet2_cls_ssg.py @@ -0,0 +1,50 @@ +import torch.nn as nn +import torch.nn.functional as F +from pointnet_util import PointNetSetAbstraction + + +class get_model(nn.Module): + def __init__(self,num_class,normal_channel=True): + super(get_model, self).__init__() + in_channel = 6 if normal_channel else 3 + self.normal_channel = normal_channel + self.sa1 = PointNetSetAbstraction(npoint=512, radius=0.2, nsample=32, in_channel=in_channel, mlp=[64, 64, 128], group_all=False) + self.sa2 = PointNetSetAbstraction(npoint=128, radius=0.4, nsample=64, in_channel=128 + 3, mlp=[128, 128, 256], group_all=False) + self.sa3 = PointNetSetAbstraction(npoint=None, radius=None, nsample=None, in_channel=256 + 3, mlp=[256, 512, 1024], group_all=True) + self.fc1 = nn.Linear(1024, 512) + self.bn1 = nn.BatchNorm1d(512) + self.drop1 = nn.Dropout(0.4) + self.fc2 = nn.Linear(512, 256) + self.bn2 = nn.BatchNorm1d(256) + self.drop2 = nn.Dropout(0.4) + self.fc3 = nn.Linear(256, num_class) + + def forward(self, xyz): + B, _, _ = xyz.shape + if self.normal_channel: + norm = xyz[:, 3:, :] + xyz = xyz[:, :3, :] + else: + norm = None + l1_xyz, l1_points = self.sa1(xyz, norm) + l2_xyz, l2_points = self.sa2(l1_xyz, l1_points) + l3_xyz, l3_points = self.sa3(l2_xyz, l2_points) + x = l3_points.view(B, 1024) + x = self.drop1(F.relu(self.bn1(self.fc1(x)))) + x = self.drop2(F.relu(self.bn2(self.fc2(x)))) + x = self.fc3(x) + x = F.log_softmax(x, -1) + + + return x, l3_points + + + +class get_loss(nn.Module): + def __init__(self): + super(get_loss, self).__init__() + + def forward(self, pred, target, trans_feat): + total_loss = F.nll_loss(pred, target) + + return total_loss diff --git a/models/pointnet2_part_seg_msg.py b/models/pointnet2_part_seg_msg.py new file mode 100644 index 000000000..15c229805 --- /dev/null +++ b/models/pointnet2_part_seg_msg.py @@ -0,0 +1,59 @@ +import torch.nn as nn +import torch +import torch.nn.functional as F +from models.pointnet_util import PointNetSetAbstractionMsg,PointNetSetAbstraction,PointNetFeaturePropagation + + +class get_model(nn.Module): + def __init__(self, num_classes, normal_channel=False): + super(get_model, self).__init__() + if normal_channel: + additional_channel = 3 + else: + additional_channel = 0 + self.normal_channel = normal_channel + self.sa1 = PointNetSetAbstractionMsg(512, [0.1, 0.2, 0.4], [32, 64, 128], 3+additional_channel, [[32, 32, 64], [64, 64, 128], [64, 96, 128]]) + self.sa2 = PointNetSetAbstractionMsg(128, [0.4,0.8], [64, 128], 128+128+64, [[128, 128, 256], [128, 196, 256]]) + self.sa3 = PointNetSetAbstraction(npoint=None, radius=None, nsample=None, in_channel=512 + 3, mlp=[256, 512, 1024], group_all=True) + self.fp3 = PointNetFeaturePropagation(in_channel=1536, mlp=[256, 256]) + self.fp2 = PointNetFeaturePropagation(in_channel=576, mlp=[256, 128]) + self.fp1 = PointNetFeaturePropagation(in_channel=150+additional_channel, mlp=[128, 128]) + self.conv1 = nn.Conv1d(128, 128, 1) + self.bn1 = nn.BatchNorm1d(128) + self.drop1 = nn.Dropout(0.5) + self.conv2 = nn.Conv1d(128, num_classes, 1) + + def forward(self, xyz, cls_label): + # Set Abstraction layers + B,C,N = xyz.shape + if self.normal_channel: + l0_points = xyz + l0_xyz = xyz[:,:3,:] + else: + l0_points = xyz + l0_xyz = xyz + l1_xyz, l1_points = self.sa1(l0_xyz, l0_points) + l2_xyz, l2_points = self.sa2(l1_xyz, l1_points) + l3_xyz, l3_points = self.sa3(l2_xyz, l2_points) + # Feature Propagation layers + l2_points = self.fp3(l2_xyz, l3_xyz, l2_points, l3_points) + l1_points = self.fp2(l1_xyz, l2_xyz, l1_points, l2_points) + cls_label_one_hot = cls_label.view(B,16,1).repeat(1,1,N) + l0_points = self.fp1(l0_xyz, l1_xyz, torch.cat([cls_label_one_hot,l0_xyz,l0_points],1), l1_points) + # FC layers + feat = F.relu(self.bn1(self.conv1(l0_points))) + x = self.drop1(feat) + x = self.conv2(x) + x = F.log_softmax(x, dim=1) + x = x.permute(0, 2, 1) + return x, l3_points + + +class get_loss(nn.Module): + def __init__(self): + super(get_loss, self).__init__() + + def forward(self, pred, target, trans_feat): + total_loss = F.nll_loss(pred, target) + + return total_loss \ No newline at end of file diff --git a/models/pointnet2_part_seg_ssg.py b/models/pointnet2_part_seg_ssg.py new file mode 100644 index 000000000..ba7cb7707 --- /dev/null +++ b/models/pointnet2_part_seg_ssg.py @@ -0,0 +1,59 @@ +import torch.nn as nn +import torch +import torch.nn.functional as F +from models.pointnet_util import PointNetSetAbstraction,PointNetFeaturePropagation + + +class get_model(nn.Module): + def __init__(self, num_classes, normal_channel=False): + super(get_model, self).__init__() + if normal_channel: + additional_channel = 3 + else: + additional_channel = 0 + self.normal_channel = normal_channel + self.sa1 = PointNetSetAbstraction(npoint=512, radius=0.2, nsample=32, in_channel=6+additional_channel, mlp=[64, 64, 128], group_all=False) + self.sa2 = PointNetSetAbstraction(npoint=128, radius=0.4, nsample=64, in_channel=128 + 3, mlp=[128, 128, 256], group_all=False) + self.sa3 = PointNetSetAbstraction(npoint=None, radius=None, nsample=None, in_channel=256 + 3, mlp=[256, 512, 1024], group_all=True) + self.fp3 = PointNetFeaturePropagation(in_channel=1280, mlp=[256, 256]) + self.fp2 = PointNetFeaturePropagation(in_channel=384, mlp=[256, 128]) + self.fp1 = PointNetFeaturePropagation(in_channel=128+16+6+additional_channel, mlp=[128, 128, 128]) + self.conv1 = nn.Conv1d(128, 128, 1) + self.bn1 = nn.BatchNorm1d(128) + self.drop1 = nn.Dropout(0.5) + self.conv2 = nn.Conv1d(128, num_classes, 1) + + def forward(self, xyz, cls_label): + # Set Abstraction layers + B,C,N = xyz.shape + if self.normal_channel: + l0_points = xyz + l0_xyz = xyz[:,:3,:] + else: + l0_points = xyz + l0_xyz = xyz + l1_xyz, l1_points = self.sa1(l0_xyz, l0_points) + l2_xyz, l2_points = self.sa2(l1_xyz, l1_points) + l3_xyz, l3_points = self.sa3(l2_xyz, l2_points) + # Feature Propagation layers + l2_points = self.fp3(l2_xyz, l3_xyz, l2_points, l3_points) + l1_points = self.fp2(l1_xyz, l2_xyz, l1_points, l2_points) + cls_label_one_hot = cls_label.view(B,16,1).repeat(1,1,N) + l0_points = self.fp1(l0_xyz, l1_xyz, torch.cat([cls_label_one_hot,l0_xyz,l0_points],1), l1_points) + # FC layers + feat = F.relu(self.bn1(self.conv1(l0_points))) + x = self.drop1(feat) + x = self.conv2(x) + x = F.log_softmax(x, dim=1) + x = x.permute(0, 2, 1) + return x, l3_points + + +class get_loss(nn.Module): + def __init__(self): + super(get_loss, self).__init__() + + def forward(self, pred, target, trans_feat): + total_loss = F.nll_loss(pred, target) + + return total_loss \ No newline at end of file diff --git a/models/pointnet2_sem_seg.py b/models/pointnet2_sem_seg.py new file mode 100644 index 000000000..f4719c0e8 --- /dev/null +++ b/models/pointnet2_sem_seg.py @@ -0,0 +1,63 @@ +import torch.nn as nn +import torch.nn.functional as F +from models.pointnet_util import PointNetSetAbstraction,PointNetFeaturePropagation + + +class get_model(nn.Module): + def __init__(self, num_classes, with_rgb=True): + super(get_model, self).__init__() + self.with_rgb = with_rgb + if with_rgb: + additional_channel = 3 + else: + additional_channel = 0 + self.sa1 = PointNetSetAbstraction(1024, 0.1, 32, 6 + additional_channel, [32, 32, 64], False) + self.sa2 = PointNetSetAbstraction(256, 0.2, 32, 64 + 3, [64, 64, 128], False) + self.sa3 = PointNetSetAbstraction(64, 0.4, 32, 128 + 3, [128, 128, 256], False) + self.sa4 = PointNetSetAbstraction(16, 0.8, 32, 256 + 3, [256, 256, 512], False) + self.fp4 = PointNetFeaturePropagation(768, [256, 256]) + self.fp3 = PointNetFeaturePropagation(384, [256, 256]) + self.fp2 = PointNetFeaturePropagation(320, [256, 128]) + self.fp1 = PointNetFeaturePropagation(128, [128, 128, 128]) + self.conv1 = nn.Conv1d(128, 128, 1) + self.bn1 = nn.BatchNorm1d(128) + self.drop1 = nn.Dropout(0.5) + self.conv2 = nn.Conv1d(128, num_classes, 1) + + def forward(self, xyz): + if self.with_rgb: + l0_points = xyz + l0_xyz = xyz[:,:3,:] + else: + l0_points = xyz + l0_xyz = xyz + l1_xyz, l1_points = self.sa1(l0_xyz, l0_points) + l2_xyz, l2_points = self.sa2(l1_xyz, l1_points) + l3_xyz, l3_points = self.sa3(l2_xyz, l2_points) + l4_xyz, l4_points = self.sa4(l3_xyz, l3_points) + + l3_points = self.fp4(l3_xyz, l4_xyz, l3_points, l4_points) + l2_points = self.fp3(l2_xyz, l3_xyz, l2_points, l3_points) + l1_points = self.fp2(l1_xyz, l2_xyz, l1_points, l2_points) + l0_points = self.fp1(xyz, l1_xyz, None, l1_points) + + x = self.drop1(F.relu(self.bn1(self.conv1(l0_points)))) + x = self.conv2(x) + x = F.log_softmax(x, dim=1) + x = x.permute(0, 2, 1) + return x, l4_points + + +class get_loss(nn.Module): + def __init__(self): + super(get_loss, self).__init__() + def forward(self, pred, target, trans_feat): + total_loss = F.nll_loss(pred, target) + + return total_loss + +if __name__ == '__main__': + import torch + model = get_model(13, with_rgb=True) + xyz = torch.rand(12, 6, 2048) + (model(xyz)) \ No newline at end of file diff --git a/models/pointnet_cls.py b/models/pointnet_cls.py new file mode 100644 index 000000000..39232d1d8 --- /dev/null +++ b/models/pointnet_cls.py @@ -0,0 +1,40 @@ +import torch.nn as nn +import torch.utils.data +import torch.nn.functional as F +from pointnet import PointNetEncoder, feature_transform_reguliarzer + +class get_model(nn.Module): + def __init__(self, k=40, normal_channel=True): + super(get_model, self).__init__() + if normal_channel: + channel = 6 + else: + channel = 3 + self.feat = PointNetEncoder(global_feat=True, feature_transform=True, channel=channel) + self.fc1 = nn.Linear(1024, 512) + self.fc2 = nn.Linear(512, 256) + self.fc3 = nn.Linear(256, k) + self.dropout = nn.Dropout(p=0.4) + self.bn1 = nn.BatchNorm1d(512) + self.bn2 = nn.BatchNorm1d(256) + self.relu = nn.ReLU() + + def forward(self, x): + x, trans, trans_feat = self.feat(x) + x = F.relu(self.bn1(self.fc1(x))) + x = F.relu(self.bn2(self.dropout(self.fc2(x)))) + x = self.fc3(x) + x = F.log_softmax(x, dim=1) + return x, trans_feat + +class get_loss(torch.nn.Module): + def __init__(self, mat_diff_loss_scale=0.001): + super(get_loss, self).__init__() + self.mat_diff_loss_scale = mat_diff_loss_scale + + def forward(self, pred, target, trans_feat): + loss = F.nll_loss(pred, target) + mat_diff_loss = feature_transform_reguliarzer(trans_feat) + + total_loss = loss + mat_diff_loss * self.mat_diff_loss_scale + return total_loss diff --git a/models/pointnet_part_seg.py b/models/pointnet_part_seg.py new file mode 100644 index 000000000..1ed657890 --- /dev/null +++ b/models/pointnet_part_seg.py @@ -0,0 +1,86 @@ +import torch +import torch.nn as nn +import torch.nn.parallel +import torch.utils.data +import torch.nn.functional as F +from pointnet import STN3d, STNkd, feature_transform_reguliarzer + + +class get_model(nn.Module): + def __init__(self, part_num=50, normal_channel=True): + super(get_model, self).__init__() + if normal_channel: + channel = 6 + else: + channel = 3 + self.part_num = part_num + self.stn = STN3d(channel) + self.conv1 = torch.nn.Conv1d(channel, 64, 1) + self.conv2 = torch.nn.Conv1d(64, 128, 1) + self.conv3 = torch.nn.Conv1d(128, 128, 1) + self.conv4 = torch.nn.Conv1d(128, 512, 1) + self.conv5 = torch.nn.Conv1d(512, 2048, 1) + self.bn1 = nn.BatchNorm1d(64) + self.bn2 = nn.BatchNorm1d(128) + self.bn3 = nn.BatchNorm1d(128) + self.bn4 = nn.BatchNorm1d(512) + self.bn5 = nn.BatchNorm1d(2048) + self.fstn = STNkd(k=128) + self.convs1 = torch.nn.Conv1d(4944, 256, 1) + self.convs2 = torch.nn.Conv1d(256, 256, 1) + self.convs3 = torch.nn.Conv1d(256, 128, 1) + self.convs4 = torch.nn.Conv1d(128, part_num, 1) + self.bns1 = nn.BatchNorm1d(256) + self.bns2 = nn.BatchNorm1d(256) + self.bns3 = nn.BatchNorm1d(128) + + def forward(self, point_cloud, label): + B, D, N = point_cloud.size() + trans = self.stn(point_cloud) + point_cloud = point_cloud.transpose(2, 1) + if D > 3: + point_cloud, feature = point_cloud.split(3, dim=2) + point_cloud = torch.bmm(point_cloud, trans) + if D > 3: + point_cloud = torch.cat([point_cloud, feature], dim=2) + + point_cloud = point_cloud.transpose(2, 1) + + out1 = F.relu(self.bn1(self.conv1(point_cloud))) + out2 = F.relu(self.bn2(self.conv2(out1))) + out3 = F.relu(self.bn3(self.conv3(out2))) + + trans_feat = self.fstn(out3) + x = out3.transpose(2, 1) + net_transformed = torch.bmm(x, trans_feat) + net_transformed = net_transformed.transpose(2, 1) + + out4 = F.relu(self.bn4(self.conv4(net_transformed))) + out5 = self.bn5(self.conv5(out4)) + out_max = torch.max(out5, 2, keepdim=True)[0] + out_max = out_max.view(-1, 2048) + + out_max = torch.cat([out_max,label.squeeze(1)],1) + expand = out_max.view(-1, 2048+16, 1).repeat(1, 1, N) + concat = torch.cat([expand, out1, out2, out3, out4, out5], 1) + net = F.relu(self.bns1(self.convs1(concat))) + net = F.relu(self.bns2(self.convs2(net))) + net = F.relu(self.bns3(self.convs3(net))) + net = self.convs4(net) + net = net.transpose(2, 1).contiguous() + net = F.log_softmax(net.view(-1, self.part_num), dim=-1) + net = net.view(B, N, self.part_num) # [B, N, 50] + + return net, trans_feat + + +class get_loss(torch.nn.Module): + def __init__(self, mat_diff_loss_scale=0.001): + super(get_loss, self).__init__() + self.mat_diff_loss_scale = mat_diff_loss_scale + + def forward(self, pred, target, trans_feat): + loss = F.nll_loss(pred, target) + mat_diff_loss = feature_transform_reguliarzer(trans_feat) + total_loss = loss + mat_diff_loss * self.mat_diff_loss_scale + return total_loss \ No newline at end of file diff --git a/models/pointnet_sem_seg.py b/models/pointnet_sem_seg.py new file mode 100644 index 000000000..5a83c4cbf --- /dev/null +++ b/models/pointnet_sem_seg.py @@ -0,0 +1,54 @@ +import torch +import torch.nn as nn +import torch.nn.parallel +import torch.utils.data +import torch.nn.functional as F +from pointnet import PointNetEncoder, feature_transform_reguliarzer + + +class get_model(nn.Module): + def __init__(self, num_class, with_rgb=True): + super(get_model, self).__init__() + if with_rgb: + channel = 6 + else: + channel = 3 + self.k = num_class + self.feat = PointNetEncoder(global_feat=False, feature_transform=True, channel=channel) + self.conv1 = torch.nn.Conv1d(1088, 512, 1) + self.conv2 = torch.nn.Conv1d(512, 256, 1) + self.conv3 = torch.nn.Conv1d(256, 128, 1) + self.conv4 = torch.nn.Conv1d(128, self.k, 1) + self.bn1 = nn.BatchNorm1d(512) + self.bn2 = nn.BatchNorm1d(256) + self.bn3 = nn.BatchNorm1d(128) + + def forward(self, x): + batchsize = x.size()[0] + n_pts = x.size()[2] + x, trans, trans_feat = self.feat(x) + x = F.relu(self.bn1(self.conv1(x))) + x = F.relu(self.bn2(self.conv2(x))) + x = F.relu(self.bn3(self.conv3(x))) + x = self.conv4(x) + x = x.transpose(2,1).contiguous() + x = F.log_softmax(x.view(-1,self.k), dim=-1) + x = x.view(batchsize, n_pts, self.k) + return x, trans_feat + +class get_loss(torch.nn.Module): + def __init__(self, mat_diff_loss_scale=0.001): + super(get_loss, self).__init__() + self.mat_diff_loss_scale = mat_diff_loss_scale + + def forward(self, pred, target, trans_feat): + loss = F.nll_loss(pred, target) + mat_diff_loss = feature_transform_reguliarzer(trans_feat) + total_loss = loss + mat_diff_loss * self.mat_diff_loss_scale + return total_loss + + +if __name__ == '__main__': + model = get_model(13, with_rgb=False) + xyz = torch.rand(12, 3, 2048) + (model(xyz)) \ No newline at end of file diff --git a/model/pointnet_util.py b/models/pointnet_util.py similarity index 100% rename from model/pointnet_util.py rename to models/pointnet_util.py diff --git a/provider.py b/provider.py new file mode 100644 index 000000000..56046916e --- /dev/null +++ b/provider.py @@ -0,0 +1,251 @@ +import numpy as np + +def normalize_data(batch_data): + """ Normalize the batch data, use coordinates of the block centered at origin, + Input: + BxNxC array + Output: + BxNxC array + """ + B, N, C = batch_data.shape + normal_data = np.zeros((B, N, C)) + for b in range(B): + pc = batch_data[b] + centroid = np.mean(pc, axis=0) + pc = pc - centroid + m = np.max(np.sqrt(np.sum(pc ** 2, axis=1))) + pc = pc / m + normal_data[b] = pc + return normal_data + + +def shuffle_data(data, labels): + """ Shuffle data and labels. + Input: + data: B,N,... numpy array + label: B,... numpy array + Return: + shuffled data, label and shuffle indices + """ + idx = np.arange(len(labels)) + np.random.shuffle(idx) + return data[idx, ...], labels[idx], idx + +def shuffle_points(batch_data): + """ Shuffle orders of points in each point cloud -- changes FPS behavior. + Use the same shuffling idx for the entire batch. + Input: + BxNxC array + Output: + BxNxC array + """ + idx = np.arange(batch_data.shape[1]) + np.random.shuffle(idx) + return batch_data[:,idx,:] + +def rotate_point_cloud(batch_data): + """ Randomly rotate the point clouds to augument the dataset + rotation is per shape based along up direction + Input: + BxNx3 array, original batch of point clouds + Return: + BxNx3 array, rotated batch of point clouds + """ + rotated_data = np.zeros(batch_data.shape, dtype=np.float32) + for k in range(batch_data.shape[0]): + rotation_angle = np.random.uniform() * 2 * np.pi + cosval = np.cos(rotation_angle) + sinval = np.sin(rotation_angle) + rotation_matrix = np.array([[cosval, 0, sinval], + [0, 1, 0], + [-sinval, 0, cosval]]) + shape_pc = batch_data[k, ...] + rotated_data[k, ...] = np.dot(shape_pc.reshape((-1, 3)), rotation_matrix) + return rotated_data + +def rotate_point_cloud_z(batch_data): + """ Randomly rotate the point clouds to augument the dataset + rotation is per shape based along up direction + Input: + BxNx3 array, original batch of point clouds + Return: + BxNx3 array, rotated batch of point clouds + """ + rotated_data = np.zeros(batch_data.shape, dtype=np.float32) + for k in range(batch_data.shape[0]): + rotation_angle = np.random.uniform() * 2 * np.pi + cosval = np.cos(rotation_angle) + sinval = np.sin(rotation_angle) + rotation_matrix = np.array([[cosval, sinval, 0], + [-sinval, cosval, 0], + [0, 0, 1]]) + shape_pc = batch_data[k, ...] + rotated_data[k, ...] = np.dot(shape_pc.reshape((-1, 3)), rotation_matrix) + return rotated_data + +def rotate_point_cloud_with_normal(batch_xyz_normal): + ''' Randomly rotate XYZ, normal point cloud. + Input: + batch_xyz_normal: B,N,6, first three channels are XYZ, last 3 all normal + Output: + B,N,6, rotated XYZ, normal point cloud + ''' + for k in range(batch_xyz_normal.shape[0]): + rotation_angle = np.random.uniform() * 2 * np.pi + cosval = np.cos(rotation_angle) + sinval = np.sin(rotation_angle) + rotation_matrix = np.array([[cosval, 0, sinval], + [0, 1, 0], + [-sinval, 0, cosval]]) + shape_pc = batch_xyz_normal[k,:,0:3] + shape_normal = batch_xyz_normal[k,:,3:6] + batch_xyz_normal[k,:,0:3] = np.dot(shape_pc.reshape((-1, 3)), rotation_matrix) + batch_xyz_normal[k,:,3:6] = np.dot(shape_normal.reshape((-1, 3)), rotation_matrix) + return batch_xyz_normal + +def rotate_perturbation_point_cloud_with_normal(batch_data, angle_sigma=0.06, angle_clip=0.18): + """ Randomly perturb the point clouds by small rotations + Input: + BxNx6 array, original batch of point clouds and point normals + Return: + BxNx3 array, rotated batch of point clouds + """ + rotated_data = np.zeros(batch_data.shape, dtype=np.float32) + for k in range(batch_data.shape[0]): + angles = np.clip(angle_sigma*np.random.randn(3), -angle_clip, angle_clip) + Rx = np.array([[1,0,0], + [0,np.cos(angles[0]),-np.sin(angles[0])], + [0,np.sin(angles[0]),np.cos(angles[0])]]) + Ry = np.array([[np.cos(angles[1]),0,np.sin(angles[1])], + [0,1,0], + [-np.sin(angles[1]),0,np.cos(angles[1])]]) + Rz = np.array([[np.cos(angles[2]),-np.sin(angles[2]),0], + [np.sin(angles[2]),np.cos(angles[2]),0], + [0,0,1]]) + R = np.dot(Rz, np.dot(Ry,Rx)) + shape_pc = batch_data[k,:,0:3] + shape_normal = batch_data[k,:,3:6] + rotated_data[k,:,0:3] = np.dot(shape_pc.reshape((-1, 3)), R) + rotated_data[k,:,3:6] = np.dot(shape_normal.reshape((-1, 3)), R) + return rotated_data + + +def rotate_point_cloud_by_angle(batch_data, rotation_angle): + """ Rotate the point cloud along up direction with certain angle. + Input: + BxNx3 array, original batch of point clouds + Return: + BxNx3 array, rotated batch of point clouds + """ + rotated_data = np.zeros(batch_data.shape, dtype=np.float32) + for k in range(batch_data.shape[0]): + #rotation_angle = np.random.uniform() * 2 * np.pi + cosval = np.cos(rotation_angle) + sinval = np.sin(rotation_angle) + rotation_matrix = np.array([[cosval, 0, sinval], + [0, 1, 0], + [-sinval, 0, cosval]]) + shape_pc = batch_data[k,:,0:3] + rotated_data[k,:,0:3] = np.dot(shape_pc.reshape((-1, 3)), rotation_matrix) + return rotated_data + +def rotate_point_cloud_by_angle_with_normal(batch_data, rotation_angle): + """ Rotate the point cloud along up direction with certain angle. + Input: + BxNx6 array, original batch of point clouds with normal + scalar, angle of rotation + Return: + BxNx6 array, rotated batch of point clouds iwth normal + """ + rotated_data = np.zeros(batch_data.shape, dtype=np.float32) + for k in range(batch_data.shape[0]): + #rotation_angle = np.random.uniform() * 2 * np.pi + cosval = np.cos(rotation_angle) + sinval = np.sin(rotation_angle) + rotation_matrix = np.array([[cosval, 0, sinval], + [0, 1, 0], + [-sinval, 0, cosval]]) + shape_pc = batch_data[k,:,0:3] + shape_normal = batch_data[k,:,3:6] + rotated_data[k,:,0:3] = np.dot(shape_pc.reshape((-1, 3)), rotation_matrix) + rotated_data[k,:,3:6] = np.dot(shape_normal.reshape((-1,3)), rotation_matrix) + return rotated_data + + + +def rotate_perturbation_point_cloud(batch_data, angle_sigma=0.06, angle_clip=0.18): + """ Randomly perturb the point clouds by small rotations + Input: + BxNx3 array, original batch of point clouds + Return: + BxNx3 array, rotated batch of point clouds + """ + rotated_data = np.zeros(batch_data.shape, dtype=np.float32) + for k in range(batch_data.shape[0]): + angles = np.clip(angle_sigma*np.random.randn(3), -angle_clip, angle_clip) + Rx = np.array([[1,0,0], + [0,np.cos(angles[0]),-np.sin(angles[0])], + [0,np.sin(angles[0]),np.cos(angles[0])]]) + Ry = np.array([[np.cos(angles[1]),0,np.sin(angles[1])], + [0,1,0], + [-np.sin(angles[1]),0,np.cos(angles[1])]]) + Rz = np.array([[np.cos(angles[2]),-np.sin(angles[2]),0], + [np.sin(angles[2]),np.cos(angles[2]),0], + [0,0,1]]) + R = np.dot(Rz, np.dot(Ry,Rx)) + shape_pc = batch_data[k, ...] + rotated_data[k, ...] = np.dot(shape_pc.reshape((-1, 3)), R) + return rotated_data + + +def jitter_point_cloud(batch_data, sigma=0.01, clip=0.05): + """ Randomly jitter points. jittering is per point. + Input: + BxNx3 array, original batch of point clouds + Return: + BxNx3 array, jittered batch of point clouds + """ + B, N, C = batch_data.shape + assert(clip > 0) + jittered_data = np.clip(sigma * np.random.randn(B, N, C), -1*clip, clip) + jittered_data += batch_data + return jittered_data + +def shift_point_cloud(batch_data, shift_range=0.1): + """ Randomly shift point cloud. Shift is per point cloud. + Input: + BxNx3 array, original batch of point clouds + Return: + BxNx3 array, shifted batch of point clouds + """ + B, N, C = batch_data.shape + shifts = np.random.uniform(-shift_range, shift_range, (B,3)) + for batch_index in range(B): + batch_data[batch_index,:,:] += shifts[batch_index,:] + return batch_data + + +def random_scale_point_cloud(batch_data, scale_low=0.8, scale_high=1.25): + """ Randomly scale the point cloud. Scale is per point cloud. + Input: + BxNx3 array, original batch of point clouds + Return: + BxNx3 array, scaled batch of point clouds + """ + B, N, C = batch_data.shape + scales = np.random.uniform(scale_low, scale_high, B) + for batch_index in range(B): + batch_data[batch_index,:,:] *= scales[batch_index] + return batch_data + +def random_point_dropout(batch_pc, max_dropout_ratio=0.875): + ''' batch_pc: BxNx3 ''' + for b in range(batch_pc.shape[0]): + dropout_ratio = np.random.random()*max_dropout_ratio # 0~0.875 + drop_idx = np.where(np.random.random((batch_pc.shape[1]))<=dropout_ratio)[0] + if len(drop_idx)>0: + batch_pc[b,drop_idx,:] = batch_pc[b,0,:] # set to the first point + return batch_pc + + + diff --git a/test_cls.py b/test_cls.py new file mode 100644 index 000000000..ce64445ed --- /dev/null +++ b/test_cls.py @@ -0,0 +1,105 @@ +""" +Author: Benny +Date: Nov 2019 +""" +from data_utils.ModelNetDataLoader import ModelNetDataLoader +import argparse +import numpy as np +import os +import torch +import logging +from tqdm import tqdm +import sys +import importlib + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +ROOT_DIR = BASE_DIR +sys.path.append(os.path.join(ROOT_DIR, 'models')) + + +def parse_args(): + '''PARAMETERS''' + parser = argparse.ArgumentParser('PointNet') + parser.add_argument('--batch_size', type=int, default=24, help='batch size in training') + parser.add_argument('--gpu', type=str, default='0', help='specify gpu device') + parser.add_argument('--num_point', type=int, default=1024, help='Point Number [default: 1024]') + parser.add_argument('--log_dir', type=str, default='pointnet2_ssg_normal', help='Experiment root') + parser.add_argument('--normal', action='store_true', default=True, help='Whether to use normal information [default: False]') + parser.add_argument('--num_votes', type=int, default=3, help='Aggregate classification scores with voting [default: 3]') + return parser.parse_args() + +def test(model, loader, num_class=40, vote_num=1): + mean_correct = [] + class_acc = np.zeros((num_class,3)) + for j, data in tqdm(enumerate(loader), total=len(loader)): + points, target = data + target = target[:, 0] + points = points.transpose(2, 1) + points, target = points.cuda(), target.cuda() + classifier = model.eval() + vote_pool = torch.zeros(target.size()[0],num_class).cuda() + for _ in range(vote_num): + pred, _ = classifier(points) + vote_pool += pred + pred = vote_pool/vote_num + pred_choice = pred.data.max(1)[1] + for cat in np.unique(target.cpu()): + classacc = pred_choice[target==cat].eq(target[target==cat].long().data).cpu().sum() + class_acc[cat,0]+= classacc.item()/float(points[target==cat].size()[0]) + class_acc[cat,1]+=1 + correct = pred_choice.eq(target.long().data).cpu().sum() + mean_correct.append(correct.item()/float(points.size()[0])) + class_acc[:,2] = class_acc[:,0]/ class_acc[:,1] + class_acc = np.mean(class_acc[:,2]) + instance_acc = np.mean(mean_correct) + return instance_acc, class_acc + + +def main(args): + def log_string(str): + logger.info(str) + print(str) + + '''HYPER PARAMETER''' + os.environ["CUDA_VISIBLE_DEVICES"] = args.gpu + + '''CREATE DIR''' + experiment_dir = 'log/classification/' + args.log_dir + + '''LOG''' + args = parse_args() + logger = logging.getLogger("Model") + logger.setLevel(logging.INFO) + formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') + file_handler = logging.FileHandler('%s/eval.txt' % experiment_dir) + file_handler.setLevel(logging.INFO) + file_handler.setFormatter(formatter) + logger.addHandler(file_handler) + log_string('PARAMETER ...') + log_string(args) + + '''DATA LOADING''' + log_string('Load dataset ...') + DATA_PATH = 'data/modelnet40_normal_resampled/' + TEST_DATASET = ModelNetDataLoader(root=DATA_PATH, npoint=args.num_point, split='test', normal_channel=args.normal) + testDataLoader = torch.utils.data.DataLoader(TEST_DATASET, batch_size=args.batch_size, shuffle=False, num_workers=4) + + '''MODEL LOADING''' + num_class = 40 + model_name = os.listdir(experiment_dir+'/logs')[0].split('.')[0] + MODEL = importlib.import_module(model_name) + + classifier = MODEL.get_model(num_class,normal_channel=args.normal).cuda() + + checkpoint = torch.load(str(experiment_dir) + '/checkpoints/best_model.pth') + classifier.load_state_dict(checkpoint['model_state_dict']) + + with torch.no_grad(): + instance_acc, class_acc = test(classifier.eval(), testDataLoader, vote_num=args.num_votes) + log_string('Test Instance Accuracy: %f, Class Accuracy: %f' % (instance_acc, class_acc)) + + + +if __name__ == '__main__': + args = parse_args() + main(args) diff --git a/test_partseg.py b/test_partseg.py new file mode 100644 index 000000000..8e14982fa --- /dev/null +++ b/test_partseg.py @@ -0,0 +1,157 @@ +""" +Author: Benny +Date: Nov 2019 +""" +import argparse +import os +from data_utils.ShapeNetDataLoader import PartNormalDataset +import torch +import logging +import sys +import importlib +from tqdm import tqdm +import numpy as np + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +ROOT_DIR = BASE_DIR +sys.path.append(os.path.join(ROOT_DIR, 'models')) + +seg_classes = {'Earphone': [16, 17, 18], 'Motorbike': [30, 31, 32, 33, 34, 35], 'Rocket': [41, 42, 43], 'Car': [8, 9, 10, 11], 'Laptop': [28, 29], 'Cap': [6, 7], 'Skateboard': [44, 45, 46], 'Mug': [36, 37], 'Guitar': [19, 20, 21], 'Bag': [4, 5], 'Lamp': [24, 25, 26, 27], 'Table': [47, 48, 49], 'Airplane': [0, 1, 2, 3], 'Pistol': [38, 39, 40], 'Chair': [12, 13, 14, 15], 'Knife': [22, 23]} +seg_label_to_cat = {} # {0:Airplane, 1:Airplane, ...49:Table} +for cat in seg_classes.keys(): + for label in seg_classes[cat]: + seg_label_to_cat[label] = cat + +def to_categorical(y, num_classes): + """ 1-hot encodes a tensor """ + new_y = torch.eye(num_classes)[y.cpu().data.numpy(),] + if (y.is_cuda): + return new_y.cuda() + return new_y + + +def parse_args(): + '''PARAMETERS''' + parser = argparse.ArgumentParser('PointNet') + parser.add_argument('--batch_size', type=int, default=24, help='batch size in testing') + parser.add_argument('--gpu', type=str, default='0', help='specify gpu device') + parser.add_argument('--num_point', type=int, default=2048, help='Point Number [default: 1024]') + parser.add_argument('--log_dir', type=str, default='pointnet2_part_seg_ssg', help='Experiment root') + parser.add_argument('--normal', action='store_true', default=True, help='Whether to use normal information [default: False]') + parser.add_argument('--num_votes', type=int, default=3, help='Aggregate segmentation scores with voting [default: 3]') + return parser.parse_args() + +def main(args): + def log_string(str): + logger.info(str) + print(str) + + '''HYPER PARAMETER''' + os.environ["CUDA_VISIBLE_DEVICES"] = args.gpu + experiment_dir = 'log/part_seg/' + args.log_dir + + '''LOG''' + args = parse_args() + logger = logging.getLogger("Model") + logger.setLevel(logging.INFO) + formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') + file_handler = logging.FileHandler('%s/eval.txt' % experiment_dir) + file_handler.setLevel(logging.INFO) + file_handler.setFormatter(formatter) + logger.addHandler(file_handler) + log_string('PARAMETER ...') + log_string(args) + + root = 'data/shapenetcore_partanno_segmentation_benchmark_v0_normal/' + + TEST_DATASET = PartNormalDataset(root = root, npoints=args.num_point, split='test', normal_channel=args.normal) + testDataLoader = torch.utils.data.DataLoader(TEST_DATASET, batch_size=args.batch_size,shuffle=False, num_workers=4) + log_string("The number of test data is: %d" % len(TEST_DATASET)) + num_classes = 16 + num_part = 50 + + '''MODEL LOADING''' + model_name = os.listdir(experiment_dir+'/logs')[0].split('.')[0] + MODEL = importlib.import_module(model_name) + classifier = MODEL.get_model(num_part, normal_channel=args.normal).cuda() + checkpoint = torch.load(str(experiment_dir) + '/checkpoints/best_model.pth') + classifier.load_state_dict(checkpoint['model_state_dict']) + + + with torch.no_grad(): + test_metrics = {} + total_correct = 0 + total_seen = 0 + total_seen_class = [0 for _ in range(num_part)] + total_correct_class = [0 for _ in range(num_part)] + shape_ious = {cat: [] for cat in seg_classes.keys()} + seg_label_to_cat = {} # {0:Airplane, 1:Airplane, ...49:Table} + for cat in seg_classes.keys(): + for label in seg_classes[cat]: + seg_label_to_cat[label] = cat + + for batch_id, (points, label, target) in tqdm(enumerate(testDataLoader), total=len(testDataLoader), smoothing=0.9): + batchsize, num_point, _ = points.size() + cur_batch_size, NUM_POINT, _ = points.size() + points, label, target = points.float().cuda(), label.long().cuda(), target.long().cuda() + points = points.transpose(2, 1) + classifier = classifier.eval() + vote_pool = torch.zeros(target.size()[0], target.size()[1], num_part).cuda() + for _ in range(args.num_votes): + seg_pred, _ = classifier(points, to_categorical(label, num_classes)) + vote_pool += seg_pred + seg_pred = vote_pool / args.num_votes + cur_pred_val = seg_pred.cpu().data.numpy() + cur_pred_val_logits = cur_pred_val + cur_pred_val = np.zeros((cur_batch_size, NUM_POINT)).astype(np.int32) + target = target.cpu().data.numpy() + for i in range(cur_batch_size): + cat = seg_label_to_cat[target[i, 0]] + logits = cur_pred_val_logits[i, :, :] + cur_pred_val[i, :] = np.argmax(logits[:, seg_classes[cat]], 1) + seg_classes[cat][0] + correct = np.sum(cur_pred_val == target) + total_correct += correct + total_seen += (cur_batch_size * NUM_POINT) + + for l in range(num_part): + total_seen_class[l] += np.sum(target == l) + total_correct_class[l] += (np.sum((cur_pred_val == l) & (target == l))) + + for i in range(cur_batch_size): + segp = cur_pred_val[i, :] + segl = target[i, :] + cat = seg_label_to_cat[segl[0]] + part_ious = [0.0 for _ in range(len(seg_classes[cat]))] + for l in seg_classes[cat]: + if (np.sum(segl == l) == 0) and ( + np.sum(segp == l) == 0): # part is not present, no prediction as well + part_ious[l - seg_classes[cat][0]] = 1.0 + else: + part_ious[l - seg_classes[cat][0]] = np.sum((segl == l) & (segp == l)) / float( + np.sum((segl == l) | (segp == l))) + shape_ious[cat].append(np.mean(part_ious)) + + all_shape_ious = [] + for cat in shape_ious.keys(): + for iou in shape_ious[cat]: + all_shape_ious.append(iou) + shape_ious[cat] = np.mean(shape_ious[cat]) + mean_shape_ious = np.mean(list(shape_ious.values())) + test_metrics['accuracy'] = total_correct / float(total_seen) + test_metrics['class_avg_accuracy'] = np.mean( + np.array(total_correct_class) / np.array(total_seen_class, dtype=np.float)) + for cat in sorted(shape_ious.keys()): + log_string('eval mIoU of %s %f' % (cat + ' ' * (14 - len(cat)), shape_ious[cat])) + test_metrics['class_avg_iou'] = mean_shape_ious + test_metrics['inctance_avg_iou'] = np.mean(all_shape_ious) + + + log_string('Accuracy is: %.5f'%test_metrics['accuracy']) + log_string('Class avg accuracy is: %.5f'%test_metrics['class_avg_accuracy']) + log_string('Class avg mIOU is: %.5f'%test_metrics['class_avg_iou']) + log_string('Inctance avg mIOU is: %.5f'%test_metrics['inctance_avg_iou']) + +if __name__ == '__main__': + args = parse_args() + main(args) + diff --git a/train_clf.py b/train_clf.py deleted file mode 100644 index 17d94125e..000000000 --- a/train_clf.py +++ /dev/null @@ -1,159 +0,0 @@ -import argparse -import os -import torch -import torch.nn.parallel -import torch.utils.data -import torch.nn.functional as F -from data_utils.ModelNetDataLoader import ModelNetDataLoader, load_data -import datetime -import logging -from pathlib import Path -from tqdm import tqdm -from utils import test, save_checkpoint -from model.pointnet2 import PointNet2ClsMsg -from model.pointnet import PointNetCls, feature_transform_reguliarzer - - -def parse_args(): - '''PARAMETERS''' - parser = argparse.ArgumentParser('PointNet') - parser.add_argument('--batchsize', type=int, default=24, help='batch size in training') - parser.add_argument('--epoch', default=200, type=int, help='number of epoch in training') - parser.add_argument('--learning_rate', default=0.001, type=float, help='learning rate in training') - parser.add_argument('--gpu', type=str, default='0', help='specify gpu device') - parser.add_argument('--train_metric', type=str, default=False, help='whether evaluate on training dataset') - parser.add_argument('--optimizer', type=str, default='SGD', help='optimizer for training') - parser.add_argument('--pretrain', type=str, default=None,help='whether use pretrain model') - parser.add_argument('--decay_rate', type=float, default=1e-4, help='decay rate of learning rate') - parser.add_argument('--rotation', default=None, help='range of training rotation') - parser.add_argument('--model_name', default='pointnet2', help='range of training rotation') - parser.add_argument('--feature_transform', default=False, help="use feature transform in pointnet") - return parser.parse_args() - -def main(args): - '''HYPER PARAMETER''' - os.environ["CUDA_VISIBLE_DEVICES"] = args.gpu - datapath = './data/ModelNet/' - - if args.rotation is not None: - ROTATION = (int(args.rotation[0:2]),int(args.rotation[3:5])) - else: - ROTATION = None - - '''CREATE DIR''' - experiment_dir = Path('./experiment/') - experiment_dir.mkdir(exist_ok=True) - checkpoints_dir = Path('./experiment/checkpoints/') - checkpoints_dir.mkdir(exist_ok=True) - log_dir = Path('./experiment/logs/') - log_dir.mkdir(exist_ok=True) - - '''LOG''' - args = parse_args() - logger = logging.getLogger("PointNet2") - logger.setLevel(logging.INFO) - formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') - file_handler = logging.FileHandler('./experiment/logs/train_%s_'%args.model_name+ str(datetime.datetime.now().strftime('%Y-%m-%d %H-%M'))+'.txt') - file_handler.setLevel(logging.INFO) - file_handler.setFormatter(formatter) - logger.addHandler(file_handler) - logger.info('---------------------------------------------------TRANING---------------------------------------------------') - logger.info('PARAMETER ...') - logger.info(args) - - '''DATA LOADING''' - logger.info('Load dataset ...') - train_data, train_label, test_data, test_label = load_data(datapath, classification=True) - logger.info("The number of training data is: %d",train_data.shape[0]) - logger.info("The number of test data is: %d", test_data.shape[0]) - trainDataset = ModelNetDataLoader(train_data, train_label, rotation=ROTATION) - if ROTATION is not None: - print('The range of training rotation is',ROTATION) - testDataset = ModelNetDataLoader(test_data, test_label, rotation=ROTATION) - trainDataLoader = torch.utils.data.DataLoader(trainDataset, batch_size=args.batchsize, shuffle=True) - testDataLoader = torch.utils.data.DataLoader(testDataset, batch_size=args.batchsize, shuffle=False) - - '''MODEL LOADING''' - num_class = 40 - classifier = PointNetCls(num_class,args.feature_transform).cuda() if args.model_name == 'pointnet' else PointNet2ClsMsg().cuda() - if args.pretrain is not None: - print('Use pretrain model...') - logger.info('Use pretrain model') - checkpoint = torch.load(args.pretrain) - start_epoch = checkpoint['epoch'] - classifier.load_state_dict(checkpoint['model_state_dict']) - else: - print('No existing model, starting training from scratch...') - start_epoch = 0 - - - if args.optimizer == 'SGD': - optimizer = torch.optim.SGD(classifier.parameters(), lr=0.01, momentum=0.9) - elif args.optimizer == 'Adam': - optimizer = torch.optim.Adam( - classifier.parameters(), - lr=args.learning_rate, - betas=(0.9, 0.999), - eps=1e-08, - weight_decay=args.decay_rate - ) - scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=20, gamma=0.5) - global_epoch = 0 - global_step = 0 - best_tst_accuracy = 0.0 - blue = lambda x: '\033[94m' + x + '\033[0m' - - '''TRANING''' - logger.info('Start training...') - for epoch in range(start_epoch,args.epoch): - print('Epoch %d (%d/%s):' % (global_epoch + 1, epoch + 1, args.epoch)) - logger.info('Epoch %d (%d/%s):' ,global_epoch + 1, epoch + 1, args.epoch) - - scheduler.step() - for batch_id, data in tqdm(enumerate(trainDataLoader, 0), total=len(trainDataLoader), smoothing=0.9): - points, target = data - target = target[:, 0] - points = points.transpose(2, 1) - points, target = points.cuda(), target.cuda() - optimizer.zero_grad() - classifier = classifier.train() - pred, trans_feat = classifier(points) - loss = F.nll_loss(pred, target.long()) - if args.feature_transform and args.model_name == 'pointnet': - loss += feature_transform_reguliarzer(trans_feat) * 0.001 - loss.backward() - optimizer.step() - global_step += 1 - - train_acc = test(classifier.eval(), trainDataLoader) if args.train_metric else None - acc = test(classifier, testDataLoader) - - - print('\r Loss: %f' % loss.data) - logger.info('Loss: %.2f', loss.data) - if args.train_metric: - print('Train Accuracy: %f' % train_acc) - logger.info('Train Accuracy: %f', (train_acc)) - print('\r Test %s: %f' % (blue('Accuracy'),acc)) - logger.info('Test Accuracy: %f', acc) - - if (acc >= best_tst_accuracy) and epoch > 5: - best_tst_accuracy = acc - logger.info('Save model...') - save_checkpoint( - global_epoch + 1, - train_acc if args.train_metric else 0.0, - acc, - classifier, - optimizer, - str(checkpoints_dir), - args.model_name) - print('Saving model....') - global_epoch += 1 - print('Best Accuracy: %f'%best_tst_accuracy) - - logger.info('End of training...') - -if __name__ == '__main__': - args = parse_args() - main(args) diff --git a/train_cls.py b/train_cls.py new file mode 100644 index 000000000..d9f9b8613 --- /dev/null +++ b/train_cls.py @@ -0,0 +1,209 @@ +""" +Author: Benny +Date: Nov 2019 +""" +from data_utils.ModelNetDataLoader import ModelNetDataLoader +import argparse +import numpy as np +import os +import torch +import datetime +import logging +from pathlib import Path +from tqdm import tqdm +import sys +import provider +import importlib +import shutil + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +ROOT_DIR = BASE_DIR +sys.path.append(os.path.join(ROOT_DIR, 'models')) + + +def parse_args(): + '''PARAMETERS''' + parser = argparse.ArgumentParser('PointNet') + parser.add_argument('--batch_size', type=int, default=24, help='batch size in training [default: 24]') + parser.add_argument('--model', default='pointnet_cls', help='model name [default: pointnet_cls]') + parser.add_argument('--epoch', default=200, type=int, help='number of epoch in training [default: 200]') + parser.add_argument('--learning_rate', default=0.001, type=float, help='learning rate in training [default: 0.001]') + parser.add_argument('--gpu', type=str, default='0', help='specify gpu device [default: 0]') + parser.add_argument('--num_point', type=int, default=1024, help='Point Number [default: 1024]') + parser.add_argument('--optimizer', type=str, default='Adam', help='optimizer for training [default: Adam]') + parser.add_argument('--log_dir', type=str, default=None, help='experiment root') + parser.add_argument('--decay_rate', type=float, default=1e-4, help='decay rate [default: 1e-4]') + parser.add_argument('--normal', action='store_true', default=False, help='Whether to use normal information [default: False]') + return parser.parse_args() + +def test(model, loader, num_class=40): + mean_correct = [] + class_acc = np.zeros((num_class,3)) + for j, data in tqdm(enumerate(loader), total=len(loader)): + points, target = data + target = target[:, 0] + points = points.transpose(2, 1) + points, target = points.cuda(), target.cuda() + classifier = model.eval() + pred, _ = classifier(points) + pred_choice = pred.data.max(1)[1] + for cat in np.unique(target.cpu()): + classacc = pred_choice[target==cat].eq(target[target==cat].long().data).cpu().sum() + class_acc[cat,0]+= classacc.item()/float(points[target==cat].size()[0]) + class_acc[cat,1]+=1 + correct = pred_choice.eq(target.long().data).cpu().sum() + mean_correct.append(correct.item()/float(points.size()[0])) + class_acc[:,2] = class_acc[:,0]/ class_acc[:,1] + class_acc = np.mean(class_acc[:,2]) + instance_acc = np.mean(mean_correct) + return instance_acc, class_acc + + +def main(args): + def log_string(str): + logger.info(str) + print(str) + + '''HYPER PARAMETER''' + os.environ["CUDA_VISIBLE_DEVICES"] = args.gpu + + '''CREATE DIR''' + timestr = str(datetime.datetime.now().strftime('%Y-%m-%d_%H-%M')) + experiment_dir = Path('./log/') + experiment_dir.mkdir(exist_ok=True) + experiment_dir = experiment_dir.joinpath('classification') + experiment_dir.mkdir(exist_ok=True) + if args.log_dir is None: + experiment_dir = experiment_dir.joinpath(timestr) + else: + experiment_dir = experiment_dir.joinpath(args.log_dir) + experiment_dir.mkdir(exist_ok=True) + checkpoints_dir = experiment_dir.joinpath('checkpoints/') + checkpoints_dir.mkdir(exist_ok=True) + log_dir = experiment_dir.joinpath('logs/') + log_dir.mkdir(exist_ok=True) + + '''LOG''' + args = parse_args() + logger = logging.getLogger("Model") + logger.setLevel(logging.INFO) + formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') + file_handler = logging.FileHandler('%s/%s.txt' % (log_dir, args.model)) + file_handler.setLevel(logging.INFO) + file_handler.setFormatter(formatter) + logger.addHandler(file_handler) + log_string('PARAMETER ...') + log_string(args) + + '''DATA LOADING''' + log_string('Load dataset ...') + DATA_PATH = 'data/modelnet40_normal_resampled/' + + TRAIN_DATASET = ModelNetDataLoader(root=DATA_PATH, npoint=args.num_point, split='train', + normal_channel=args.normal) + TEST_DATASET = ModelNetDataLoader(root=DATA_PATH, npoint=args.num_point, split='test', + normal_channel=args.normal) + trainDataLoader = torch.utils.data.DataLoader(TRAIN_DATASET, batch_size=args.batch_size, shuffle=True, num_workers=4) + testDataLoader = torch.utils.data.DataLoader(TEST_DATASET, batch_size=args.batch_size, shuffle=False, num_workers=4) + + '''MODEL LOADING''' + num_class = 40 + MODEL = importlib.import_module(args.model) + shutil.copy('./models/%s.py' % args.model, str(experiment_dir)) + shutil.copy('./models/pointnet_util.py', str(experiment_dir)) + + classifier = MODEL.get_model(num_class,normal_channel=args.normal).cuda() + criterion = MODEL.get_loss().cuda() + + try: + checkpoint = torch.load(str(experiment_dir) + '/checkpoints/best_model.pth') + start_epoch = checkpoint['epoch'] + classifier.load_state_dict(checkpoint['model_state_dict']) + log_string('Use pretrain model') + except: + log_string('No existing model, starting training from scratch...') + start_epoch = 0 + + + if args.optimizer == 'Adam': + optimizer = torch.optim.Adam( + classifier.parameters(), + lr=args.learning_rate, + betas=(0.9, 0.999), + eps=1e-08, + weight_decay=args.decay_rate + ) + else: + optimizer = torch.optim.SGD(classifier.parameters(), lr=0.01, momentum=0.9) + + scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=20, gamma=0.7) + global_epoch = 0 + global_step = 0 + best_instance_acc = 0.0 + best_class_acc = 0.0 + mean_correct = [] + + '''TRANING''' + logger.info('Start training...') + for epoch in range(start_epoch,args.epoch): + log_string('Epoch %d (%d/%s):' % (global_epoch + 1, epoch + 1, args.epoch)) + + scheduler.step() + for batch_id, data in tqdm(enumerate(trainDataLoader, 0), total=len(trainDataLoader), smoothing=0.9): + points, target = data + points = points.data.numpy() + points = provider.random_point_dropout(points) + points[:,:, 0:3] = provider.random_scale_point_cloud(points[:,:, 0:3]) + points[:,:, 0:3] = provider.shift_point_cloud(points[:,:, 0:3]) + points = torch.Tensor(points) + target = target[:, 0] + + points = points.transpose(2, 1) + points, target = points.cuda(), target.cuda() + optimizer.zero_grad() + + classifier = classifier.train() + pred, trans_feat = classifier(points) + loss = criterion(pred, target.long(), trans_feat) + pred_choice = pred.data.max(1)[1] + correct = pred_choice.eq(target.long().data).cpu().sum() + mean_correct.append(correct.item() / float(points.size()[0])) + loss.backward() + optimizer.step() + global_step += 1 + + train_instance_acc = np.mean(mean_correct) + log_string('Train Instance Accuracy: %f' % train_instance_acc) + + + with torch.no_grad(): + instance_acc, class_acc = test(classifier.eval(), testDataLoader) + + if (instance_acc >= best_instance_acc): + best_instance_acc = instance_acc + best_epoch = epoch + 1 + + if (class_acc >= best_class_acc): + best_class_acc = class_acc + log_string('Test Instance Accuracy: %f, Class Accuracy: %f'% (instance_acc, class_acc)) + log_string('Best Instance Accuracy: %f, Class Accuracy: %f'% (best_instance_acc, best_class_acc)) + + if (instance_acc >= best_instance_acc): + logger.info('Save model...') + savepath = str(checkpoints_dir) + '/best_model.pth' + log_string('Saving at %s'% savepath) + state = { + 'epoch': best_epoch, + 'instance_acc': instance_acc, + 'class_acc': class_acc, + 'model_state_dict': classifier.state_dict(), + 'optimizer_state_dict': optimizer.state_dict(), + } + torch.save(state, savepath) + global_epoch += 1 + + logger.info('End of training...') + +if __name__ == '__main__': + args = parse_args() + main(args) diff --git a/train_partseg.py b/train_partseg.py index 34edbd56e..00ab242f4 100644 --- a/train_partseg.py +++ b/train_partseg.py @@ -1,20 +1,24 @@ +""" +Author: Benny +Date: Nov 2019 +""" import argparse import os -import torch -import torch.nn.parallel -import torch.utils.data -from utils import to_categorical -from collections import defaultdict -from torch.autograd import Variable from data_utils.ShapeNetDataLoader import PartNormalDataset -import torch.nn.functional as F +import torch import datetime import logging from pathlib import Path -from utils import test_partseg +import sys +import importlib +import shutil from tqdm import tqdm -from model.pointnet2 import PointNet2PartSeg_msg_one_hot -from model.pointnet import PointNetDenseCls,PointNetLoss +import provider +import numpy as np + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +ROOT_DIR = BASE_DIR +sys.path.append(os.path.join(ROOT_DIR, 'models')) seg_classes = {'Earphone': [16, 17, 18], 'Motorbike': [30, 31, 32, 33, 34, 35], 'Rocket': [41, 42, 43], 'Car': [8, 9, 10, 11], 'Laptop': [28, 29], 'Cap': [6, 7], 'Skateboard': [44, 45, 46], 'Mug': [36, 37], 'Guitar': [19, 20, 21], 'Bag': [4, 5], 'Lamp': [24, 25, 26, 27], 'Table': [47, 48, 49], 'Airplane': [0, 1, 2, 3], 'Pistol': [38, 39, 40], 'Chair': [12, 13, 14, 15], 'Knife': [22, 23]} seg_label_to_cat = {} # {0:Airplane, 1:Airplane, ...49:Table} @@ -22,155 +26,258 @@ for label in seg_classes[cat]: seg_label_to_cat[label] = cat +def to_categorical(y, num_classes): + """ 1-hot encodes a tensor """ + new_y = torch.eye(num_classes)[y.cpu().data.numpy(),] + if (y.is_cuda): + return new_y.cuda() + return new_y + + def parse_args(): - parser = argparse.ArgumentParser('PointNet2') - parser.add_argument('--batchsize', type=int, default=32, help='input batch size') - parser.add_argument('--workers', type=int, default=4, help='number of data loading workers') - parser.add_argument('--epoch', type=int, default=201, help='number of epochs for training') - parser.add_argument('--pretrain', type=str, default=None,help='whether use pretrain model') - parser.add_argument('--gpu', type=str, default='0', help='specify gpu device') - parser.add_argument('--model_name', type=str, default='pointnet2', help='Name of model') - parser.add_argument('--learning_rate', type=float, default=0.001, help='learning rate for training') - parser.add_argument('--decay_rate', type=float, default=1e-4, help='weight decay') - parser.add_argument('--optimizer', type=str, default='Adam', help='type of optimizer') - parser.add_argument('--multi_gpu', type=str, default=None, help='whether use multi gpu training') - parser.add_argument('--jitter', default=False, help="randomly jitter point cloud") - parser.add_argument('--step_size', type=int, default=20, help="randomly rotate point cloud") + parser = argparse.ArgumentParser('Model') + parser.add_argument('--model', type=str, default='pointnet2_part_seg_msg', help='model name [default: pointnet2_part_seg_msg]') + parser.add_argument('--batch_size', type=int, default=16, help='Batch Size during training [default: 16]') + parser.add_argument('--epoch', default=251, type=int, help='Epoch to run [default: 251]') + parser.add_argument('--learning_rate', default=0.001, type=float, help='Initial learning rate [default: 0.001]') + parser.add_argument('--gpu', type=str, default='0', help='GPU to use [default: GPU 0]') + parser.add_argument('--optimizer', type=str, default='Adam', help='Adam or SGD [default: Adam]') + parser.add_argument('--log_dir', type=str, default=None, help='Log path [default: None]') + parser.add_argument('--decay_rate', type=float, default=1e-4, help='weight decay [default: 1e-4]') + parser.add_argument('--npoint', type=int, default=2048, help='Point Number [default: 2048]') + parser.add_argument('--normal', action='store_true', default=False, help='Whether to use normal information [default: True]') + parser.add_argument('--step_size', type=int, default=20, help='Decay step for lr decay [default: every 20 epochs]') + parser.add_argument('--lr_decay', type=float, default=0.5, help='Decay rate for lr decay [default: 0.5]') return parser.parse_args() def main(args): - os.environ["CUDA_VISIBLE_DEVICES"] = args.gpu if args.multi_gpu is None else '0,1,2,3' + def log_string(str): + logger.info(str) + print(str) + + '''HYPER PARAMETER''' + os.environ["CUDA_VISIBLE_DEVICES"] = args.gpu + '''CREATE DIR''' - experiment_dir = Path('./experiment/') + timestr = str(datetime.datetime.now().strftime('%Y-%m-%d_%H-%M')) + experiment_dir = Path('./log/') + experiment_dir.mkdir(exist_ok=True) + experiment_dir = experiment_dir.joinpath('part_seg') + experiment_dir.mkdir(exist_ok=True) + if args.log_dir is None: + experiment_dir = experiment_dir.joinpath(timestr) + else: + experiment_dir = experiment_dir.joinpath(args.log_dir) experiment_dir.mkdir(exist_ok=True) - file_dir = Path(str(experiment_dir) +'/%sPartSeg-'%args.model_name + str(datetime.datetime.now().strftime('%Y-%m-%d_%H-%M'))) - file_dir.mkdir(exist_ok=True) - checkpoints_dir = file_dir.joinpath('checkpoints/') + checkpoints_dir = experiment_dir.joinpath('checkpoints/') checkpoints_dir.mkdir(exist_ok=True) - log_dir = file_dir.joinpath('logs/') + log_dir = experiment_dir.joinpath('logs/') log_dir.mkdir(exist_ok=True) '''LOG''' args = parse_args() - logger = logging.getLogger(args.model_name) + logger = logging.getLogger("Model") logger.setLevel(logging.INFO) formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') - file_handler = logging.FileHandler(str(log_dir) + '/train_%s_partseg.txt'%args.model_name) + file_handler = logging.FileHandler('%s/%s.txt' % (log_dir, args.model)) file_handler.setLevel(logging.INFO) file_handler.setFormatter(formatter) logger.addHandler(file_handler) - logger.info('---------------------------------------------------TRANING---------------------------------------------------') - logger.info('PARAMETER ...') - logger.info(args) - norm = True if args.model_name == 'pointnet' else False - TRAIN_DATASET = PartNormalDataset(npoints=2048, split='trainval',normalize=norm, jitter=args.jitter) - dataloader = torch.utils.data.DataLoader(TRAIN_DATASET, batch_size=args.batchsize,shuffle=True, num_workers=int(args.workers)) - TEST_DATASET = PartNormalDataset(npoints=2048, split='test',normalize=norm,jitter=False) - testdataloader = torch.utils.data.DataLoader(TEST_DATASET, batch_size=10,shuffle=True, num_workers=int(args.workers)) - print("The number of training data is:",len(TRAIN_DATASET)) - logger.info("The number of training data is:%d",len(TRAIN_DATASET)) - print("The number of test data is:", len(TEST_DATASET)) - logger.info("The number of test data is:%d", len(TEST_DATASET)) + log_string('PARAMETER ...') + log_string(args) + + root = 'data/shapenetcore_partanno_segmentation_benchmark_v0_normal/' + + TRAIN_DATASET = PartNormalDataset(root = root, npoints=args.npoint, split='trainval', normal_channel=args.normal) + trainDataLoader = torch.utils.data.DataLoader(TRAIN_DATASET, batch_size=args.batch_size,shuffle=True, num_workers=4) + TEST_DATASET = PartNormalDataset(root = root, npoints=args.npoint, split='test', normal_channel=args.normal) + testDataLoader = torch.utils.data.DataLoader(TEST_DATASET, batch_size=args.batch_size,shuffle=False, num_workers=4) + log_string("The number of training data is: %d" % len(TRAIN_DATASET)) + log_string("The number of test data is: %d" % len(TEST_DATASET)) num_classes = 16 num_part = 50 - blue = lambda x: '\033[94m' + x + '\033[0m' - model = PointNet2PartSeg_msg_one_hot(num_part) if args.model_name == 'pointnet2'else PointNetDenseCls(cat_num=num_classes,part_num=num_part) + '''MODEL LOADING''' + MODEL = importlib.import_module(args.model) + shutil.copy('models/%s.py' % args.model, str(experiment_dir)) + shutil.copy('models/pointnet_util.py', str(experiment_dir)) - if args.pretrain is not None: - model.load_state_dict(torch.load(args.pretrain)) - print('load model %s'%args.pretrain) - logger.info('load model %s'%args.pretrain) - else: - print('Training from scratch') - logger.info('Training from scratch') - pretrain = args.pretrain - init_epoch = int(pretrain[-14:-11]) if args.pretrain is not None else 0 + classifier = MODEL.get_model(num_part, normal_channel=args.normal).cuda() + criterion = MODEL.get_loss().cuda() + + + def weights_init(m): + classname = m.__class__.__name__ + if classname.find('Conv2d') != -1: + torch.nn.init.xavier_normal_(m.weight.data) + torch.nn.init.constant_(m.bias.data, 0.0) + elif classname.find('Linear') != -1: + torch.nn.init.xavier_normal_(m.weight.data) + torch.nn.init.constant_(m.bias.data, 0.0) + try: + checkpoint = torch.load(str(experiment_dir) + '/checkpoints/best_model.pth') + start_epoch = checkpoint['epoch'] + classifier.load_state_dict(checkpoint['model_state_dict']) + log_string('Use pretrain model') + except: + log_string('No existing model, starting training from scratch...') + start_epoch = 0 + classifier = classifier.apply(weights_init) - if args.optimizer == 'SGD': - optimizer = torch.optim.SGD(model.parameters(), lr=0.01, momentum=0.9) - elif args.optimizer == 'Adam': + if args.optimizer == 'Adam': optimizer = torch.optim.Adam( - model.parameters(), + classifier.parameters(), lr=args.learning_rate, betas=(0.9, 0.999), eps=1e-08, weight_decay=args.decay_rate ) - scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=args.step_size, gamma=0.5) - - '''GPU selection and multi-GPU''' - if args.multi_gpu is not None: - device_ids = [int(x) for x in args.multi_gpu.split(',')] - torch.backends.cudnn.benchmark = True - model.cuda(device_ids[0]) - model = torch.nn.DataParallel(model, device_ids=device_ids) else: - model.cuda() - criterion = PointNetLoss() + optimizer = torch.optim.SGD(classifier.parameters(), lr=args.learning_rate, momentum=0.9) + + def bn_momentum_adjust(m, momentum): + if isinstance(m, torch.nn.BatchNorm2d) or isinstance(m, torch.nn.BatchNorm1d): + m.momentum = momentum + LEARNING_RATE_CLIP = 1e-5 + MOMENTUM_ORIGINAL = 0.1 + MOMENTUM_DECCAY = 0.5 + MOMENTUM_DECCAY_STEP = args.step_size - history = defaultdict(lambda: list()) best_acc = 0 + global_epoch = 0 best_class_avg_iou = 0 best_inctance_avg_iou = 0 - for epoch in range(init_epoch,args.epoch): - scheduler.step() - lr = max(optimizer.param_groups[0]['lr'],LEARNING_RATE_CLIP) - print('Learning rate:%f' % lr) + for epoch in range(start_epoch,args.epoch): + log_string('Epoch %d (%d/%s):' % (global_epoch + 1, epoch + 1, args.epoch)) + '''Adjust learning rate and BN momentum''' + lr = max(args.learning_rate * (args.lr_decay ** (epoch // args.step_size)), LEARNING_RATE_CLIP) + log_string('Learning rate:%f' % lr) for param_group in optimizer.param_groups: param_group['lr'] = lr - for i, data in tqdm(enumerate(dataloader, 0),total=len(dataloader),smoothing=0.9): - points, label, target, norm_plt = data - points, label, target = Variable(points.float()),Variable(label.long()), Variable(target.long()) + mean_correct = [] + momentum = MOMENTUM_ORIGINAL * (MOMENTUM_DECCAY ** (epoch // MOMENTUM_DECCAY_STEP)) + if momentum < 0.01: + momentum = 0.01 + print('BN momentum updated to: %f' % momentum) + classifier = classifier.apply(lambda x: bn_momentum_adjust(x,momentum)) + + '''learning one epoch''' + for i, data in tqdm(enumerate(trainDataLoader), total=len(trainDataLoader), smoothing=0.9): + points, label, target = data + points = points.data.numpy() + points[:,:, 0:3] = provider.random_scale_point_cloud(points[:,:, 0:3]) + points[:,:, 0:3] = provider.shift_point_cloud(points[:,:, 0:3]) + points = torch.Tensor(points) + points, label, target = points.float().cuda(),label.long().cuda(), target.long().cuda() points = points.transpose(2, 1) - norm_plt = norm_plt.transpose(2, 1) - points, label, target,norm_plt = points.cuda(),label.squeeze().cuda(), target.cuda(), norm_plt.cuda() optimizer.zero_grad() - model = model.train() - if args.model_name == 'pointnet': - labels_pred, seg_pred, trans_feat = model(points, to_categorical(label, 16)) - seg_pred = seg_pred.contiguous().view(-1, num_part) - target = target.view(-1, 1)[:, 0] - loss, seg_loss, label_loss = criterion(labels_pred, label, seg_pred, target, trans_feat) - else: - seg_pred = model(points, norm_plt, to_categorical(label, 16)) - seg_pred = seg_pred.contiguous().view(-1, num_part) - target = target.view(-1, 1)[:, 0] - loss = F.nll_loss(seg_pred, target) - - history['loss'].append(loss.cpu().data.numpy()) + classifier = classifier.train() + seg_pred, trans_feat = classifier(points, to_categorical(label, num_classes)) + seg_pred = seg_pred.contiguous().view(-1, num_part) + target = target.view(-1, 1)[:, 0] + pred_choice = seg_pred.data.max(1)[1] + correct = pred_choice.eq(target.data).cpu().sum() + mean_correct.append(correct.item() / (args.batch_size * args.npoint)) + loss = criterion(seg_pred, target, trans_feat) loss.backward() optimizer.step() + train_instance_acc = np.mean(mean_correct) + log_string('Train accuracy is: %.5f' % train_instance_acc) - forpointnet2 = args.model_name == 'pointnet2' - test_metrics, test_hist_acc, cat_mean_iou = test_partseg(model.eval(), testdataloader, seg_label_to_cat,50,forpointnet2) + with torch.no_grad(): + test_metrics = {} + total_correct = 0 + total_seen = 0 + total_seen_class = [0 for _ in range(num_part)] + total_correct_class = [0 for _ in range(num_part)] + shape_ious = {cat: [] for cat in seg_classes.keys()} + seg_label_to_cat = {} # {0:Airplane, 1:Airplane, ...49:Table} + for cat in seg_classes.keys(): + for label in seg_classes[cat]: + seg_label_to_cat[label] = cat - print('Epoch %d %s accuracy: %f Class avg mIOU: %f Inctance avg mIOU: %f' % ( - epoch, blue('test'), test_metrics['accuracy'],test_metrics['class_avg_iou'],test_metrics['inctance_avg_iou'])) + for batch_id, (points, label, target) in tqdm(enumerate(testDataLoader), total=len(testDataLoader), smoothing=0.9): + cur_batch_size, NUM_POINT, _ = points.size() + points, label, target = points.float().cuda(), label.long().cuda(), target.long().cuda() + points = points.transpose(2, 1) + classifier = classifier.eval() + seg_pred, _ = classifier(points, to_categorical(label, num_classes)) + cur_pred_val = seg_pred.cpu().data.numpy() + cur_pred_val_logits = cur_pred_val + cur_pred_val = np.zeros((cur_batch_size, NUM_POINT)).astype(np.int32) + target = target.cpu().data.numpy() + for i in range(cur_batch_size): + cat = seg_label_to_cat[target[i, 0]] + logits = cur_pred_val_logits[i, :, :] + cur_pred_val[i, :] = np.argmax(logits[:, seg_classes[cat]], 1) + seg_classes[cat][0] + correct = np.sum(cur_pred_val == target) + total_correct += correct + total_seen += (cur_batch_size * NUM_POINT) + + for l in range(num_part): + total_seen_class[l] += np.sum(target == l) + total_correct_class[l] += (np.sum((cur_pred_val == l) & (target == l))) + + for i in range(cur_batch_size): + segp = cur_pred_val[i, :] + segl = target[i, :] + cat = seg_label_to_cat[segl[0]] + part_ious = [0.0 for _ in range(len(seg_classes[cat]))] + for l in seg_classes[cat]: + if (np.sum(segl == l) == 0) and ( + np.sum(segp == l) == 0): # part is not present, no prediction as well + part_ious[l - seg_classes[cat][0]] = 1.0 + else: + part_ious[l - seg_classes[cat][0]] = np.sum((segl == l) & (segp == l)) / float( + np.sum((segl == l) | (segp == l))) + shape_ious[cat].append(np.mean(part_ious)) + + all_shape_ious = [] + for cat in shape_ious.keys(): + for iou in shape_ious[cat]: + all_shape_ious.append(iou) + shape_ious[cat] = np.mean(shape_ious[cat]) + mean_shape_ious = np.mean(list(shape_ious.values())) + test_metrics['accuracy'] = total_correct / float(total_seen) + test_metrics['class_avg_accuracy'] = np.mean( + np.array(total_correct_class) / np.array(total_seen_class, dtype=np.float)) + for cat in sorted(shape_ious.keys()): + log_string('eval mIoU of %s %f' % (cat + ' ' * (14 - len(cat)), shape_ious[cat])) + test_metrics['class_avg_iou'] = mean_shape_ious + test_metrics['inctance_avg_iou'] = np.mean(all_shape_ious) + + + log_string('Epoch %d test Accuracy: %f Class avg mIOU: %f Inctance avg mIOU: %f' % ( + epoch+1, test_metrics['accuracy'],test_metrics['class_avg_iou'],test_metrics['inctance_avg_iou'])) + if (test_metrics['inctance_avg_iou'] >= best_inctance_avg_iou): + logger.info('Save model...') + savepath = str(checkpoints_dir) + '/best_model.pth' + log_string('Saving at %s'% savepath) + state = { + 'epoch': epoch, + 'train_acc': train_instance_acc, + 'test_acc': test_metrics['accuracy'], + 'class_avg_iou': test_metrics['class_avg_iou'], + 'inctance_avg_iou': test_metrics['inctance_avg_iou'], + 'model_state_dict': classifier.state_dict(), + 'optimizer_state_dict': optimizer.state_dict(), + } + torch.save(state, savepath) + log_string('Saving model....') - logger.info('Epoch %d %s Accuracy: %f Class avg mIOU: %f Inctance avg mIOU: %f' % ( - epoch, blue('test'), test_metrics['accuracy'],test_metrics['class_avg_iou'],test_metrics['inctance_avg_iou'])) if test_metrics['accuracy'] > best_acc: best_acc = test_metrics['accuracy'] - torch.save(model.state_dict(), '%s/%s_%.3d_%.4f.pth' % (checkpoints_dir,args.model_name, epoch, best_acc)) - logger.info(cat_mean_iou) - logger.info('Save model..') - print('Save model..') - print(cat_mean_iou) if test_metrics['class_avg_iou'] > best_class_avg_iou: best_class_avg_iou = test_metrics['class_avg_iou'] if test_metrics['inctance_avg_iou'] > best_inctance_avg_iou: best_inctance_avg_iou = test_metrics['inctance_avg_iou'] - print('Best accuracy is: %.5f'%best_acc) - logger.info('Best accuracy is: %.5f'%best_acc) - print('Best class avg mIOU is: %.5f'%best_class_avg_iou) - logger.info('Best class avg mIOU is: %.5f'%best_class_avg_iou) - print('Best inctance avg mIOU is: %.5f'%best_inctance_avg_iou) - logger.info('Best inctance avg mIOU is: %.5f'%best_inctance_avg_iou) - + log_string('Best accuracy is: %.5f'%best_acc) + log_string('Best class avg mIOU is: %.5f'%best_class_avg_iou) + log_string('Best inctance avg mIOU is: %.5f'%best_inctance_avg_iou) + global_epoch+=1 if __name__ == '__main__': args = parse_args() diff --git a/train_semseg.py b/train_semseg.py index 3b2a3b5df..777192053 100644 --- a/train_semseg.py +++ b/train_semseg.py @@ -1,162 +1,286 @@ +""" +Author: Benny +Date: Nov 2019 +""" import argparse import os +from data_utils.S3DISDataLoader import S3DISDataset, S3DISDatasetWholeScene import torch -import numpy as np -import torch.nn.parallel -import torch.utils.data -from collections import defaultdict -from torch.autograd import Variable -from data_utils.S3DISDataLoader import S3DISDataLoader, recognize_all_data,class2label -import torch.nn.functional as F import datetime import logging from pathlib import Path -from utils import test_semseg +import sys +import importlib +import shutil from tqdm import tqdm -from model.pointnet2 import PointNet2SemSeg -from model.pointnet import PointNetSeg, feature_transform_reguliarzer +import provider +import numpy as np + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +ROOT_DIR = BASE_DIR +sys.path.append(os.path.join(ROOT_DIR, 'models')) + +seg_classes = {'Earphone': [16, 17, 18], 'Motorbike': [30, 31, 32, 33, 34, 35], 'Rocket': [41, 42, 43], 'Car': [8, 9, 10, 11], 'Laptop': [28, 29], 'Cap': [6, 7], 'Skateboard': [44, 45, 46], 'Mug': [36, 37], 'Guitar': [19, 20, 21], 'Bag': [4, 5], 'Lamp': [24, 25, 26, 27], 'Table': [47, 48, 49], 'Airplane': [0, 1, 2, 3], 'Pistol': [38, 39, 40], 'Chair': [12, 13, 14, 15], 'Knife': [22, 23]} +seg_label_to_cat = {} # {0:Airplane, 1:Airplane, ...49:Table} +for cat in seg_classes.keys(): + for label in seg_classes[cat]: + seg_label_to_cat[label] = cat -seg_classes = class2label -seg_label_to_cat = {} -for i,cat in enumerate(seg_classes.keys()): - seg_label_to_cat[i] = cat def parse_args(): - parser = argparse.ArgumentParser('PointNet') - parser.add_argument('--batchsize', type=int, default=12, help='input batch size') - parser.add_argument('--workers', type=int, default=4, help='number of data loading workers') - parser.add_argument('--epoch', type=int, default=201, help='number of epochs for training') - parser.add_argument('--pretrain', type=str, default=None,help='whether use pretrain model') - parser.add_argument('--gpu', type=str, default='0', help='specify gpu device') - parser.add_argument('--learning_rate', type=float, default=0.001, help='learning rate for training') - parser.add_argument('--decay_rate', type=float, default=1e-4, help='weight decay') - parser.add_argument('--optimizer', type=str, default='Adam', help='type of optimizer') - parser.add_argument('--multi_gpu', type=str, default=None, help='whether use multi gpu training') - parser.add_argument('--model_name', type=str, default='pointnet2', help='Name of model') + parser = argparse.ArgumentParser('Model') + parser.add_argument('--model', type=str, default='pointnet_sem_seg', help='model name [default: pointnet_sem_seg]') + parser.add_argument('--batch_size', type=int, default=12, help='Batch Size during training [default: 12]') + parser.add_argument('--epoch', default=1024, type=int, help='Epoch to run [default: 251]') + parser.add_argument('--learning_rate', default=0.001, type=float, help='Initial learning rate [default: 0.001]') + parser.add_argument('--gpu', type=str, default='0', help='GPU to use [default: GPU 0]') + parser.add_argument('--optimizer', type=str, default='Adam', help='Adam or SGD [default: Adam]') + parser.add_argument('--log_dir', type=str, default=None, help='Log path [default: None]') + parser.add_argument('--decay_rate', type=float, default=1e-4, help='weight decay [default: 1e-4]') + parser.add_argument('--npoint', type=int, default=8192, help='Point Number [default: 2048]') + parser.add_argument('--with_rgb', action='store_true', default=False, help='Whether to use RGB information [default: False]') + parser.add_argument('--step_size', type=int, default=200, help='Decay step for lr decay [default: every 200 epochs]') + parser.add_argument('--lr_decay', type=float, default=0.7, help='Decay rate for lr decay [default: 0.7]') + parser.add_argument('--test_area', type=int, default=5, help='Which area to use for test, option: 1-6 [default: 5]') return parser.parse_args() def main(args): - os.environ["CUDA_VISIBLE_DEVICES"] = args.gpu if args.multi_gpu is None else '0,1,2,3' + def log_string(str): + logger.info(str) + print(str) + + '''HYPER PARAMETER''' + os.environ["CUDA_VISIBLE_DEVICES"] = args.gpu + '''CREATE DIR''' - experiment_dir = Path('./experiment/') + timestr = str(datetime.datetime.now().strftime('%Y-%m-%d_%H-%M')) + experiment_dir = Path('./log/') + experiment_dir.mkdir(exist_ok=True) + experiment_dir = experiment_dir.joinpath('sem_seg') + experiment_dir.mkdir(exist_ok=True) + if args.log_dir is None: + experiment_dir = experiment_dir.joinpath(timestr) + else: + experiment_dir = experiment_dir.joinpath(args.log_dir) experiment_dir.mkdir(exist_ok=True) - file_dir = Path(str(experiment_dir) +'/%sSemSeg-'%args.model_name+ str(datetime.datetime.now().strftime('%Y-%m-%d_%H-%M'))) - file_dir.mkdir(exist_ok=True) - checkpoints_dir = file_dir.joinpath('checkpoints/') + checkpoints_dir = experiment_dir.joinpath('checkpoints/') checkpoints_dir.mkdir(exist_ok=True) - log_dir = file_dir.joinpath('logs/') + log_dir = experiment_dir.joinpath('logs/') log_dir.mkdir(exist_ok=True) '''LOG''' args = parse_args() - logger = logging.getLogger(args.model_name) + logger = logging.getLogger("Model") logger.setLevel(logging.INFO) formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') - file_handler = logging.FileHandler(str(log_dir) + '/train_%s_semseg.txt'%args.model_name) + file_handler = logging.FileHandler('%s/%s.txt' % (log_dir, args.model)) file_handler.setLevel(logging.INFO) file_handler.setFormatter(formatter) logger.addHandler(file_handler) - logger.info('---------------------------------------------------TRANING---------------------------------------------------') - logger.info('PARAMETER ...') - logger.info(args) - print('Load data...') - train_data, train_label, test_data, test_label = recognize_all_data(test_area = 5) - - dataset = S3DISDataLoader(train_data,train_label) - dataloader = torch.utils.data.DataLoader(dataset, batch_size=args.batchsize, - shuffle=True, num_workers=int(args.workers)) - test_dataset = S3DISDataLoader(test_data,test_label) - testdataloader = torch.utils.data.DataLoader(test_dataset, batch_size=args.batchsize, - shuffle=True, num_workers=int(args.workers)) - - num_classes = 13 - blue = lambda x: '\033[94m' + x + '\033[0m' - model = PointNet2SemSeg(num_classes) if args.model_name == 'pointnet2' else PointNetSeg(num_classes,feature_transform=True,semseg = True) - - if args.pretrain is not None: - model.load_state_dict(torch.load(args.pretrain)) - print('load model %s'%args.pretrain) - logger.info('load model %s'%args.pretrain) - else: - print('Training from scratch') - logger.info('Training from scratch') - pretrain = args.pretrain - init_epoch = int(pretrain[-14:-11]) if args.pretrain is not None else 0 - - if args.optimizer == 'SGD': - optimizer = torch.optim.SGD(model.parameters(), lr=0.01, momentum=0.9) - elif args.optimizer == 'Adam': + log_string('PARAMETER ...') + log_string(args) + + root = 'data/stanford_indoor3d/' + NUM_CLASSES = 13 + NUM_POINT = args.npoint + BATCH_SIZE = args.batch_size + FEATURE_CHANNEL = 3 if args.with_rgb else 0 + + print("start loading training data ...") + TRAIN_DATASET = S3DISDataset(root, split='train', with_rgb=args.with_rgb, test_area=args.test_area, block_points=NUM_POINT) + print("start loading whole scene validation data ...") + TEST_DATASET_WHOLE_SCENE = S3DISDatasetWholeScene(root, split='test', with_rgb=args.with_rgb, test_area=args.test_area, block_points=NUM_POINT) + + trainDataLoader = torch.utils.data.DataLoader(TRAIN_DATASET, batch_size=BATCH_SIZE,shuffle=True, num_workers=4) + + log_string("The number of training data is: %d" % len(TRAIN_DATASET)) + log_string("The number of test data is: %d" % len(TEST_DATASET_WHOLE_SCENE)) + + '''MODEL LOADING''' + MODEL = importlib.import_module(args.model) + shutil.copy('models/%s.py' % args.model, str(experiment_dir)) + shutil.copy('models/pointnet_util.py', str(experiment_dir)) + + classifier = MODEL.get_model(NUM_CLASSES, with_rgb=args.with_rgb).cuda() + criterion = MODEL.get_loss().cuda() + + + def weights_init(m): + classname = m.__class__.__name__ + if classname.find('Conv2d') != -1: + torch.nn.init.xavier_normal_(m.weight.data) + torch.nn.init.constant_(m.bias.data, 0.0) + elif classname.find('Linear') != -1: + torch.nn.init.xavier_normal_(m.weight.data) + torch.nn.init.constant_(m.bias.data, 0.0) + + try: + checkpoint = torch.load(str(experiment_dir) + '/checkpoints/best_model.pth') + start_epoch = checkpoint['epoch'] + classifier.load_state_dict(checkpoint['model_state_dict']) + log_string('Use pretrain model') + except: + log_string('No existing model, starting training from scratch...') + start_epoch = 0 + classifier = classifier.apply(weights_init) + + if args.optimizer == 'Adam': optimizer = torch.optim.Adam( - model.parameters(), + classifier.parameters(), lr=args.learning_rate, betas=(0.9, 0.999), eps=1e-08, weight_decay=args.decay_rate ) - scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=20, gamma=0.5) - LEARNING_RATE_CLIP = 1e-5 - - '''GPU selection and multi-GPU''' - if args.multi_gpu is not None: - device_ids = [int(x) for x in args.multi_gpu.split(',')] - torch.backends.cudnn.benchmark = True - model.cuda(device_ids[0]) - model = torch.nn.DataParallel(model, device_ids=device_ids) else: - model.cuda() + optimizer = torch.optim.SGD(classifier.parameters(), lr=args.learning_rate, momentum=0.9) - history = defaultdict(lambda: list()) - best_acc = 0 - best_meaniou = 0 + def bn_momentum_adjust(m, momentum): + if isinstance(m, torch.nn.BatchNorm2d) or isinstance(m, torch.nn.BatchNorm1d): + m.momentum = momentum - for epoch in range(init_epoch,args.epoch): - scheduler.step() - lr = max(optimizer.param_groups[0]['lr'],LEARNING_RATE_CLIP) - print('Learning rate:%f' % lr) + LEARNING_RATE_CLIP = 1e-5 + MOMENTUM_ORIGINAL = 0.1 + MOMENTUM_DECCAY = 0.5 + MOMENTUM_DECCAY_STEP = args.step_size + + global_epoch = 0 + best_iou = 0 + + for epoch in range(start_epoch,args.epoch): + log_string('Epoch %d (%d/%s):' % (global_epoch + 1, epoch + 1, args.epoch)) + '''Adjust learning rate and BN momentum''' + lr = max(args.learning_rate * (args.lr_decay ** (epoch // args.step_size)), LEARNING_RATE_CLIP) + log_string('Learning rate:%f' % lr) for param_group in optimizer.param_groups: param_group['lr'] = lr - for i, data in tqdm(enumerate(dataloader, 0),total=len(dataloader),smoothing=0.9): - points, target = data - points, target = Variable(points.float()), Variable(target.long()) + mean_correct = [] + momentum = MOMENTUM_ORIGINAL * (MOMENTUM_DECCAY ** (epoch // MOMENTUM_DECCAY_STEP)) + if momentum < 0.01: + momentum = 0.01 + print('BN momentum updated to: %f' % momentum) + classifier = classifier.apply(lambda x: bn_momentum_adjust(x,momentum)) + + '''learning one epoch''' + for i, data in tqdm(enumerate(trainDataLoader), total=len(trainDataLoader), smoothing=0.9): + points, target, weight = data + points = points.data.numpy() + points[:, :, :3] = provider.normalize_data(points[:, :, :3]) + points[:,:, 0:3] = provider.random_scale_point_cloud(points[:,:, 0:3]) + points[:,:, 0:3] = provider.rotate_point_cloud_z(points[:,:, 0:3]) + points = torch.Tensor(points) + points, target = points.float().cuda(),target.long().cuda() points = points.transpose(2, 1) - points, target = points.cuda(), target.cuda() optimizer.zero_grad() - model = model.train() - if args.model_name == 'pointnet': - pred, trans_feat = model(points) - else: - pred = model(points[:,:3,:],points[:,3:,:]) - pred = pred.contiguous().view(-1, num_classes) + classifier = classifier.train() + seg_pred, trans_feat = classifier(points) + seg_pred = seg_pred.contiguous().view(-1, NUM_CLASSES) target = target.view(-1, 1)[:, 0] - loss = F.nll_loss(pred, target) - if args.model_name == 'pointnet': - loss += feature_transform_reguliarzer(trans_feat) * 0.001 - history['loss'].append(loss.cpu().data.numpy()) + pred_choice = seg_pred.data.max(1)[1] + correct = pred_choice.eq(target.data).cpu().sum() + mean_correct.append(correct.item() / (args.batch_size * args.npoint)) + loss = criterion(seg_pred, target, trans_feat) loss.backward() optimizer.step() - pointnet2 = args.model_name == 'pointnet2' - test_metrics, test_hist_acc, cat_mean_iou = test_semseg(model.eval(), testdataloader, seg_label_to_cat,num_classes = num_classes,pointnet2=pointnet2) - mean_iou = np.mean(cat_mean_iou) - print('Epoch %d %s accuracy: %f meanIOU: %f' % ( - epoch, blue('test'), test_metrics['accuracy'],mean_iou)) - logger.info('Epoch %d %s accuracy: %f meanIOU: %f' % ( - epoch, 'test', test_metrics['accuracy'],mean_iou)) - if test_metrics['accuracy'] > best_acc: - best_acc = test_metrics['accuracy'] - torch.save(model.state_dict(), '%s/%s_%.3d_%.4f.pth' % (checkpoints_dir,args.model_name, epoch, best_acc)) - logger.info(cat_mean_iou) - logger.info('Save model..') - print('Save model..') - print(cat_mean_iou) - if mean_iou > best_meaniou: - best_meaniou = mean_iou - print('Best accuracy is: %.5f'%best_acc) - logger.info('Best accuracy is: %.5f'%best_acc) - print('Best meanIOU is: %.5f'%best_meaniou) - logger.info('Best meanIOU is: %.5f'%best_meaniou) + train_instance_acc = np.mean(mean_correct) + log_string('Train accuracy is: %.5f' % train_instance_acc) + + # evaluate on whole scenes, for each block, only sample NUM_POINT points + if epoch % 10 ==0: + with torch.no_grad(): + num_batches = len(TEST_DATASET_WHOLE_SCENE) + + total_correct = 0 + total_seen = 0 + loss_sum = 0 + total_seen_class = [0 for _ in range(NUM_CLASSES)] + total_correct_class = [0 for _ in range(NUM_CLASSES)] + total_iou_deno_class = [0 for _ in range(NUM_CLASSES)] + + labelweights = np.zeros(NUM_CLASSES) + is_continue_batch = False + + extra_batch_data = np.zeros((0, NUM_POINT, 3 + FEATURE_CHANNEL)) + extra_batch_label = np.zeros((0, NUM_POINT)) + extra_batch_smpw = np.zeros((0, NUM_POINT)) + for batch_idx in tqdm(range(num_batches),total=num_batches): + if not is_continue_batch: + batch_data, batch_label, batch_smpw = TEST_DATASET_WHOLE_SCENE[batch_idx] + batch_data = np.concatenate((batch_data, extra_batch_data), axis=0) + batch_label = np.concatenate((batch_label, extra_batch_label), axis=0) + batch_smpw = np.concatenate((batch_smpw, extra_batch_smpw), axis=0) + else: + batch_data_tmp, batch_label_tmp, batch_smpw_tmp = TEST_DATASET_WHOLE_SCENE[batch_idx] + batch_data = np.concatenate((batch_data, batch_data_tmp), axis=0) + batch_label = np.concatenate((batch_label, batch_label_tmp), axis=0) + batch_smpw = np.concatenate((batch_smpw, batch_smpw_tmp), axis=0) + if batch_data.shape[0] < BATCH_SIZE: + is_continue_batch = True + continue + elif batch_data.shape[0] == BATCH_SIZE: + is_continue_batch = False + extra_batch_data = np.zeros((0, NUM_POINT, 3 + FEATURE_CHANNEL)) + extra_batch_label = np.zeros((0, NUM_POINT)) + extra_batch_smpw = np.zeros((0, NUM_POINT)) + else: + is_continue_batch = False + extra_batch_data = batch_data[BATCH_SIZE:, :, :] + extra_batch_label = batch_label[BATCH_SIZE:, :] + extra_batch_smpw = batch_smpw[BATCH_SIZE:, :] + batch_data = batch_data[:BATCH_SIZE, :, :] + batch_label = batch_label[:BATCH_SIZE, :] + batch_smpw = batch_smpw[:BATCH_SIZE, :] + + batch_data[:, :, :3] = provider.normalize_data(batch_data[:, :, :3]) + batch_label = torch.Tensor(batch_label) + batch_data = torch.Tensor(batch_data) + batch_data, batch_label = batch_data.float().cuda(), batch_label.long().cuda() + batch_data = batch_data.transpose(2, 1) + seg_pred, _ = classifier(batch_data) + seg_pred = seg_pred.contiguous() + batch_label = batch_label.cpu().data.numpy() + pred_val = seg_pred.cpu().data.max(2)[1].numpy() + correct = np.sum((pred_val == batch_label) & (batch_smpw > 0)) + total_correct += correct + total_seen += np.sum(batch_smpw > 0) + tmp, _ = np.histogram(batch_label, range(NUM_CLASSES + 1)) + labelweights += tmp + for l in range(NUM_CLASSES): + total_seen_class[l] += np.sum((batch_label == l) & (batch_smpw > 0)) + total_correct_class[l] += np.sum((pred_val == l) & (batch_label == l) & (batch_smpw > 0)) + total_iou_deno_class[l] += np.sum(((pred_val == l) | (batch_label == l)) & (batch_smpw > 0)) + + mIoU = np.mean(np.array(total_correct_class) / (np.array(total_iou_deno_class, dtype=np.float) + 1e-6)) + log_string('eval whole scene mean loss: %f' % (loss_sum / float(num_batches))) + log_string('eval point avg class IoU: %f' % mIoU) + log_string('eval whole scene point accuracy: %f' % (total_correct / float(total_seen))) + log_string('eval whole scene point avg class acc: %f' % ( + np.mean(np.array(total_correct_class) / (np.array(total_seen_class, dtype=np.float) + 1e-6)))) + labelweights = labelweights.astype(np.float32) / np.sum(labelweights.astype(np.float32)) + + iou_per_class_str = '------- IoU --------\n' + for l in range(NUM_CLASSES): + iou_per_class_str += 'class %s weight: %.3f, IoU: %.3f \n' % ( + seg_label_to_cat[l] + ' ' * (14 - len(seg_label_to_cat[l])), labelweights[l], + total_correct_class[l] / float(total_iou_deno_class[l])) + log_string(iou_per_class_str) + + if (mIoU >= best_iou): + logger.info('Save model...') + savepath = str(checkpoints_dir) + '/best_model.pth' + log_string('Saving at %s' % savepath) + state = { + 'epoch': epoch, + 'class_avg_iou': mIoU, + 'model_state_dict': classifier.state_dict(), + 'optimizer_state_dict': optimizer.state_dict(), + } + torch.save(state, savepath) + log_string('Saving model....') if __name__ == '__main__': args = parse_args() main(args) + diff --git a/utils.py b/utils.py deleted file mode 100644 index 87afafc9f..000000000 --- a/utils.py +++ /dev/null @@ -1,229 +0,0 @@ -# *_*coding:utf-8 *_* -import os -import numpy as np -import torch -import matplotlib.pyplot as plt -from torch.autograd import Variable -from tqdm import tqdm -from collections import defaultdict -import datetime -import pandas as pd -import torch.nn.functional as F -def to_categorical(y, num_classes): - """ 1-hot encodes a tensor """ - new_y = torch.eye(num_classes)[y.cpu().data.numpy(),] - if (y.is_cuda): - return new_y.cuda() - return new_y - -def show_example(x, y, x_reconstruction, y_pred,save_dir, figname): - x = x.squeeze().cpu().data.numpy() - x = x.permute(0,2,1) - y = y.cpu().data.numpy() - x_reconstruction = x_reconstruction.squeeze().cpu().data.numpy() - _, y_pred = torch.max(y_pred, -1) - y_pred = y_pred.cpu().data.numpy() - - fig, ax = plt.subplots(1, 2) - ax[0].imshow(x, cmap='Greys') - ax[0].set_title('Input: %d' % y) - ax[1].imshow(x_reconstruction, cmap='Greys') - ax[1].set_title('Output: %d' % y_pred) - plt.savefig(save_dir + figname + '.png') - -def save_checkpoint(epoch, train_accuracy, test_accuracy, model, optimizer, path,modelnet='checkpoint'): - savepath = path + '/%s-%f-%04d.pth' % (modelnet,test_accuracy, epoch) - state = { - 'epoch': epoch, - 'train_accuracy': train_accuracy, - 'test_accuracy': test_accuracy, - 'model_state_dict': model.state_dict(), - 'optimizer_state_dict': optimizer.state_dict(), - } - torch.save(state, savepath) - -def test(model, loader): - mean_correct = [] - for j, data in enumerate(loader, 0): - points, target = data - target = target[:, 0] - points = points.transpose(2, 1) - points, target = points.cuda(), target.cuda() - classifier = model.eval() - pred, _ = classifier(points) - pred_choice = pred.data.max(1)[1] - correct = pred_choice.eq(target.long().data).cpu().sum() - mean_correct.append(correct.item()/float(points.size()[0])) - return np.mean(mean_correct) - -def compute_cat_iou(pred,target,iou_tabel): - iou_list = [] - target = target.cpu().data.numpy() - for j in range(pred.size(0)): - batch_pred = pred[j] - batch_target = target[j] - batch_choice = batch_pred.data.max(1)[1].cpu().data.numpy() - for cat in np.unique(batch_target): - # intersection = np.sum((batch_target == cat) & (batch_choice == cat)) - # union = float(np.sum((batch_target == cat) | (batch_choice == cat))) - # iou = intersection/union if not union ==0 else 1 - I = np.sum(np.logical_and(batch_choice == cat, batch_target == cat)) - U = np.sum(np.logical_or(batch_choice == cat, batch_target == cat)) - if U == 0: - iou = 1 # If the union of groundtruth and prediction points is empty, then count part IoU as 1 - else: - iou = I / float(U) - iou_tabel[cat,0] += iou - iou_tabel[cat,1] += 1 - iou_list.append(iou) - return iou_tabel,iou_list - -def compute_overall_iou(pred, target, num_classes): - shape_ious = [] - pred_np = pred.cpu().data.numpy() - target_np = target.cpu().data.numpy() - for shape_idx in range(pred.size(0)): - part_ious = [] - for part in range(num_classes): - I = np.sum(np.logical_and(pred_np[shape_idx].max(1) == part, target_np[shape_idx] == part)) - U = np.sum(np.logical_or(pred_np[shape_idx].max(1) == part, target_np[shape_idx] == part)) - if U == 0: - iou = 1 #If the union of groundtruth and prediction points is empty, then count part IoU as 1 - else: - iou = I / float(U) - part_ious.append(iou) - shape_ious.append(np.mean(part_ious)) - return shape_ious - -def test_partseg(model, loader, catdict, num_classes = 50,forpointnet2=False): - ''' catdict = {0:Airplane, 1:Airplane, ...49:Table} ''' - iou_tabel = np.zeros((len(catdict),3)) - iou_list = [] - metrics = defaultdict(lambda:list()) - hist_acc = [] - # mean_correct = [] - for batch_id, (points, label, target, norm_plt) in tqdm(enumerate(loader), total=len(loader), smoothing=0.9): - batchsize, num_point,_= points.size() - points, label, target, norm_plt = Variable(points.float()),Variable(label.long()), Variable(target.long()),Variable(norm_plt.float()) - points = points.transpose(2, 1) - norm_plt = norm_plt.transpose(2, 1) - points, label, target, norm_plt = points.cuda(), label.squeeze().cuda(), target.cuda(), norm_plt.cuda() - if forpointnet2: - seg_pred = model(points, norm_plt, to_categorical(label, 16)) - else: - labels_pred, seg_pred, _ = model(points,to_categorical(label,16)) - # labels_pred_choice = labels_pred.data.max(1)[1] - # labels_correct = labels_pred_choice.eq(label.long().data).cpu().sum() - # mean_correct.append(labels_correct.item() / float(points.size()[0])) - # print(pred.size()) - iou_tabel, iou = compute_cat_iou(seg_pred,target,iou_tabel) - iou_list+=iou - # shape_ious += compute_overall_iou(pred, target, num_classes) - seg_pred = seg_pred.contiguous().view(-1, num_classes) - target = target.view(-1, 1)[:, 0] - pred_choice = seg_pred.data.max(1)[1] - correct = pred_choice.eq(target.data).cpu().sum() - metrics['accuracy'].append(correct.item()/ (batchsize * num_point)) - iou_tabel[:,2] = iou_tabel[:,0] /iou_tabel[:,1] - hist_acc += metrics['accuracy'] - metrics['accuracy'] = np.mean(hist_acc) - metrics['inctance_avg_iou'] = np.mean(iou_list) - # metrics['label_accuracy'] = np.mean(mean_correct) - iou_tabel = pd.DataFrame(iou_tabel,columns=['iou','count','mean_iou']) - iou_tabel['Category_IOU'] = [catdict[i] for i in range(len(catdict)) ] - cat_iou = iou_tabel.groupby('Category_IOU')['mean_iou'].mean() - metrics['class_avg_iou'] = np.mean(cat_iou) - - return metrics, hist_acc, cat_iou - -def test_semseg(model, loader, catdict, num_classes = 13, pointnet2=False): - iou_tabel = np.zeros((len(catdict),3)) - metrics = defaultdict(lambda:list()) - hist_acc = [] - for batch_id, (points, target) in tqdm(enumerate(loader), total=len(loader), smoothing=0.9): - batchsize, num_point, _ = points.size() - points, target = Variable(points.float()), Variable(target.long()) - points = points.transpose(2, 1) - points, target = points.cuda(), target.cuda() - if pointnet2: - pred = model(points[:, :3, :], points[:, 3:, :]) - else: - pred, _ = model(points) - # print(pred.size()) - iou_tabel, iou_list = compute_cat_iou(pred,target,iou_tabel) - # shape_ious += compute_overall_iou(pred, target, num_classes) - pred = pred.contiguous().view(-1, num_classes) - target = target.view(-1, 1)[:, 0] - pred_choice = pred.data.max(1)[1] - correct = pred_choice.eq(target.data).cpu().sum() - metrics['accuracy'].append(correct.item()/ (batchsize * num_point)) - iou_tabel[:,2] = iou_tabel[:,0] /iou_tabel[:,1] - hist_acc += metrics['accuracy'] - metrics['accuracy'] = np.mean(metrics['accuracy']) - metrics['iou'] = np.mean(iou_tabel[:, 2]) - iou_tabel = pd.DataFrame(iou_tabel,columns=['iou','count','mean_iou']) - iou_tabel['Category_IOU'] = [catdict[i] for i in range(len(catdict)) ] - # print(iou_tabel) - cat_iou = iou_tabel.groupby('Category_IOU')['mean_iou'].mean() - - return metrics, hist_acc, cat_iou - - -def compute_avg_curve(y, n_points_avg): - avg_kernel = np.ones((n_points_avg,)) / n_points_avg - rolling_mean = np.convolve(y, avg_kernel, mode='valid') - return rolling_mean - -def plot_loss_curve(history,n_points_avg,n_points_plot,save_dir): - curve = np.asarray(history['loss'])[-n_points_plot:] - avg_curve = compute_avg_curve(curve, n_points_avg) - plt.plot(avg_curve, '-g') - - curve = np.asarray(history['margin_loss'])[-n_points_plot:] - avg_curve = compute_avg_curve(curve, n_points_avg) - plt.plot(avg_curve, '-b') - - curve = np.asarray(history['reconstruction_loss'])[-n_points_plot:] - avg_curve = compute_avg_curve(curve, n_points_avg) - plt.plot(avg_curve, '-r') - - plt.legend(['Total Loss', 'Margin Loss', 'Reconstruction Loss']) - plt.savefig(save_dir + '/'+ str(datetime.datetime.now().strftime('%Y-%m-%d %H-%M')) + '_total_result.png') - plt.close() - -def plot_acc_curve(total_train_acc,total_test_acc,save_dir): - plt.plot(total_train_acc, '-b',label = 'train_acc') - plt.plot(total_test_acc, '-r',label = 'test_acc') - plt.legend() - plt.ylabel('acc') - plt.xlabel('epoch') - plt.title('Accuracy of training and test') - plt.savefig(save_dir +'/'+ str(datetime.datetime.now().strftime('%Y-%m-%d %H-%M'))+'_total_acc.png') - plt.close() - -def show_point_cloud(tuple,seg_label=[],title=None): - import matplotlib.pyplot as plt - if seg_label == []: - x = [x[0] for x in tuple] - y = [y[1] for y in tuple] - z = [z[2] for z in tuple] - ax = plt.subplot(111, projection='3d') - ax.scatter(x, y, z, c='b', cmap='spectral') - ax.set_zlabel('Z') - ax.set_ylabel('Y') - ax.set_xlabel('X') - else: - category = list(np.unique(seg_label)) - color = ['b','r','g','y','w','b','p'] - ax = plt.subplot(111, projection='3d') - for categ_index in range(len(category)): - tuple_seg = tuple[seg_label == category[categ_index]] - x = [x[0] for x in tuple_seg] - y = [y[1] for y in tuple_seg] - z = [z[2] for z in tuple_seg] - ax.scatter(x, y, z, c=color[categ_index], cmap='spectral') - ax.set_zlabel('Z') - ax.set_ylabel('Y') - ax.set_xlabel('X') - plt.title(title) - plt.show() \ No newline at end of file diff --git a/visualizer/example.jpg b/visualizer/example.jpg deleted file mode 100644 index 10ccb46acad94380a6805c3dc405140f661031dc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19459 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 T-%tNH0{=$f|Az>G&OZG=|Jw_t diff --git a/visualizer/pic2.png b/visualizer/pic2.png new file mode 100644 index 0000000000000000000000000000000000000000..53fddb4b00f37ae8f57a22b0da74a05c7ddab6f8 GIT binary patch literal 220561 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%0D>@M9^4@lv%XZ~o7JWuxaJF&2k{ z^uG~4JP@Kd{c+4=4}&5xvYmga_wiBswXJKL>V)>{H|T0BKRtWF#lWiP<#s9>(mRly z+zLlKj0B}L*Pu}T7&_BjE#~~GlydM(koW;t{x)2jZ(_$B66_mEI+e)QsqHN0F1~+G zO%e7-FVWXB8*QD%0YlaU##7rSW>?iAebc9LIWU%3|cQVNIk( zmsC@wHPCwW@k|zH&Dry0q||)~JuG^9Fev(0AWY)TPkyk9)GEcTz+p4UpgFQGqFASb zE(1EmS<3S~Za?@w_ljROy;?p6N6%c`w~{yJzBgqq`vy$!Lhx@w@f012T+%J!^@`ll zrUZPemLyLpRL7>WHc7r3Kn(CDk3DuBl>Ylw=Dq{f1~-s|Fjo_&QqxGzLQ$t#P8U-* z<-}G)|MYT^(~&`&P0XgHsh&PK|K7}*IKKR*2Q$67b-|@4V07;LbT#YP%j(my%{~s{ zkDno_;ezK=p_RN%mWIiKV+8`VKcnYOJdxrbQ#LIosc;XTVZXkK{pO~2n{%Cy1AoN) zBxJ#g+#pA4cAGFz3S5ER_uHJFRXFT@K7uX_NfBEyo_|+Tvp61-PAM91AUsarHc1w;-7qt<=V1?bb23Y;W839~eZ_n|ZLAqcYlh+0c&zZW@gn zTBX)t#%tHU;e~&ma-i5W1U3NvW)OQ*Sa;EnS7+r=#VkM51$m9p(^yYDX?m!ZB$e2`=9Yl?+`q$>mMvQ8ge(&|^tP+(}wKUl*e!N$s0U0Y%NNghb zO$z5HQ)1YStu-Vt$jddn%?lH*GLGWU(S#k1JqH>>mFa`L(0MJ%H0V~d^P6Zob;L`Z zY+oP{B#c&P<8JlEsoGH5d&MjrjLV5a(Q;8AIA#o3xkUUcS8=&js#iSmgrfiYYcDow zdw<3vsmD`X+WiB9l)x0;T<71;n#_VzQ92?!-sY0QW!DGzl3Zy zy(Dy*e?##%_^Q~l78xX1RG9+_^vS^*7_+%I*0%7H69QT9d=q^$+{P@L^g5PfY(*XkVY-~r?xAvyT(q8s z(`u{WLRLb1uChsKu@>xsyDdNBAmpR~jZvb>&A%mn!D|8ZOa{EGF{uX!m~TC{X4il` zmTKrX`evfCdc$Hdj4jsGdqWbx2vb**)lz*kx$O-G*WQu(S07iR_thaQ)HOvhZ^`7{ z?HeJSh}=;2<2*~F+b~E+R8|{)bGE_6M%~;9NqqUZECIdatB42+YV&>m-@IyWvyrRfb7+9SzF<&dOmhjrsV=)|>VSx|pPDaE0 zwTE2Wz|rGiC3J^S4mg7a+J{Y%@;VQJGi8u-`;l92in=LMC0)OoSC}c%8{0%fQj$Nm z#4ED~yG2PdGj~S}Js|a8pECVh6LMb{g1N9D9c@x+hSzZXd$X?G6X>U&e}_ z+b`R zW_v$jT5^sZ9xu)s<+S=iy8L&5b%UnzSh15;F4%8pW8WINXAf=D`q&Z!# z_s4a>B5+1yS?y6={+XK9C90vf(bzgc)lezBiXcnE9phm|^F@uo@{&V)pcU9v<|2DLP&CPL~`#V}X>`XTat=s+3#dK&YZKw>13 zwfZO11JXT9G4sav+l=iUkCt8($J4doH2mUj6y5pMs1Zir_Z8Q>Y7a4q{~$B~w6HmG z(uk-Hbraz-G5F8~+wKZ_4z_WxP6fde_OS`Wfkvy^hFpT1!G0QhbNM zeemRvlonvmG^j+J&RQp`c)~=DNJbe~&Xahy_sL%2wJIEwdpEp(v-0yQCu5z04E<=Y zG(Q(!k(dt8pq{;o1nAP~;&eC9cytMp5;b=LC|5)JHZUafs<*98A=l1uEQb?k#9nJ8 zlneuh&)S&&&=DPJG%;fE%foA{{^t%|&mHNC+}1qm8F$r1?yI4ol{$Jf)@$Z$-$qt!X-er`NTTAK|23n>-$vJCy?tV3YrrW2`gCJ^ed+FjMJZy){Y; zuFW~n8D`NyI}lXkbfTEL(3h#04#+ceLp3P6=15ky-2rWzN{#Uqed7T-EkO|hh)iAw z#yv6G#5U~ugV6Mh(2J+~`vOxkj-{)L(CU^lJEO^Uip`p696b)xP4`I2dcD)rntu*i zBPkZGFCTu#_-*dogPXM6)J2Q0@js@-%fe0Q ziUFHuOXZx`mPJRWrX4FtbmM`3w@&1MOZX6CV^l&tAkJeDP97K4$+vSpo^^B{j*^nS zB9^C{T%FcDOr4^>&UUaF*DIed_wN+?$y}@WitoG$6t^fiWZpn{qhMQ=G88F+Y+Amn z)6|#14C9!kgjx@}8b?)PcdY-0Q^e|ms3Y>uV;6%8hrE>2XO~knROUdkMwk}<7My;{ zgs&BFu$rqVgWZpw=NZ{wZrflJG;9pRygS0XAM$4F$fn2vim$HIQgxGP*OMN#vg;{_ zIRTKzV0~P&muOW;=SZ@ZTVBL37_c<8k?J_jp2fA39@YUVhfB>rP5Lb80xTirKzqW4 zG{l(Ypp%re7hbo*=IO7MIa|qd&viVlzw}OBIbtz5|7(nM$Kl5ngQ1L>)pf7+^`gQAtiPvDe%5I_6rQcDQXb!AJVHx zREHRO97l^6*HlAFGw17X9gdv)jCYHuOLsfeUgGrLwRW*9WF*)!!d=oHPgUt>@Q8z7pd_e z7ZLp6+(hf@xbj=f$$P1o)(IJ^9Th8jS8+<@8IG*th;51C*AKwhFN(#``diPLd3{7H z-M6o1c8T+ZeVPN6iTNXto3wZ#3PU-8gvH45EY%lPdDbZ{=>x6lhY=H3M#m-(XRpK( zPt29|G4CeLedZYlMHge$Ve1Y1Hm(*-)cm`BxagW28Z_ZK>r6MBXFnkCxY}{tn2YbK zn!H}nTv5jzt4_9`ephAuqNz!uAwS%tcDRl9w28oVXuhx$Ih;vPa=Njg;sha*)F~VH z_XVLrO?iBeKK~Y0_3+$uG-W+V^j)Q=oWfpz?BqRpBl;3#c;V-|g~$!2(zD&%jUM{{EXo9U= z7@g_4OetG9-+0JF6qsFEGn$_^v*;AItDNokKBR)a1w=>(<$`w4kOu}4yxOcIa5Jb{ zSPt#}6w32(y}lLcZC>qGf*UX+hMjZA1G~+PW1`-Js%wSRY}#LvnW!|Bd#(K)@!#O! z*dG*qJJ?`Ae_Mz6Fpd*lnC(9p_Olk}RYK2iuw^jC-!CWC$-}+O5sGjmy&;sJBvYNI z$xx5e)H&?q<>iKna{-C`uD)L=RU9XG4ml0=s;P=2&FTt8S(j56v}zD&Hus@Job2LT zwfqsg12cBZ2U5s<8{z{Gu{s|+Jb3<$3%6la1XxO$psK~hsd>;AmAy$j{ z@_V~uw}m* zljXs8zoqtW{Ew{|)N_71isW)X-#rIBi3EBVgz)Qp;BK-V<(M2c#RnsT==r%#`c0Q9 z7#G3Xs)CV&eb|3ic41ojc8SyF+BcE+exydoi9c?lNs8Pm0)iXp;dKNE)uDYV7h^GF zdtxF85feGGPkI_daq|{DccwKHj-cdCBAlZ+{o;^KLwdg3^3lZ^d{UP%U0d!^-uB``1b?ej0 zw69u6y1mGacP!MbV52Ppkrfn4nJBqi@dD|6;#kLfea)LX$cD~@@Z2za#-(2j-@Obn z;VNsHTQHF!@I@-%FZqAyC!BdFbrcf6sh9|AVeJ#?AMufXXX(W{FwSa<+JE+|2d-(M zz>SfNaxb^9F2t+j_MTy@ie&EWb=LcQe=mXHMY^SkK+!8s^6g1o=wd{dEhF9E3Aavo zw^&UZmd>{!uoqTGhwff8a9jTOCE#jg{%7Y{oaL0q#!i47sItzBFrM2gDons7`+%Tg ztEicun`soreDvL1u?#8s^4gWju15`Il6`xN+I(7mlD=rC4nyzSuH^{<&Ih zTFr8+zcqor@cFX(@;Z%{WseW6@)SM>!tRV5exuYPNaW!sSrXMCd%IYOv&;2%Ba6E^ zl?)A%bzU;u$IMYHbJ~>>U6ECM3dI<0K7QGy3wR;I6OqR<$j=i;9e!UvG;I(y`?2fQ zKYj2J(^2L&R^bfl%CYGiZ6Bh3pr>y2!(X3VT=h5*w0fH)X|@7lspKdApgS0n1|q{P zya=K~#2V#pt-<`H+6aDamO0WIbm;b4viMqg|7&Y8Mg{!SdO;=UI=LjUVvg`yf#VF#!@)3HgZjLh6ip9!E&#<6<{3)&=!Z9ks;~uD0;DYL<32dBuc)w*;1``Gz_6lp8n&mC@rOV z)oM@e$x%}}Zh5mQd9zLkN_z8)Pw~{cgF&K{bwZ0DqOcAX*e{Zx9bZdHp1Gpyd}hG3 zV(&w-rF7TV%-ULmuL($(LTUA>q@rQ#=a99*{|D*BAgqjVs?~o%U zU<%rslMJcQ!_#Z3;f-5ei*io8RRI7MI7!0y4R^iPdPn!TEr0LU;=>373w11;wB8># zFmuKvU?arC^RKMZtzQM7J?S~CwG$`ST z@R&*fp4E6<)tg+x2GT)L;pYt|rgi-iKA3^Xd$-RAiJwgTL`V?N0@p}BmzznH72yV| z8*Jxn&KasGWzea0M(pk)o2aKq_3@Y88E&pcQGYt>h)n+Gxz*;(izR=Rm0K~G&KcY^ zbY+rFqUJA;+Jdz#MOXi~yzY0yQ~v=xN$;dtt`d6X`Kp*NWeY358sJ&|E(ixO9s9rd z2xMOOAle@T_B1O+9oh-3Jmx3OZ$)uJAuz;TyfX)eiropL*1TtaIT()pm$2k{MrZVh zkr(dtlZyTcy~u!I9=2M+8eul+F+)5zuuk3e`XQg2%3+*1Fi}NlD_p=_>I@78YlV8L zt^wD$eMV)}UKnf}u}E}e@Ts=%S1!V2%KyP&z(zEV4yFJ=RbarLv3Wfj9T_ki50$+(~bAM;5bD>ef z7lTt>>pzQGnyhuAQ`HT`tA&+P&FQ7+AM->__Ufgr3k>Z_q&};6v}6pCNV^4+=*cog1S@A72F0I1zZ$KP|8A_FPKVntDVGE21WzSb8OWwxI;2E z!7!+=Z5M;e@s8*ohwk_$CMoHFF3gB}egNGWx%vW?sRpQ^htAttO4+jnMAhpgopxv9*y> zvhd<*a`2J^He#udtq{!SjNlZi3c_^WYga(?+!AwO1Gjm&^y&t%IvjkSY|bYyDjcbS zE7KQul4%JX+?eh3t{>Uprx$*&fa89*^%mm1we$4F^Cuxb0~TQCZ+Ts)=8!o^IMN7) zp0~n0GNL3eV>z)^dm`YEf^`h?eENnYlTrE-IZefy3J_(?wmiVzZsj>4nTt+eS$H0U zC&pu%es!%koeR}+BMc~Ag~6QaL}QT$6;aY4LXEKk7jy$$?;so`t%8{>?Rq-f{F7wd zQI=M`G~W^ZOZ8pMHierxr{ICXdM@U+=Eq()TBSJ|>>mfMX&Uo?CO-@d$3Cnv(EQ`Q zLmhF#ZMoNEkR|yY#sk);8_y!|N$hUKue;K(Fj-?oyoq1HXnIsxp@WY(L7Jsi z73U(GZ@rW;$k86;&E-LA4x>L{Rn69*Wo(2yU4zI>`d8lLiUL2CdlSTbJxaWz==NqC zbVU>OBKZ82iPvwAlOLx8ML>P3Gw@^YAX)ifn4Bu=(Vk73;GB)Q$Y)KR*P>R{$R77< z;jJiV`i99LgFTVOHKRD}s`b*$?z02S2Ge%A@`imaag&Yci+&X0AC45s#{10n{DT9s zzq&oFhyf}Fc|DQ4iN~%c1yij7IkgGYpdi*OVFf(vhq zc0KnS2N zFqBv_Q}%iwl1_>OCUf}-n%ju(UKqIW@1*g3M2H z4A}AY=W-LsdK@z$n)3~2yMO~WvL$1GOz3;vqsX|cx7@G{|DmOAysO?OG1OB}GLqJ< zTbupuf41j`6rq-#ON*H!FSpAhe0`LDahXzGp#htK9H|v0&ZsF{j$!&B-z}uw6xe&# zc{DGWP`IhNLET1zVW!Ni0{e7Vv`NDD@PmyDIGtTQ@Nu>(c`XeQM|th~zTVh8xdnSP zg^NR8hinW;_^Ik{g!UPJYU3>AWMHyXF`FD~O}wFB7DY>UjVJnhOu|Pgg@=+Z{9+(Z z`f#Kev!Neu0*q)A?`x0K3CRZ_gVS&p-5Af$$tlQmml)orjCVi8(?suB21moR2+(d}7$*Cp>=tVx%WWGp}^pOd8*lQp)p&v0QU1-XytdpLPX=Xi>Qk z0c;UGtdU-WE&p}Er2o9^7AxC>lc@c|0AGK9ppSGTe(nZ-XP4xXyXTg%58EXuAGlB# zvW3`z(tH4BG}cD01$Zs>79F4GEM zFc6t{Ey4>iof~A&%pXfPK^<9}2S5x3J9`U*4z3Bznh$NtYznbyM-3R9%nt?JXA_`2 zdDWXH=k`ZmGe6t_ap@^Q>8TJc0~X@Ym$g0*7aaxu1s+lc6;9%STc|C0KAGLYHW}um zrEDBSrw)H$f(N5K^!$DZN!R80V>SSNURT@l5V$kckRVOGB8>+?nS^7m4(l$&+6kQ` z&gF3U@KUQO_inoYtSlKw7gIyr@)(01|H(2p&UJ-E9y|-S(~QQ6>93(doL7Q}xWNuOvv}wN%9`Ml@QuVFW_?z&~BcRS~q`U{HgO+;j?pEKhh>o z)fD|}h~fXawS+AQR<_k5q8$q!xJ$!A^jCtBmW+*KdX|p1xqEqP;|a6kcv0lO_X`vC zuCSPwx{W=(=~mHS3z5AUCb5Pz-j~tYMp=hbB zdV}<-m}5M*Fk!t}grL{Hw4N)`~(&Ci;a0UIldethpc|G)De-#a95-9$YBCVBl^kUjn=tIb#C z;l5cJKp%nN82x}Brv1|A;iSdo%&QJ|Y~P#`tCDP$j2O*G73Osf$4c6S!pQ;lyl{cLhWDh+&- zzdUto=iK~m9%I3&QOIo!-V<=;Rm`(1Nz$?^JKeVQ#{wFcGYtHr5%arIHauX&X+&~97s5{iK#BD? zTr3%(4^Rg{o>0=N_FQrw{IOj0GG(L8@C1; zGamIYt}Nt^Ka-Za39o$c&wVaW3ZMR|)h8g8_j%0pSEA0_{;Y5l-XMZdbo*lTao5SS zM)0_WXnN%NNF+z1(hD9CM!pC$fHBP1@Ryam@?tRW#j^Pcw@zKpXzZVm&)F^8&`vC6 z6)7FJ=3nOYY1s{U-!F1!kQpTyJC%?DtMQj^5hvG{Gh$e@ax2+V(3IywE53y&m=di~ z>kZ;QUd3(|{&k=2c`pRJ7fe?@h_ouLpoq51KG7$u5tiJ}@!c_t(93|qM#e+{W$f4t zH)d5faxm>85E=fX1>gaRJk^S-hG!AcWW|0*bQHo8eb@w>H;`(Wt9 z-4S2i1M_Idld^VtAKPkzLKmqL3rJ&=uO*YLrmU)BR4hWM3{!i=g5sxhwNJgd)WPxn z3(?HAF*thghFeDv>QNbI-kWvIn7%`1eD;cIxo{i3`2sh&M#j8;^0QN!**>0A;KI7o z*2x0RD@f!Q+S5-nUkjpOI|h@>Pw2c5X^d-cmx2spRxsVEo4wos^z!>lFE$tWWC_T- zCb=exGzWg0WPtfQc~8~BerKObk~L2faOFn1i1pH6H>l@!$%QemRkn!AXbkz^+D)LV z)mBcvK3B8Uk0D+IF!!~sUjL$*W@}wf>lQYH(tbqK-2ezr1D=e>z^zdW1_yNT+I$eVAed1TNI~QRfx(ep?z?vkdD^2^1Z}t|q{8zw z5K6(9jH=#NoNnw2_{WPapO13uJYTaTiV+pViyU^l>1cO!_n(F$7svd`vLNL(r7l3U z@HC6;jr7>so*Il(>g0hXRiX;QxF<53c&O?^3%s<>hflquuYRB#p1E0ft9+d4+VOpK zI>)0w|HGsmj>-y;eQ*8_gQ+6_9KI`eD2bue(k4X+IwNac%o-D-j#I}1B>pA$ZWO7gz z^8-=!W6U%VDWuYxs>qR0#zkTMClC0So`G78)O9bL(&IsjU&`+#uj{dfDTd~5&q^$k zR`<5z_i+qs)(U2(DXicIKW`%Q za&Ubqov*^_KviV^pD3)i95bZ0GvQBo3uF+;uuIJ-;oXGZx^W-+u+B`-k~=UOPA~CDPGm0r}E|Q zn*v*piF`dAV^oD&sSB6zB*8uf<%OXK3tX1D2(VBEd7csbhWNdY+Ugfk5X)t~noHN| ze3J^~p|IT9pJ6MU^nj;Q-9RqGF+IWV-A+^P%#!ZFHc(BZo?Y_^iYeC32%-lTuluS; zUazmZPQg$OwWkF9r9xJqfNgcExV*_8jsTq7Q-2)fEa#Hu5vP|8$nT&)x>MP*gB!0x zgzP>K*|v>nix0>_de25+j;c#+du*voZb`7f$#{yA?&vzoV6~aDqjrr!*MdmKp$#qd z2hYXpRJ)OJ@ML1+gDv5DIxx5?XgC=4+49M{Np+Hni^^f9gm*mxDY>SCn zrWhW=u5>t3S6P}Kr!Y4nZh_i{5!uD!+hP|VlPzveXwl)_zvnF$sS3CtFd;R1M}%pW z_H4972EEs}QGDvvt=!_#|1ovYh6voGE_d@UJw=u=N!K?lvRcO4m`T3HJ#k6m0tQGT z+a-e&b7o#&zh-57q{qQy^S;j%N(7Qrc!?nxkh_}aas;Bh{zhefJ>#tS+PwQazy$PY z57uk@F8{M*)wXldaYf&4&d7_p_}Iy0Y~19FLGw%1P7lu0fa2{`{Eo@MJ&G4sb;hHL zi*i@T6n?L;<)e`+D5=$_FE;BE_gC3tz$v|YemdZ5e=4Jr?jabjBjR&iBu7fKJrJVa z4nTPmsQEhnQTkB4qGK{ExtX>+fqx!3^Kb;tFuEaC8&?SzlW__8Az9IFm@1CD;N zV)&}>iT>3*-8>lSO&ERYpW3ni4cpv$YWA00DmgkbV4-1%(d^(kru%hh!ut6Lp~T=n zzi0I+-4bdW;58kcGuQOijTh#$zp*21gV{fd7OFK*``ED&3d=Y&Uc0HZP!FaD3r50K z*XQ2R-}VO+XGto{#)!UA6)T)8v*_5*(x~QAy;oMW09U;4v%*0EgL1hq|Slq73HTv^8%NzM?kFrMoRWdy)eJ5p>ob&mn|oaNX}85 z`$pWsy+W6z`}fn)?zyi;d2(gb1?-r-oVdKAJBSU_0|Ug`rT+1*@1s{`P>N7(d}q!8 zKoMuEy;8)TM?wSdY&&#$WpneWYr$+l7=9}_Pq$^)Kn!rID;|0TI_<7Gigt}Q3B#fz z1`Y{dTYJ53_vmcUx*Ju8TC%v23u`l9RgNn%8iuV2gz3(+%jWA;C-P6Ur%!0jTc}%^ z6gqbOsL?ncv$qe_ifUn1cEw8f#PQrN&5Da+#SK*n zRB&L%qQ8~QNi6x8-Fs!WCYRkAuhG3yTLoEa7x=1gyo%mG_;lNmfCEAJt=P;0+-PE+gQFwmNDK?l!j;&^6e_`pTCx{HT}54f5)VfGtrFEL;tD){5>x&4JxJ{g~qY9 z?u?RR2z)j7(J!m**dJ=_q+(FH?zQyuvagmaJfR@Dugh3`(_hi~X!7rjdJH7c^AI2h z1?LEkz6s4bW_*4`r*{9y%ckEKkZXUU-jL0FR{j{Le6-yH*Yqz^jRE+;$`aLSUQc=$ zgQN)%w7`iRpu0uTgk8KfGoTabxyraKeIqU@lfKg(psnn{QKe_0jVP1Ic~+rSm}_2; zIDhTw%*957b|Tq=0t*pU4i8rCL3A~Yq!1D3w#_>_(}sxCSSof%0h93P<8*|3)1Xy# zA&Q%JV2?+v0kNwiLTjGW%fh6`qI**FLw{oGbdn}0PZh@#;T*43D!82yYI`jK1dc(L zn`3a4{HMZl6)H_tq}U%MlE)ia;m5zedNrvpcGQRuA&%k7ypxFmape6vA)&m>mFZ{N zt)%Dfh^OKYN1qAXtS$XVSnhjJGB;1ySTT`oyqdPaFFb&@sKn0y=YyOQQ(F3||14z_ z{U{3icf=_VtMV{fsf}=?-G&@X*oN0o+%GJ@HzaHdRs4LQcG>3u#m22AFs90ET>1|e zUb4ta!-Vgqqop3x4dTe$2^N0!LD5IXKTEMP52YQ_m5t+YL3?z**R=L^dggiU(z(i^ zJ4PvlYqb^#oV|KyVVgsl)-d3ic%hYWR~6r*#k9gwq&*aG48BiOvDrRyv&NyX|> z!QiDQinmq84p8>`4Jekr?;L-I?!kIy_-{91s-k6;c08jp}7#kP5zu&^kNP7B{ zs^YjqqmgMs1*{x9hXt=&Bm}<%!}~A&d!jc=U8ADcOa_zk{U_C^W#9p+umHr2n4ZEn zgk#l+xgz|Qj>eJrcHOgK$c+BhpO5}Pblh9c2=SnMGAU)x~IeA=PkbW%QWQu+OZ4i;?TSIs3m2k2Q~wt!2?~3DG&3O=z&294!!#j~^{x zuap%kWxF}LlEj01R;aa+9c9T?7$>Sd|U zY)=!?^JwHea@jQSH=^8z-Xl%!lRl{0gGNu=ut%zaJ~7EM8c0=?qGQ=K(7;>{p(*2| zHwAC*j~K2^k0gYHzoTW|TwyhJ*1k!=_EF<#RxmF4>vnW>XhDx8oLcn5i#>d9a6`KY`WsMIy*yNG zQwvyV_IUi{gI~}0BB#?GeoCXvZKalnts}8kw)J2L{m2v2VH-}tw%&lr$qz4nDp#dV z|5Wg!(lKs>2E{`9KvJ~kopHPB&BFy#SEOtvjz6BLhy0>S%gP&+s<@OTXx;sIb*G%~ zDxUq`Ls54X3zN}&0PSF8KfW)==Qhr0l!M>gGic!fy1xASmYztB~dA|=iNO(TG- zFgPov?0J+8{NjTY6ty1?!(2M0n_p<^h zTNR%o(9QYR>pbC$YT2P{nSpUhd5MIFdgUfRgwjK*r#j8mTsBw3&z)%ZNGA_soZGF? z(1%H&%iSd$fOf^n{HNfQ1TKLzpCH~OiW_G@WTs;5y8|YD!FE#Z#EuoWsnohmuDS`B zj8}^iPPaq5GTJ|nvw7>T_=zCY@s^s)j{W5U%@n_1z-Io=!9ZhL&*dZ1v#-lnZ(RPe z=ulz^qVOm=J^WD7L>5g0lOoPn6#*7M&MhKm+j8tF(TcX9kbPlMu73q~3jYKz82v_w zzPE^o$OnXR{wRAr^CdMffhQ|Zkkm|Hjcj8D^iOas#o$G36^Y{E(b{^Q>d`=hP@J+6x* zX!!{201gO`CMOiYY!s?VuJ#w|mk(R-We-*pr zfcie)t3_26Nq#Fl?-R6A&54&WAsv<9kOZMozs->i$d;e#lQ**hSLq5 zqMmefps|-$=frZ4qfZr561l7~{Y;UmAhf>~VQSb@j}ygmH%0P>W#6#Ku<5V&!;V3* z3DEPLf{3L}Ar)E~0b`ygiaUC_GdGa+rdx{)9X>AX+Mk`mFkk*HYVV+DO4!h!eYa}Y zAqVI6zT-5j7G2Q_rf;lB1fb<{$DDybw`cTde!hzir#nd`0$u}8q;0wO>v?g&m60mA zcbx+R&r1YnZx|9SV@#(J--_x1?rC=A*cKvlWum5t5iu~cS*TJ8Eq*)*EN%pQ9u(LZ zw&()9e$~#pLalbm;{XNg*>WiQrJ1#)CO)s5LS-}ce`I&^Qa_Lwdnb9kk_v~D(D5_J zO!_|kq0VA+x$t82n|V#L!*TpgVud(z-!CiOt>8mPS>e3MoA7DqL-Eq)uvg{#wUYUwCgv!;$B}CCY9D~Wa z%>nv1m#VqZ-%gf|ZrigWD47fIzPi&*VHKzqbV$gBRnw+P-zvB#(9ns)Vag0POMZ{D z$-GoHb1k`_8?9Lt?}*n=`xH1?gf^P}Q;(dMkKCOBnUt&tw812@Wsd4&vI6A7GQIzU zPw}6RMC&O@5a5V6oepUGwpL5Xh^EZi-I8UxZS(KHC`)&zVCI}@gxcr8fkZUV!*Z+w zaY9JU!DmFh2AhP`qyNo-qV~|p!wlmQ5!Oo=C8XZ37lFhJcQQMUJ4?MV`d@$f!ObyoOL7cgVKv$i^>Gq9kEM8AArz(E( zp?Y=XBPQSU{51s0->io~I@0@86?&@)dStrj*q*!YhepGLx|xO{7`|rV+)rA#lh@0i zXIcQRlVA8DOJ5Om_0D`rrL;{D5H1Pm4FtdkvEs;#XN9IE)_q5WVVNZQBMB4jD2e}N zZ#j=(|C(pi|G{^ztIs9egjFSXi<7C!cPrc?yFJpKd0x-}0I`(niDc|@)}wv&{Uf>9 zsEF%ziZPl}ye~&|x?ZstotYaRdE=y{Bo|+Kw)2zCAJWW!f+GO4v{x|5gqn`Ll$ZW~ z35v`@oi|Zg35GQ}kwXHbYxaO60q%BstCKLxL?3?nlAtylerb#0J0;j1ofNuB=I>r;0l|gtxjqV7FedC+0=akXG0Q{s>+6SvuJ{ zTKSyOh-1u?TcL7_;Y>ahiO8^{7~c>?h)h;y9M{gbI5{TIJj|%JCe{(Zx;q8xOtr#s z0C&=9?nIb7FT&(>59-`Iubl(5s5%>knF`ew-2bqf3{OChHwuL{=V;QURg;mgC%u>) zwMm)YtLhY>6obG}__!5c(cl50*2 zc*h{?i)m%m{5!&tf?z?)t{|(?`X{P);Otkri@c*;AaRa=?^9;e(Wf>hql3LWHfqva zH{)XG8`>c408|qW7U}0C#|u19yk;m_h3Vr4)X_&ad2iMB6J07deL&hqf1G8%}o zg`hl<+?L^`yj867Dv(;saX&YrGKK@*;YMKJu~BHB-sLL4F)xnwUrCv_!Kj5FjPi(H zqkW})9lg2QRSQMlO=}-Beh1$3x#;ELF7#MKf9e7l98Z$$Hz*qfV3!SBVua~slrREu zn!vJe3^ltq8^?yfig!a9g~Jrsy>@8I(OvOortYNFovv1%{fW+*f;$Oz40yLeK#LBVn>UWrio&`U6-MhrAwrX=cwSDL&LmLQgD+Y58wo8TfZ1L>7|Oh zs?(%1na8+6C~GLVa=uG95W0%}Ip_&Bxe^h0lU!!qY^7qhtzCC^?lCaLk}}H??L_GF zN&ha$o`lp1%esefHc~RApvU^Pt@Lezf!t2q-YnnE<36jpJ@r#k!IfLMM)am7T0Zt5 z%T76$rjRdeP){|iC{E6$ASJPiLyofpTtb*h2-!E)IQeQ`S*+Z1!m;Kq@BdM}e z)0mLx)AIr=hQfv??hkFL0ZTwZZ0)vaM=SiIVr_MHhP&FMXweK@IDY`U_;H5w9X}m& z$z8A~(|mX_P8Kohu4EQshV<(yeE}~Mc;!`X@d-~xxu*Mut`N?At7p5INt%JYsNu!2 z79)oHwd9#bY}Cu@*<9*X9SF2}n_GjKnhD#^pLIB|QYrcVkQ zv65rjKqN>KU*UxKrN{&5F#Rgq-{4aKmN!Y@u6~2l?g1?%PjjU+-3Is`Xw&WW!2;C& zjHbi~J>_gcB9*x-LS0IWUT0_aRXvn{=5ZjDLC|$#Ggn#}A3GRfn!P~gmw~o=ij}$KRP|oD$jJ;X7(W2rU=sYo9nEdi_vZm!T3o2t9X z^-Hn$D>3q+`nnM))M+v2KAKU))K&*-*987AyGqG8HHLIk*Z5)61U=Zt?&d5+ws$Rg z#<9VQxEWNtP@1a@_j^iNBrW6O{#Eg(skK-0;yu_mW!6!HSHyI+C`2hNe(>SK(13_G zC+t|;eLP-0bxU5GB|MNFSt;Ar!iAPFBAfFstBBM&n(o5TO6q|D^JXice}Bf8(xP0x zm5ga+Y?YLr7{t^hbj8ovhndeiz_Vjh|JST8%BGAeoJCNs2DGo`IyFo-JX!Me;qdBG z8}nSn6^VoV(5?pBf^M8q1o+|Doqd|nWz*Jve>k`=B0>irOHl)?@md5wSTx>i9RGMRZbPnChG*L3B-9h!@!{4}ToL)y@`p zjV4$Q6ZUTK6l2~jAA=H>q!nVu#ag1@dKTe4hUjNMxGCbAz;QZ2mYTc(XWZiiP^lci z<8n@7(bHkxFzyq`Z#pshjQzFB9>}I1^3~Sz~z2+^7V?i02$)B$j=t&gW;xE_D{mIbeD{q{o5C8^|%wVp3w6uZ}tpf>LT_^ zrs3ywdWY=3zSf)@2?BXmrW=%NU)_Lr4&q`IkBE2Ini{dK^B;^*-vbi?6mF6WCsPcKA;(46>Z4f16UYn(S$iOnw>_$DTcn|o4S(VJ zBDZRSkYnCnf@U5=U=&xHCgDG|7W+mcSygRXrXgVD+c?%*yZHpz&+cirx#9QQwC3r+ zC2qX2z(#Dr{OrY_X1RL@4UV$-=3gxg;~=s`P5NXVTw?r@g*4e9xOBVX&7AQLVoDPGs*7FbHc~_hW5b%@He$$HiGVwfrh|y#d)bl<%lQ=a2 zcAW60P;#V;$>jqqn^A+q7DyZJRIG0^STcp#`Q$8vq4M?H#?sSCK~ZxwhIh>nY5dPH z7>!S!8Z|F-H|JdgLtAOm9I08dqj7RtBxw%sw*DK<_b}m`W?Y=TVJ{(gFHNCb8GGB) z><9Y0F*r_*7Xa7xjrr4A-8^E44A)P>*^Gxb6pZftJl;-VKw}xH>2zE z+k{EyT1L@03vcdT-dyyXSD4H7yn@DvLPpT0(!rBgmWrDIps$91@ji#~WJ{LFmv|9} z^2-NmHLpz@Wekvhry**=W}!LY+AB`wEy>Lp5(#Xd!`xwBN}B6y{?_fy@Zh_vi|-t9 zAgogG%S`nUX2c#)rasw3^0)BZOcC4yvKtLbLjV;DskqL|6g%UcVQncqB#hpeV}Yr$ z+<@xf&aZXY8JJ0HE@Wm-8w@L~Kn_B4`PE;m8l6a#6wYy$c9OT~Ag3ZtDz>{|5&|)B zVo_wgOesc;!blCcd3OXIMuPfB07yEZZ^!+rn4!p4+GZ#Khx;M7AtMr=Tp=0k8<_#;50VdynS@(TcQjaR3l&;#O7T zP)pWdsJKh8)Em(GqyjWJQe_Vh3|mI?Zx=`Il^a~DerpU;I#%C@!s&-GB))rf$3F_$ zp^!&7KlE|#2$Zfh9CXjZikeKMy#Ny8?AfkD>x6V%o~55{pWK4nN^Q{g?LD3v9Jm1x zRO|yMnOtY3`KlA^$-WoWdo?^;LLE$yH4jr>6_AWa9&-XX`( zwE&*L$@V9gc>PgBuLc!rpgOhfQy1#n=$@Lr29ojgue{BW@cqQ8JxdQb?N=t5i<(gMIsHr#sB}$wH}kT5Ne-{jBci; zOZjY@0W70OK-hLeZt_o9NeJ6KlzSmw^@qpfqm_b8TWlWgHkzq_iHk7c#eA-l-1zQ@ z+S^pvZ0)YRIqaey9KgKd)$mIJ`sDW7*epQhf05fz(DC6-SGLfP4wRa?kR|N9K|zOq z;b=#f9Rurt)`MSb97h&gSchr=nx>W z&RHwhRlh-1Y$tm;%ISl)tKjF>g11kO=Zn-`V{bG%SK>#H+GoYlVfjWyq35&?Oi@+1J|p zT^Zx2l1k(_14(w?+9RIak}SrNu?F7Gqu=n}Uv+~))d(`D+--yRU%lP$t)y97XHY9Q z8FA!C) zGPrR-@?WK+p*bVfPRK!<2>~JQ``|;O&$9t zRD|T~(dlO^d{o3SK7ay?9FGwOL-_Q%T#wc1;@s!`iBIHM4ppWAZ*(-rSA4ds0-Z|E z;GO3`@)r6sqsL+6EXO?n~Jl#(mt-ySvdooSt(#@ zJD!_MttKi7PhAHrgO$LQgqdi!LdYW-XzsD&iw&3M{&n0iMGV{jdoY6-hMp~dcmS;WJYR|qQF|@V04yU}t=<>; z25_9cU^zhMjB8Lro!pT9c3{#_BHE{cZ}}B!HNMk@;6y$+22b9RA<_8^x#V@NRW$fW^NmREe^E} z4l{oIAlzC~kgQX|r2ih2=%j=F%k<*(jR7A)QKvL0vsy$ z{k>w^)G=0sw_)8V{EVD;lms?@foq;ORI;2|QZ85a&~7vHO${LMnSlI6kYAH9q8m_p zAN;0ip6_M+s_jrUC0?u~P1UUG;P2r1!*aZNvcu%QghlROOG9rz!*XIG0M91pTzSZw zz!5Ve$m)(H8!%YEcHbGZ;Corbc@6{a&?SY}G|MxM%Gr;&1AGm)rvp{lA*#Q{@OQ00 z1oR?+er78Wqg1UE5}pKaGkJ?f2|5|dAMXSwUIL~v;I#shAj>n0vaCV}Ajf8|}%;~`oRjw(GZbK7nY36}{&%j?~S6R>& zK>5DO6S?-6SN!kAPb#W_t-n}k>lH1$4d6pFx%EtwDmUwvkAY*?G;Ge;MvPPt9q+yQ6zh}v zbsMo$RT5Qq;;^|Iku%*DHr-g^Zu`$>dgNotP)kvLUGH$`S)L>b@1wGpIkiV_^x#Tu z{xN>L8G(89YhKNp1>>Zo%U*!;;}*AgR%Ptw4%pvqZ)@Eqw%QBaI}S1-)nRm?ymfJ3 zWG6312y1;OZu300BuTK9Z_kZJ2xso<#{wl7Xp%t0LXyKiFPaTm4k*kR)3t2SwU@8Fy$Izp_S8K8y7|t3%&kME zcej;a%83T;>U=XkTwn_ypl`-uRCdYPXB4xId4^zb!tM-IZL-n(RtKfcDBr%!Ocj_4 zl`Xlow2uz06u7AlHIO^FMdAKUAlbP2=5tLeK|iCUyat1ztABn~Mi!n6HI8&TBRzbq z@Z>pDlrbE*jY6n?DUXoS20Hhm;j|8$n0`wtf(0SXXPXV#biRK(XyV;kwp34m zpupnPqA)m+afD$vxgOPlH(E!}1LU-uNk1L;cAU$93`mH%x$-6D6Z&8Kb!3 zPlcnOMu~mO+LqBM0*w4_5zA!_-qH7}8d@tijM9J~& zXl?liKShC+;KPkeT-m)fuC;Pw;P-^bmll|Y0y&^1pyl99>d7rAfB+i^RmkLRjYaM7 z?p*fLRGd$_gZ~0>$G3~IV0_w5J6mRwdGQcccOynBxMo>Y^F~}4al(zTqVEzNB0>@Z?QWydmf)sOMT*uZ0;y$QR-7<`+> z_V}6HuJi4wD^F*E9Sm7EzB+zr@38LFhYss;v2ZojXH;F0$S{eth_dI+!>3dToIu@GGahsJO>K*t#mh))Ns z&gn2W-Md~&;0qJi?!SX0|3Cm9iHkB@Vlm0aCp7!{Fq(@45>(5EJ9Kt zMkpKd(!>Vit11FUtO zacd;g@gF`&ZVFTfv{M_4wOoZV85JDkei`p}YN-N995|K3%k&K_vL!=iHa+3&(|H3= z)5`T;iJr^Xs=fnRlnnt=yr|#!BkoOusXyurg3e{U(o9m78cUR)OsPV%}?4 zkwz^MuT%4c1_K!*%<4wDPy^CQrJoqMOBqErVyWsIfHS{xHL8;a-fnQ-#-H=JQ-}uu zgQ_Q@nfdbcp`qdKgFH*#_Q#vt`&B;ObBO?%4)UC5pkLQ)VGp433tqRVDwp;7%G817C1AdRC^!>z-&kV{534Ah-=72ZZ{1dLAZ~{E5><( zJ_`7Sxm##<*^5Od`Bno?Wp(9zq1jRA;6oUEW=zW)yn_)v%VBU3s}rnRRU#X@Jczo3zaJUYZ0e}T~k{e3QRl5vkAe$CdRY2tYFC&!In>5%A41@1| zcGZOIYxz+8Q~Izwjf9ReT{)YV#9x@`g<29s*9nX0Px~ zgw4SBwRdQ>T{ZQrQYB-EO*nJ{u*(3teio*4k2Id?BE{6Jfd2THv_DWa7(aWHE{l2U zk04643^HCsaRkm{7*-186u=~x)vceMMBp&7V*e!;eOR=`Ojgq(R|b6X(t!CC>k9>H z{TZ=11|S1>l$1FtPM(6Br`Z`9@aQn(Ze zy9eZ#+d@^M*|CoDyoZEe1EcRcN2(JuwmZl3F8=B^F1$|c7!7bc3}X)`If6o+UUoc%J1I1CP;cQBLNRKh1)uCA3=BgeSv z_C!uuD%_~(9GTfZc|A|geqXlB4GZDkh z>%lds)(6W*C=h`6pN&j(o=pDDB8U*+#S6<#HBL>6JqS`v&}7|_Qdys+X5#X!%MR2g zTe_^Sdc=?>L8CTR0Q&-eJI;cR&tYJr>w+Fzxgg%UaA$tHZf zNamMxayh`50C7YU9gLrHIn9wvHrB&L?&dOc7${1tmc)SwK7Qh9a6w1{h-Ed$0$=52 zXOlIbrXiQ1i$;;uR*0YBMXI1G7S0nTXQ-9L8C33EyAUJ;lc$h3EKQ0oWDyr>DF_K;@fgzy71XUifH zYnA(NnN(;~x%bXEN#+S}$@>L#%Xo!@OSMX4`K+_>J1-!Q2;MD{o;c4LrN&pD6mS;= zFt)K-mW#ujuH(cW1LgyTIh}nV&M02h8Q=QhO%r})s*?yr2j>ZVP}esUf9j%dEsU>9 z?uKXf)r-d9ddZ^+}vllL-3*bs6PN)&R`T!sJ6gzB5WOlnpexM(k7L~m^Z_CJEv5*6W7&3 zY%mVn20Xk}?wJZtd7M@69i-7KWPPm6H9fz>aS)N^RQYSb-#d_2Y_d3X47Y5bAip}o zDv}>HYIKXH%(&Q&7>oHf4Ha*Z8*xX4lP{_NtOmRL%nWob5{yVL5at8^u4sg^2vGLL zdjZJ!rdX9zn2bC(&_>T7Ru^`T&)ew?&gp^? zj!bkL0S*i%U?~95_S@%X{)7Zsz3z(wT2#<*>VA_3Y>sVnriBTmD_SmyoFk?gk`*VO zuMwdVq`h;!DFNLPr19jKFe2rXEhz|h`Axh@(eyz8l0xBk65gF}>KujfLE3;+jZS(= z4jk0u-=5r{URElXcCeDm@2?0$ci#@=4(FC;c!d|yLg*J+pUVN`Ve9I@H@q7RK-_Y2 zjKqR{T#+wo@k#{U@|!pN>CAlxpn59Xt@5{yOZ@`a9u>$LM9{(5VQW6@RQWr~P6GP=nf9P#|AYqjUJ#O>aN%MCV_$C8_&N#aRwpgC znBXr!=m_1poVSLk+J8_F%Wsu8uHr*+>6Kp3P zF&?1zqptZ!XccT6S(M(k4i0XhKWb=?qLI(M!37&nilwn`c`S`CEq-o`opUXFCl&aEpu*iN|5%*udX3O25ki8dfRo&9HC|IlB`XQK{0 zT-f*HCi|u&V(7eljj6FkTZxGB9_twCdP6Jm$ZBX`0Ai#HInStNKhk46fE19dikXeI z#V3m!JP#~RSo^~F`%CM%0~R$w0a2=9Ta@tjr&J${#Z|q3#uMcO2&h8U(R-Vsg5?*1 z7>_F3STLj09y{mPc0tuHsjXfKTBo!y^FaM$wAT7c-H1+uuM%6MGSRmfdMSAvJnaNc zLUDx{ri^U_5tfmXLaDwJP|egXCnk`ftC)0*lv1(}f?z!3RAZ1lr_n?Ur5ZrMOw`5# zOfo_ADqhXSA5z|bQ$xFPuvipAT?m4esg5wVHf<61LMq76ux9YB?U{?eS)(V^^^J}n zjoRP4mdCgRBHIV36*k1h#JT6_*KMTERe5D&&|(4FMiuNkXO5GV(Z|>DI1tYmjqv7* zDFemIfZ6#_c#gZx_W!&`Suvo2=|1|k@n~3-*_GA8jE43Fze##Tm6xZDjdFW{U4yn%bW>Lb2q8Ev%2bMbFD|(#S3KzCK=uhUg|(43I5UGW3Ot|**Xhhs;l>o z^7itJ>m9)aGp$V77)py^>n1A+p`qQ)hn&D)+RP^+epV9dp}!g0$U0h44<#dG`SA$A zww!=8?W-xCUAEt$^WiAx&C=l;)Geak|K=aS9=|B?z5`p}XIjquiD#Q9pXH05^7c$} zDjXoX+p#@6-->aI=#=YMV^Ku_X0*M3V0fhpvmgN0PKrL+_go$|ej=aw9BEjpyPys} zpe6NiX3QcHrv7 zd*lmGGY}~oV;wfRoMb#|LzatH5)ufR8hh}Ro?PHR;xJQ z^|N58`pgmf_Ve=Df9(NNk*$rQS)T^FHE}H<7P6G*ktnU|ci4Yv1lz9e-N>nAo?I8V zAyv)D6cXs*>}?xHk`=%KAb^&2Ory$IY;;&BIp0Zkz;S4k05&;uDjxIw*H!|ce2JDb za)ACV-Kh&WE|*K_fJQG1F2$J7q)Sk5eeSS~ib#w-C*EAF90Gf0Tqin_jF$7yOLz*H zrKsqj^o)+D7%U(#)*H~<`Yi(m?#j-2@QDydGe^%H2IDom^Wdb;sU$lu5@z$CWvk?w zESle-uQzyTdC9Ra5JJl?z+Y7k*n<0CO<4TrE8*7&UO5v<~CfS~>dyBV7TiNa(2$9OON39TLS61KceYk_|usWL0yV=kotsneB=aU$rjP2)L46|Wksa?57cQN}qGBv`f{ zb#w7w65R;(+fa;_hb?;ztHp+t)rt2#ND-o>beY2y;L~-pThPYnV9q!Jrng0e(yV3= z8{T6%`^tvftq-?~8gkrD`I~u>gYb?*t9>;Zm8a!@3skPuHu>b_-SBhooHgT-JxPND zfVqKzAmkNRFO;Cgtxtb7f}FWXvz-4**zqbH6eGDNs_V^{wb5pI#-o3b*LqR*L+zba z$a2x%7Js5yAek1QF=Aq*v#}+~KbuIg0=LO+u{h=bt;uc7GGpXO{O3F6IdY!)p~R#!?09;-dl}eGm!_5Tfa+@5YchAk z1RE^=xZmc^@(AO^jML+uNiiH2!C~G&xV@@4vml=epxV?5t+Jv-dpXvA zoYo>pkZ|f^X%i#DeXPAzyh?5t^FuJrt4qtLU5ur@qVJRVGq@eqfahb~Zw8dysKI8? z9?xpsPl6uvLZ~+ca0>G{=5p?q8Y?d=HTtWO(q(k>zVFq4no(vWA~Ab!FIrVQ7wB1b zlf)7v1U4ZU+VHhVHPk04C^!_|iNE6&C5VvA62^bO+sjr=PbzHAnbp&ph38@4^ud64 z%~i`C#%TApee(DU=D>(&vTp?D_@-e%iGBgFhC5fcVd~J!Q-J$JeTU-a0cHl(l5ENj?eyQ)vL%4vnit~@RZL5a!D!1T0RK23uNd->YJ5asSS7A8d zp<-VU6Ds+2QZBi`0wJ#@>)0tmiWvkCZ z`l<25ZcW^6cL?Rl@YALI^HWHZda(k#T0JvHca%JFe9rzo(dNnH$=&}w_lFPy@A;DP z7PjSO>ew5BbM4kKi+6!%G38Qm> zX(RprJ^$?3DEA~NHsujQcD*5fuTm>C;_B*Mf9(LXZ&S+&j}2x8TsD3z6>8zQ0O(3` zIF?yO@E?nzv$MUvcYD{Is{@Zo(i}e@W4xeWV|!Yf@lw;GnbzRHF~NNV@=mfc#4FL@ z{U}iYqpv-IKIHzNcCUz(>lBxw}aiq@J_y7Q(RAl~##rRKonPnq*a)#yP5-z@gw*^@%Nf|90-7NaKI^>72R#BN>j(y;l-_ky`DM?}|M(z^M35L*X+& z7QgUbb~xW3*zd|b)sbK2Tb$RJ1+JCqmn8vtt-m&gqqq+?W*s`B6#Nwh7b_xUjb^+D z5#0HA4;Y-Rw3m2S_p?iUkbQ`&<* z7ABm99nfLbO9z~0i~cW`p(bLDUOcV!&mKjEV|qu5<6<+YwcnXg3(EZf}u10D!@XNM&lS@&hNhy1>> zeU6?<92DdD@;MBI)bV)sAv92(^LtoVcm?@m(Ts82cxsv$a9HM;1!mtiV!=h4 z$4#IjA29?*%Gej=U;LC-(uqU1I;NgTIcfvb^IT}SLtB%!-fuc2ec+gaAR*zKix<2CjLOBtVbuspFy?W z2k8YqinIKvc*KoAofv1trc%ztVrNQ35-Q^yd9N2rc}lM+-WwU>0b7bpOj5MBvw8}* zVJ?IG2uND)^J_E)%{|J2Za{v9jKKJx|E2kz{f*9Vm`uvmJjRb>;VnNl;g7;V_Xna< zx@UT?>9Ac+I6%tzO2zKjS|XSO!lcV0H`SanGtW7$+`&DqkzZ8sS#xv{jA8%8&Mi2S(i zjO`kPtb_Rwm+~LR!n?2dae=wbIuDYddG6(YX6hXtSVR^>jj?5V5(j~$4JtTb0VadN708Zt9^7Y9|CQ1Hi&Zqh+n9_e2LF3GDyiPU z;ulSqK*^^TOBh(kw91m4K(sK4R2C2pp(PTP0 z8LP>)_tRPe80%x-n1RM{YePG#n9!D%Tr?)*Hv2haEw;#NVI(m-T=YNGTMr8iNCLP1 z*0{;Sa}RWd{tnQaQmQuoxqRsG>j3sZfcY;cQq}uUCq3t{f@7xkw|K{OvCI3m4O)jV zUpHJ)3|D#!$$cst=FDY&NPg8xl*mO=oKW*}F47=De2)t6AYb`C71qkXVsVciGgx$*sm(GDNu zJzfwBF~>n&K3`I7@r<>7sTyDp7M#;SJ1ZNL%U$DRed54Dch<+7jHba*d7^s+;Yvp| zC*PlK=8F0z@3yJB`gqF}+^e*%2m=Yi<}p(wcLu$=3RP8+eTtO_?#spkl6WxlsPSLJu{;dZ}tR?#hSxv_F(QHW7&Q^}ho?23Pe< zLEP*@dl1`~AUSXPLLM`GdcfD&#_zGRPNIif97exLp_v#F^|kai4$1p~J)B-&NBdDM z;V*>|pDjn@wn4FCq#TE%tsZVFUZ&6pfgR2H;gh7kcwcg2TVU`O?1ZO$H7U>DPNys@ zWM%1pt{U3x_+L0kg$)VH2#b3V*idjxt2+8Gr)yp%e+S+ferS2W$tXl-=%Fe^aJRUc z8>atb=%bRQmfjsBThzKU_f+HuO)=X4Un(3oP)n&e<7{R;# znCG#};#v}4kUz8>v*>F7^#$ciUhZ#2IwaoQ=&zZA4o1*xsR{>^Yvw9^v}DDgbJPK~4X^gK6ZNg*2*|9? z2dd)q*sxq{L&x8Wvo36wN4kozM=sX+qWrB8ExLrM`}T?%`~6O>Cvtez#-9d1M&&)AB(_lgH&@#K5^wY-FHA%XtmU)-GrT zd;pd9Dkt`^H;AOT!EQT2Xjq=T#fcKv44($>7s-p@`AD zmU)cV2Z&aod}w(!-}cKk@-a_TJo)6?fK%_XS`WC&1{UZKnTuD-m8PaZr6sT_gNNs@ zctdB!CXiv5;6~lA4ufAMzg*hR9(KEr)A$`YVaxdhU_m)aU-bo44l523qHS3$AlTAM ze01~Bf{KiFJciowxJ0M(+2_&K_r#^94hnXV(^6DaX2R{t&tt=!$~{WmewD`}LzFCj z8!*EQIj%!Uqiy@R7wes2^yD$`bY#vz@BaKoHVgyo1{VkJ7e-}&%*$aZt<6e~=vc8T z4^1#|e?D`I)^y?^)1}Rul!4qam^)AnaC7-vL<3<8oq|UGw|KM$Zd`n=^*29 z+p^k|h2W35|Io{>A7^c{$`u9W5PDl5)eROAWvt-)J8+sG#ep^{`tPD;k);NLw&tsf z1EAgc*qF-wz5Tnq?o7vICS9Z62L&-Qh-GSSnFJ}K@G5MKio8w~5V z81rr8kkP>gqg%rRaQk$r^UYsA-(t9=RojLJ9@Co5xvIb_9-G}yar@#v{o-A~;h}(V zHS>ux=OjG|QcUq_*@ z>I@Awk=NAUfba zX4;9!SR-w)d448;j*=&@`~TD!^hB)K41Q2uo8jVxqCsv1ezJX?UTKZlSS8hz@T%`k zGTFw~7z>k;TqH`40UK?K^D7;ALiN(-j!HtbXOVw;7`|*3w6E}wljo$iA$OnXuFd?H zH>ekIGs&gEUcDsMOl!+zq4jRB>O|pHoN8oX4>Y=7iT;=j9PF>bo}N7Qt4dqmGwWPz zRnf*GcU=JsMD%sdDy<;Os$-z7J%V@phwoCZeCDi}(Z1sW$J|lwSWtNB+^mqT<=&>+ z-A>&YeY8usZ1kil1;zscW(dKF_D#0eX35_;aR-8{+c%z6?*PJ)c^!i$z{U$4>x}I# zUJzlZNGx#YtQ(i*!CCI(x=kJD%2dwS+7OqeFiYmYq}{RGgtqj~THm`e*n~$i+BD49 zmwRx3rqxf05<+j4JUhlXA1CRmLfo-<%K{B_xegScma-i`k$+)8)+FRiSf`>f_4bsLH(E;LB*-RiTrKpTKM4qbZ^Y`zMtI zt@KyWX|7)!sV_sR1R#YosWe{ohA7z6zkt_kT7~of&ZAUwCb*OuV3NUBv%`n$DH+Q1 z?}vl;R`Y_$llVD^qrN>`BRYUPcY#%~7Ss^*b4{7a6gsI2mWB%u(}zv3N9*99@-#9C z?MYx+T=zQM!hg(?@h#boE7!Q^Pm4$V>0sVATLj9Y34nLb>$0H zci8;(xe19D#yg5^s)~@H8$0$1TI@sUMle1%E>@%U<>w}@u*CMHh zlZg{Dm?GKKpzV{y@2I3$77Z>osEmt}^ucU$;q%-`t z9IRTl@*L*vvUygjlF5cMFxxl6&5FvHC#8oA&d)#-&~gsV0~aS)#;?EoZs8?Ja`(?X z$j9#Rv7@;G7Y*zxVvuo|68_(eHc|h(L(wl1yYLH&v03XS*$Q^%pT1q%}jd?Y1$7SFFiIQzG8*um?i6w*o-3S+W9irN|M#D(Bl zf4GrH>9crt=IaUt6@M#?;Iy@DFlTmeBi-@En8@aHcTwADKKJb(>)=J}L3QK#m&4wT z%ze1^wzvgP>hn73$a&rl;-5@txWaK&(N}p&F*g<8EkU4Y8!hhTcG5bZgi{8r zE%-oPh*LMsOI6~{0h#23zgm-~Kna1sjPrLD%2aRjI51f}P&}OFcmYHU<{5816KCgm zA=qMz?+WDXWy{FQAj5EL1sH^#gCGVnXvyOVArvjPt>HOJkChl*cDA&TTYhYE(T*Dp z*9uDa@%!bQT?X$1;}f%5b;H-*O7#A4-fJEzVgBU5w&__+$_!9#_QvxL8;eXmWjiYs>PyzWgXSPQKCUyuHC_o%K7z z57rLyYL@JRqq%HEWSVlaAcZxu%1M4r?(;>J$xxHskPmX2+qKNF2N?wX4N z)?cupz2m~(yI7pqBIdVGWC-ag&6=r*cf5tmhXwZsuKm|JHJYU-scO=%YoiDmUO?kv zMF9EU)<-xow4;{Zu`NAj-+ao$R6uSIK&A6@z4STdvYRR4TJ6xhh2Y#~+Ei~-N(g50 zs#{}{1^_RHa9>UMt4OO-UQsIegqLtB6E*#a0ULq6wP*|CIeGX=grAwv?)MgE4X~ZF zj4r`@Y!30yPsFh}^EQJqw)t)~<3@(p%~((l!E>7eGd4EF!Y%d|)nl$a)+71DgTOm~ z-QyO&kNY3WuAcL7O3!~=R$kiReXhk>tuwThgRO3eJEvM6M%5;K_HOd)mdgL%e+6RQ zsVJ2h@1$@4?oW3+=!2R!v?DzAZZLFtITFsU+HJCjXm@)WI^*34+@HYMEhe}MMrX7W zF8#Zi5W3k#cGQXIDM5+g@%s2!ch-YBydOLJ*hf6IJwS`O)1I z7!!`MtGdzsQ3UnEy!hm6*~-8r=2MHHWep#zutJR zzkd1N>Z8lm+j17Fy@ihQv}d3fEGo?Hf`BvtxhI=!p<^93=Syyz(X^++TWhiyVt?!; z?eDaY^I#d2WsSxj^ZL#|$3Et##!rL*xsB$##j6aKIl9Gwk>nk88Op6=C_5|rpM#DW zX%2Kn1=;|6wg^B;JZ_zdHpxCTB*sn-APFcpO6|VHHB6X7KBY|Z zEB7wRmSo8YUmkGAzd06EjrWTR;=1f7o>z>~{fFH4WSj23op7%^ySE7lxiG0w?x_Qi zbmfaWgTn=%evOKII4jmm-}5$Z{@Akb=0LOSslbGtrDG92qXD;0(dumDt-R}?yxcOS ze+2I%^)|(Wz{Oz(f=sj0K+rq!0}y`eX-!GQb$#QO`+gg%b{`C)JFZ?57QD{qaF_K+ z2X=1wJrl>DpWtFPI=D#TO4vI6rM29~HiuQ6uhICXn{@{_koK$O&+}m_+#p_mxa$5; zG`DY;BQt;ivxxESG@V#YxXO^}%Fp33(m=02_xGuFeRh!w_7Y~IO~E%qb$*09W}4Kd zyfOEIa#SVoo`1pOrwEe4s$CG>U}R=E*rwXsh@Ee9i||WDUPanajQ7|NtHyA;ip=@+ z9*DuOmVs<8FeBN{s4BFsG>AmVgQO)}pSp6;AJ(kH&N>19#7HDi-MYR_{Rji#=F`A- zPmb}WRX{7z`QkKbTce@VsPgwNbMK~PuMVA+bjNR)nysv!rLuCw0em7k1a*1sp*3lT zD*ajn`^z_IA6n`ZXU#i0O^Io-3+tCBQ+@d?{A&Eu#^=yLZD6bPR!&Cw=%g^7ctOy! z=E;M7<-QetmZYNn#nnh?;&($8S3${Bq$0pue%`(Gzh+}44_NBpEvE0qS5$wJt5M=1 z7rdIi#~`Qv+F7{d15t=6%j6c0WzJ{!QS?$dJ!_A0^~`JZWw60i=6@m6Fw={w}#Q)Nfz< zkG!4DzSZD9>(b7TQ1%QkV+75G)irrWOy|_Bj1FROjlrt;yMM2~$scAu#P-bBF=s01 zLHSeF<95KG6~Io?)am=#D;i)6!&Q%eM_$2It6_-GOU*>MH&xxp0Y{^kX!I5TBa=WO zBYHY7k014%s+ZdhyN~lei~q;8vPWU#fafYJddV?O$B=smyYqPMhih>cw>ZSE)Q!;3uk+}meJ6%1rn@?Ss0Cpz zqd_|X?7mBBk!)Kz&|TL1vI^)}E$c!VV^k%Y7;rt3YWD>d0GT&+00GvJ3Xaqq_eom! z`LhW9w0QaxbwExch|OpYV1#L24KUuPdBv|6`!A5C=lj2IH4ckMgb3wp^G~$3-!F4C z%(2NjaEL)%Av{V<6Lz$ZS9bPqAK1ZcGwj>OmA4wUn6!0-_Z}=W*F#)9-1X;n==`!F zw*q{LW0kvf&d5ro$A>+QEIuLw^?W8gskuXfu6abw^Wn=X_ zc6-^wm&i`e^2qt##ZW5nF{NM@YVs)yS!bo^Z6KJW!s|O2H{~cRp{`~>!PI=oNYiE; zGwW}&X!uDMAOPe!qlQt8@q@~2*tb&aS0;BFOl)20!NXl%4%L^nE^=3I?BL#OK%2iH z59?ym)d&&^(8&v%vWaJ+T3N5Wb49yx8?RP+tjavQcMs?triD^!T9G#>e$*gJRA7&d z*2$@D1D=TfJ_)mmYb-GX5aa>$wbzC%T-X1H6eo3g%;zT{(34ibZ}%=gq~{;dH{v7T z7pS?&DD8dl?MM6rc5(#b<+dKs^X^-W=x+<559J@$)CoI%Wa#1e^F8`~zPFqvtY}WH z<==Sw!C3#i=H>GZw!qH@;AB9YeVFF|xXTcA;h24}iF;ynmjj3@?Y>{nab)@iFUQp_ z{j$Ipiq@DLYh2(l;1L(TsQcFZ(TwJ)%!QySSf;3(3r^~u-~GRIe!6XYyD=GEol9X< z4Sj1L<$o%mLF6(Qt+djcumbo7%teW)mmJ)s&zRAyuhF-W204VN;n6d;aD+IqT2z6VHM-j zh!*hlNeu-4aH?S7XP(AHV${?1=&k5td@ah}8ojq^wKiZgj=|ZXC1tmn;Rzsu3uTM&f6w!>NuYAJtR2>j%M+E=GA z6#KE6`LR!3hgI3x5@<*^vYdQQ>(nbx^f=)FecUOXYx4W<2QR;gHxl$7v;C~+6A%99 zc}{Sqq4jlxmBB%m-19?w9EF#aJkkeK@IjF?=M{^B7kTOFO;zE2O0q|O?rwRqSeg>K9v#0|vUqTKMb`NkXn89jt2nej z?TSiR(Lk573il$k$wBkWT=5>`x~dslrAdm&CeVTVjd)3ilNqh7;yS?Cy>wc4uE@i^ zA!x}6_07ljxC6>gC^oRzIjRh0+ePr4EtRE~cgnJaCWYasFJa-)v&JD;zS97QjVQERcZ@FtlD2MxTj83p3_xdbH0miMx=`yoU

41 zG6EYTwZJuK6fb`@HLmBO^69ilP|BW+E66B%8VNT78zLZexvC{s7{%YiH4qzA;(9bL zS9;JWzsG5szfzQh`Z4VrZU63_0}=GV4$2QeLV}1eBgV<4Eh^kZ;V!}Zg@Ph_at3~v zilh8w@fk>2zJMXdxFI$urV=)U>4qfm`%}s|VYlvFpx?KyX=uk^o-*vOh~BLwEXar! zle>4oaE~*o((Jphm(~!(6DN2Z@X3b8D4K@1R3HjzJ8un`W7}UJ7)f69bq>x$jG9)l9NO?BG;~T+qQ*Z$9s?bJxl9!82L`B>_5ArN=eIWy*PVl;bX}hH0 ziy88lYuERAd9COJHvS<61yg;)5&tKY=zcA7V>xIpNU1ocOBkB(XEb zZ?B1xk4*^*z5N;8rnXAKL{K~kjru>5&OM&#_W$D!v5Krx4i)O|Msl{~G^5<*TZ*F6 zxdA6~EL z3!aXfIB&8TIg5+(p6#wbqjxLVW`MU;vX=T6**v!x#Z%6^;GhF=ln^kZykS%xmow*y z$0j(oPL^*65QZt+!%;i?V6eu8ft8`fbI09J;<- z20JDLmTE$i{@Zym_({254#$m(@3ZjeD` zj@7+12!k;-{<+-?`@$EW2)#v}c=>OsTfwlb%b&C-b|X)q#}tVIt>wY-3TF}5DT4x9 zBsu4i4z>Pwb@DLp$uV}TrGc1t(k*ahBF(-(7@Mi;Pvd+^geV1HBKtiW%A`C0+uwYT z{>S~#Cj>@AdN-8v$J~apKR2Yjl_d;Rt$fL4?5;=ci9I1#&E3{vg)U8+FNa@_@@Y9n zp)lEHUm~{RGc42l!bZw&DN5Y;lBuvM3FE;A;uOFPNRIILyif)SyacFp>^F{s9#Hu-S9S z$nZZ!PL!n{v+~i3Ijzf1C&v<5((Z+JsX|>$(?2=fcpDZMjuHDK5fqy?vN|aeK!Woi zV^Xaa_fpAw$*Dm~Zc<8K7{m!AvTlKSKxR}TaE@4dSgB^0Rk#p&;Z1h}2SGUi5H=f| zfw&QJ&dk^D@iW!d=L2azN9#e1VNQ*b@19Xk2JPXy%}OW!7!OM(T$;Wg!fk;hd#`A)#w z%UZc8Z<_bpe%+GD^=hDaN_aN6X0h>EZ{suboqW!$vF=(Q^ZRGxd8Mk=9%j{|-{RNf z%e+d=O0;EhFm41JBr~&aBd1#68dLEV2?lPsN|%2@lBZIY^3k(FN(My?Ugk-3iap&5R9nF*6M)pk|N!} zW{}wjU?7%h)-uc$yqy2j1-dn-92radWP!W3GvnfwyAy8iQ6jN%Fv%r3rnxyW!c??k z)2NG8NpA29!CeIT2zETFFRb$E8A)%!6i8i9xJ8Xd{=ORU`b5%jj5|>!wn3BhF%gO< zNfM%d3oNtxKCPa*I2Gu})EfWg8moKI6#~OCc&o10(r#KUD0XuDt`3+b1tm=n^F-Oo z`R1&hDw3oXF}3Z;SsO~|LUwo#;#==6|bV{3EDJ^nJg;^6xDyjD?>F zMZFQFWw(s|#U5cCh;aa!1S5lK>qGEZKzz7puK;F177Zw@*TrciwDk?6t+<=M%i%U> z{52cF++JYhS*nb1oBaf1Rg`&9HiglM~t2fCJw3EQQ#WtTO_naW$)R^*DAcj*|i z)k@#V0JE~Kr`(j2`@Q`vvGi8e3jl!}?D#oO`~mneBdsm&>k02bcB6E_Tm2c-YQT#V zNzH$eBYljthve%}79=7e{5X(6pX8Xa)>VIv{^Ww$2=ENm5fTNoZpH;K$~J&*(qG}+ zLY`>BD>SZ7hTMe2lN?f``S7vjsagMcMee+CxVcMsr9%xBqe(a}ovwe2Y@I*?^^i^Z ztbLodTM8~iAn3`AKA1(_9r*!yG9qEO81hl~;Cb9RB<2q6*mbJ6L$=rvQzy(rud33x zv0Lm`8JFgxe@k6-aLD~$yGi_8-04o!r!xg0t{C7ghoD%R@7s~V2LLlxc|VnfjM-3h zMP7x)Bu+i4Q5r={iIBrh=F`I(FW!guEzmv_O6~alq zOw;vKLY*6K`XhB&JoDqH)Gc%WI9(&HTWI+9Mdxg<)cz5K68l3Ugrk+&94y4f$;069_W^sq=AEs z1s5BG%}u=qUcnrJKJ&PD>f{d=zQ_*Pc`?kuQGUW;@J-jK3UGn^bksV4B`fS_0aSbC zp?Kz>OS-0ik8w6?KD0#Cf+xrQOf9%Qw=*ZSs0d~;n3*`R*2Df54i!~ItYw%i98j#> z=(OUDL!;X3&Pj5}eiTT>-qs$%)<gL_*EPGvS@Yi9i&?Qm}QBhh)RFYtBOj0%kq7?9b>4x?}e04*C zB8E}n^g3*-DeJJSF-d3rObRO2TA9A$`Ort{L@lJ8cRe+S2}$VR!c_Ga40CZBd5Zk# zOA?Qj z2wM8EZQpCk3{mzSXS{s9dQF`6#rLF5g-2RUSU$avsMQ#kyr#g!ByL&!i_M?)6(QU! z7vy0S;oDEpR(+(uMQueS{q!-;mF2t>O$2+dvZ|Z~zApO#G|b3v-w=OdtsoHx*C_>o zueZMJGz`VCfEnO{Rlv9lhHSYJ@g&Cz0%-E!3XF@L76+vR>s3~BG+efE+PO(`Rx%nM z`=_g5SY#{_Edo4>13%s(Mc6!iak2&WqM`2yc#8kE8{DcA0{)1&K6 z`*Rej5{aeDvjS?w;1O}Ecaz^^^D-y<9%s7NLE4(ObP-!THY~7xqV+FQWkD|Ca9$Wa z8%=3h^-<*Uttn~TS$!uJ?Uro5?edMZur_a9DisJj>?_j3jwkUDZ8gp1a(KC8|C|VD zKc@Gq7$EOKUOm&Ud~LniBX_H%Jz*0G0c~a=lb%eTmaqm#wgd(SC|F?gQ+(`QZPL@x zIxCO9M}Ro-%~Zra+GqZG_3PcJcN1vj)KIUkYt<>=>qRI0cH4MLFK>;s!`H#mY8y=K z312`P&BYkHuRJ4&FrPoqTUilTb$oLm@eKBKZ2s!VxEF0-*GWc-BlS=fqIqRU(fHn zKjxJW(R_5sa6l3$xR1U`Uv3aPE2TYkTSm^Ar&x8C`TE$cEzQR$<$oqSpsnTm6HmLZ zhUhjGg%7_TOm#(UDO%8O-MAT(39wTSOcw;f6S>AkQ;1cKe`)rPuFzfLPnYuCBki%i zGf@HTLMD|{Z67nYmasmlN9~nzy?frje40$fEj}oW`gBiau3ZKhf3j8_^D$2T zg94E&u4y157dh@6ZaqRT%Js|ce5govp3~Rgk*E53VOVC7$Yp4ney6RR`o1j|BH1N4 zaSL#e?XS$p52_DL3WXQEpceO+c^xAE@l&4LyYP$T=?S%r8V7K7yckUtYu!rQ5Y*!c z{%Zb5kDg&UGuHfVb|<7TDzS=7lK5pzGCDCVcM16=7-so-k(3d_Smv%)Cbw_jMsmsr z%ZAn*y#tN2MeTH}`P~T|K};AZZm9lY0Vzu^c#%#5^20HOOSO)6o>tmpd`p`Vv&k5!>zwUu>}6k*0(QkT(EJ|9H~LGS5Uj^g6iswgtl* z!&w%MPPIB)bv-Z4f*#aF0}JuZD$}$!SF4jLVBP|V_5Ic|7^-(%_4sLO@6MD7VQ{B| z1kc{;ck$c0DTI$bz86Zpvdt|T>6BBJRsjOrnqG=>{t`UllE8w2=467C-G0AF(gu1} zWT`rbhY-}BD7qxvb`ssm?F(>?;PiHXb$rv5Wh@W8?L^qDH7e2#{!?j zVY`aMOF80!B+zyzDmeZ5RYg3md;o-?E4iF}vr%piPpx3Dr005EojoI%jkW>h?p^z6 zZT#~vFraa%TLYq`HsAYBF1ZaHQo+8)yz*uDo|~F3Aor!Dri=-OyZJjoUDHUgFk1Xu z5}0APF~j)q=}VJPz9N`lQurfGj1HG1XZ*Q_zVei|@8t$EpYIW(x7S!aSn5yZ#BKzl znX}ffNRxu3ckMU*x_S?l4!d1l<~`+n!jt@LPu35VSH;yUhMUc|0BIl zOgx?QeEZ)iQ(-$H#DcLHCd}&b?e0q*O%(4lc?BgrU((txF+ObcQl0XiqZ)7b!gH2n zZ1L&G>MtNM=WI*s5QkG{z{b)W{H&N&SGQCMqPhd<5FHhRrki~Gy$x;KxX%_>QAdnr1(f6atVeF^iAVqWPBpHq*hbwDt&P_O);8S3UT{%>3Kc zK(Dzbg_DKS&D3F*PL>NOSLWf!6;yS*p=k-UjC8v6G=21fH$1j7UcZy%zyLv(-a`3_Rf5bFahkz9y4_Pe$y#PCKVF}n zO6PqTk;xwY$S*dye~w%F*tE$ z4UltWRAjNR7=Z2x&UWSN~FEHbPen z^U&-TVsX~4f(SM|q)TcMDPb+o|0I9hu?`k}qbK)vtYFToe_!+C9tU(geUskTLJm64 z>5uT?QSKmLc;-{dTcF%9@&TWDu+Brq&k;e0`W?APKXW(YqdWl~!<(YK(<59CHUY(h zMc&RIrLAkjMOTDW&8@wsmr7RDmi`ULRe=5dTE3+p5%>o_<^Ei=Bo@$rbeWYgwI`x@ zNecc?=Ydsr8Z>D!!b!?j;fu=mCO;=_+hOXDXn3ITZr3r}%gs`63nmC zqR|gb1xBU|mUyVSi!M=;oCkc0lHnkcZEq*e0UISC!l#+o*0R#0W!GXv(MK~BYDos* zg**U9G*cd^`W~rvzF?4BzcYkv@*JPDJ-qHej+UHd0+k;ifirnuH=|AGI6LQT<+*zc-zeFay1Ld}Uh#joKUB4qw{O ze9FCN$cy+Mnk8>~e(lYZbOm8B!wRIdt_|7+RNm12^YdJ_ZHI&ICkss;+ZE5zqM7h_ z`A<^Cha}!0gRZj;#hqjjNr&u^&q?>_K`&gs5W1^mn1Il%1g3w0JR1VEg%;Wu19#+W zR)|bP|EZId+?}Cc6pk-xt-u-BUA~>12*`N9SHwPw-k6RB-7bbwI$c_FNErkH0Z%Xn zCVeYlbLm4bQsYmFU)+4hq2aMH-%|Y;)TyKy?sQT_U)nte& zKmBirMr9E=w{~70>SJ?BqTmoD(R%+Fl;+p&4H@+m(0@*AldD$xm_uaUPKSga)77y5 zT;DD|Pb{<#6AKA1=a$z>6E@zlV}4GbN{-e7y7z(6%R5pMY^^NTcp0K)We7)YJe9Al%De!rtl0BycK}y!*NF;j07@kAbF8Om{kp70g?)qDgX}bu07WHy$zUn0j(l-09ER#v`Pkz)c2()hBrmH^sediNE;s z$-_9q>rRBa*9VEHW8O!VKZ|}Ksr8>7^If~ywi!1H5{`)GZc*crW=4j5mT8U)ig)MY zQzJlX>t>Jg&Wk`?=>Yyi@r7a5+o#gA>y8UHPsHy=u^N2UC zPI-J2=vbx+OhHtZ#Y`8Tr0E86tHqqT5C%zq~gsNluM9J?TeyG5ME}KmaQ^t4?mTN^i_u z;PO|Lm#gEu&CuLEN=bUSCP3bgAn>v z<|%0)hkKh(=k*&IC_rb;9#FDXWi$$#F|ROKpC1A|6Z@*09x-gh*1h0e!Pw7Tws(~f z=mlcj{FUvv%a^c1nq+|54M)mCgE~;Z^?s2dh^3(CfRzab{A*oR6S_1HjU@N8ovILg zy}66>y>cnwk_F3c@W!AKq_`7n-pw6PrND{h(^b_?ST9fue9QK}+ose5n+m{M*5l*H zIOk9O0hAGt+BA>9-I12bcJ z9Y|wmA>`}(35OxRg!fw=O+3L~r%;IRB9ZlgD4(F}gOAdHjr|%R2b!lN-;RG09-sK2 zZt!>n5)SK_x;+VcfG1<#`_02mwP$azppoclDRfVpknB{r)5gOtHDej1f@4d&IKG+v z)YAI+SXH#s%=Ya+?p8<9mCrbV5Pa`{64Z(G*(58e6T znK=3{BVGaH7b<0YBXUW3?&MzkTo5lLRO-_*c%3oRXTW72ubKk^rb2L8o8`uf$&?@hz_@o0?9@?2iGd0wH^oMuGP*z1JT(R_Fs2>yDWYr97< zKUt41aO3b=qt9=YTpP3I`1CH-bo-BTtWda%VEdiHqC>Y`F>`M-^)}dd-1o!tAV(F;R;&VV}f5bfV6pDBY}xc9cH!q3WziqQVW9MIGkhR` zXvj=a3)6nX&@(n*tTGNVkeSh+JDoj2FPtD2w|QQ7-h$CU;VuIuH97x=Bkr&`-9C zT|PVMdRE|?= zB@eQ!#t__SNfX~!0T%u5NCfXhos{$7nUfmj2E<^8|6RyjLm6FbI^|mZv)|S-$8ljH z6+rh2a%_a3frSzmt*FDh3#`gCD7=!c&YWJooveM)$d82|H_2%Cor;|uWv@jYS}3z( zuQlB~-=MWo)Mlh={D_NK$=chJ`l^HV6GAmRBjc4G;NktBj&{LVW??hMv`GsW|Ge27 z#H+}L`f(aSC2fzpk!n4+i-%qlT6%czW`;0}?$c;e@xG?f%Q3g4dM&#+3=AHDvY|FVgHQoZe`yx^?0G5ZA$=qts1YBp}VYZi?tBi7|Q&yCG; zd3^5qJ8gmVynN%CF!sA$BTt|vNuJ7hB?2`aaGB$R%N~(}h5O;{p%nyxMzzT4%!}F* z95YxFw@dcWJB*YMV zVzHRMdO4aJqst3D3xb*+NEkP0OmB0)>}Kd}zMJ4j(+5*I$0j@jx8(vG;uRHMWsiGZ zl98&s@2q?Bglp{m}i`$cSvdI8jvOL&vVBr-rrWN zwAou@*MyWDxm`)qrCe(Uf8S(-x@)3VeLK60d%LAC`D@mXyFf`Fdy%@oYs5T5Us0T@CF{0O*`3nRe`bbi+OUC)j3 zW7yiFMVY*SNhs%OMYO(M^M@|6h;7EYuVwwKrA-8w_8AXKrZGLIicz?HZ@8?VYC7lX z2~62qF3XOE_||PMWQ;uBL*wL_RRI@4m5)G5ZE-+{Y~wh7+omd8|x)N-hgw2*L^7Asw>GZG7Q$y&3T z>kAPxIiy8pztQY)V8`yj!{Wb47HmTJ;K?r!%~spg>DV-i6Ar#E6^;){;n!0Umv`>7 z{lcujkxi+?{$;DKy4u9st0h?fmrgh={?F53FvSmjE~UtZG)YK0VFnWI6Bks*pw=pn ze3}v$#zn3xpF+8({c1!dXv~JR2n|w9hX}{&z84Tu9)fBa(Br!3zRKYLM?wD&yv#A z`^+w&=Pybc96_YuI-!jwRqv%D+#NaqaKzQ{|F2kiB0-i~+QAIv$jV- zM4Ls={h$}-0KS5UDUF7n$w{98`ilVp07+kbP2AVVF8c3~rH1{$W#`25ESmD&ep#8m z+f?x)X72HFer#11j+HP9cOI6dic}sAF1d|i=M!e&PDKA3gK_aC|G0@PLSpDwi!4uS zO0~Je=j;Kx9pvpQ-uIIFKoxx>u0!IoPP2eCn3Ms_y%}L8nwk1(F76Z8q7rG$xqq)K z3~>ogJWF3z7b@280C!s|WuJDFY9>rXNs<%+zv|L~> z~&Ua_xs>J?4%lz-R%|ef9=SOK&d?3LVE1D`{EL>kgliOFXwtHfsaXtkh z>O=DW%GN003DDMW8tcvi)+a~I2ft|6RWVPLYJCo$h$<3qGNUBVF1>{d{CzymR)1$d zMQ~ZGR!ac1xvq(yAixk`%A4<(0baG(zM&()%a+UZ--tEY_eAO?ZU`n_$sxRR)1u2) zGXy|GJwMuHJAm1+b_X%Co_mu4+fi=Ah~?rjK5@U(r_>Ae_gT-Aijf}2o~ z=gujlawQtqSi=d6q^pjM!{|S}j>xc`Z2WvU%-vXEzt}hJUQO`uCdUPhexeJx9P*9K zt*`!9YjOA}kXT99J~9jgHynC%`yPON*zysfTK#j+lz|KwZ6wRA7r2Q6qkEnzp~w3g zTdC6r{xVnfl~JP_1QB|Ld~VHEb7`kKX9(ww_8iJ9Na_@yEv0^94PPH^PB5*Y7BgLx zsiNVv5W8V*%Ua0s(d2>;ziJCBf`eG6PinxT9?&|st%(Br#^kMAcDA)Sz<^xeaXQ59 z)%Kz23&EOFUhzoz>;IM>3VpO<5G-2z+541g1cG$tb}{URTkpg#tX0tObLoD3#E6Md zCk9mKj+DVQ$L>^-=M?mQ-YW=}P8I%t%5bm(rp+Btq_cNS7j1Ui1@^rl!w%p-#e=Dr z>~_tr%Ov-jmW~)6un;K2RCX~qs?FjuQOB#<5`@Hc#736RC5Fj z6t%dKOe>ozdZyPE@Z2~lc3#po_wOcqIPZd8pElmsfe#?zu#wteKR%&UFHx=ZrpiOf zmG*(9#RfT*y##~^@tnWH5)VfVjMrSNJtXqJ*B{1GPhb?+ESrBZwYpXi#AjOJ#{G)+ z9iK{`J1yx5vo4=l@-6)~nmozdQs6k>IsMGwCDx2Ask zrKd+5JGmCRnEI>>*52Y?p=z(|-)~k;q(wtdee{Gd?>=cf1tD}Nu&oOc4F#oDr%c=$ z1)dx@v9LZ_isp%0V26L3G^I*qbS%04=$_ zQxY^N9%NB;#Rkj~Yu>gs>V5=)Q)%F|qFaN-XbXlD?S&T~&g(ep(oj}+v_C(e)|!8_sc>iI6$A0X zc1DPJVCe+Y9RlctbRGmrmk}Se zNqROHm?dBXV;j&bT`u!4c2sp)ar@dL{60K$`1VFZcRj0v^xDZ-QcvaX$dcd@F};7f z74Eov<@Q$32yzRolsl{3fE%kEL=f|S(cz2~b#aA!)yGnmfk#eGKSXV5njHTR6cFTa zJhuQS%zKHN%3?cmO)BDOm;BFFGDWXMOzzJE_;a#%t#xJ$fmca>U&wS3_?k79?aK?Q zSL7EN{^O$lnOmqm#@gk5EBBA+j0u;?M7B-L&M;PbfiDU-GFhIPQ|>szmaP1rFeoN} zn)36xg)=`nEyu;LpJDbMiX))O8TNqrzr|Iqb5nw-()`(h;7#&nSiTxiV^&R1*HYWYXC)jY@`(X^%zvMlTsl`N6Wn}iWT!9moHmhxocPOi?T z4KpI)Q6mcb04y*b`RZxQx5Ri9(%>N^2Vh`%I?!tz^JRPIx-U|)<`VQVijSz=dbf?I z^B2lL8Oa4QI>)uXf1Y9DiQGO*NN$JKheP%;o7_34lba<2!y?-f<+_J#IORq;lpdG4 z1HQUU)Nb7YDS5&#mxLH6mhC}_pPcqK2nyV2wUBecdUuT1g$Ohe#f4bAtSgQ!+A1TF z*Jn&ZkLCX8?6A%^&UeTKdV-J78@rqa42}HQX}ZbwrQ!fo_kQvPL98>5bgkpx(z{Vx z^*330yvt#?Dti!dlgvV$4op6PZ$!Lm+#2gf#J$BH!J_m)5`45GfC~Ud3i_v~Q<-pq zF|kd1Z&s=*7q=7)_1IgNs-esrO@iRb0F|^Q6L8uk-63E*SJg z&yUk2F)u~7^wF+h6OgsNg`NFD1EgIZCHdCGvW?cLg`Cn! z{5i8idS4H((jx#v5Rr=`EqO1LN6-YxqedVdDeQ|qV^MaZU8#EYDUfjZAGAZ%BePGt zhT-PXBGLI=2t9LqaJAXfcr0Ueqzf}qSVQtRvK#mbAVo0~>Nfg!t<7*}vbD4;>j7#m zrHFhJd`aYEMx@GzeQ~07i!VBcyZreUU3aeiSPY~ZSS-v zMif15(LThB`*2DO(t`P2nh%-k))wB9egR1X6Gb(#3ToablgUda)JsQGG_Dnz%#;?S z>r_5Zit6Pd9a0zj9Cc3kKGq9Z*)De)4Nbkc4Xl5>jA7G-_#(@Y-74nV3=_DUOPP|K z^`#EXgrIBA&*`|xy)PV(^qnRh6rm!9)&ED^(xdwtWuasg;S7QJAA>v{t$O*v3)|e` z9XKebL#n2e-PgF2t|Eq?_1Fpdk^neBS3`Ah)yQSI=x zf$Mi#CF|H$QK>3?rHUAd(;*v@Q;`0W#+WkW#NmQ5iK`t6kwQ=Z74m7u)HzyawFO{A0 z^=~c8=M*r-v#X9XPt7k!dVl7Sua8MozFaV062h3OjlNo>3QTOB%5iefVq{J@gia~g z9{H`ZaDL!VSRL}Vqaa~Gh!{rH%oDXxUvYjDSLybZeX1wyLOdi3`P>;A(KL2f^S#*h zPB{5=^Yg`PDV_HnJ7e!l>EL)L;YhK=Y7g#uXuyz?g?O+n$FiplWl=NX`#$Hn|EJ_AOD0Dt*c7%0jlGpUAmwM@93lhXK6pO z%2;#!^|&n6`iB1-lG@0XrULgzi_Ipzeg-{qiPs*?b1f_r-s0f4HDGxS7Cyf79ftgu zf;A`0Kz=UarcSx?7t=O1%Ou^7tt&TUbX@d$0|u?Ut$*TbB{L6T3Zty<|Jy%)2A#3k zMN2mN&Y@{5B-8Q?MrgVXxQ-ygp}dqZrLA*g+dageL0h!uZxse;%yr$)U<>Lyf|ZXV zQ(0?xbJt5Nsn?QoJSP*lJ7OyXiVsukaNl->0?wW(f`9c~lBnqJgCTFb!E^*J&=URL z01C##I1Kr@l-Lm>{)hI0V@vt(==Z!{BK6d)NuE<-o@YG5uo90`$YTkEKHM{R#p(*9 z>I)$cuRoc3G4MQeU|9WmgUVss;>A4^OO|hv|8y)|>Fiaw6NO{gxHX=v5B~-csQg;L z6b@e=z&an>)tC* z-J9O;M<-Vv6yZtQaWts_5Yn;BExO6E!fPFu(u} zDHc(F?-s~^KkKpjYnHjm?eGToRecgnHeAcv?0w%K_w^gA6meD3Y1h(*%cn+MiK-yV z|N35WV5d|Cd{2vF8I8lk|FDUnH5o#v5m_!$-HT=nTO&NEYtYQo?6L!Zp^)mYHvn;~U*LLVR6*+)KWUb*0 zk@Ei~`=&WfN6&#-PfJ9zk;o>58Ue=VHr!+Xg;Y9N!F^Dtubt4q~dKQr3QSlk+mTbs=QN}G?3d(~S#-@{lfSRoJ6g~a-izsP%4 zyX4Mv$%2WJHmlx4g$_l22@_7R5xERLjI;8A7`v^+U8C7H^lkQ~X6trwYgiTZ*hU%)EfQW%K&Q2&R(xdn=dxv?rqM zW0|5^*vXD}30yHs3|mPrAs+{NX-6ej)7NDolg4H)8W8R1xLzJCe0F)*drq2u3+VN; zPZhX{9HDB?BFupAyA7?vWMmc^+7@KpFb!Y)gt{N2CG~>@;L{QXW{P1JJ~_O7KwT}) zB){Ip_|q0B!IG<4&&kH1fW~F74`b8!VmE5um%RK#l^IhzIO5Cdq%7_-7bA!3LTqNa zH^nAJBnT8@4>~KJgzWchE63`1gpQOkEbxCCtZK^~gvhAI&u)ZsG%YYc(buIW0vl-T zzS4E8cKV#_6+OKivRsvt1dKeb<+>GQ4a^qJhA7DDy&{Er&p(+PLG9F`z4Gxa*+I~- zc^afL?zHO4u(@dJMPtN;5P*!0A^Iop9WP(8{Hz-r+p@W@7WiOw+CR;+mens3^nwH@ zV)?h%wV+VkK?(LR6k+qLQ4iWTTY4$Zlb9P!g?g>QO36TZT!{A^1BtxD<9wABco4;2 zRQ1J^I+GoY*)8@g{ouD0@4I76eKw!)LPFfBDK9FBSh;vAv;Q$EMglA0Y#2Rv^cM6gSTM*L8Yi50qF;)gHLATGHl?M4jf+co6|Em|m z|9W@s(0Q^%PP62lBn&nPJinHbrN(JlP!H(7GQt2*Tfyefx1gyrGz!Pdm? zs%yT9sVHO-%U4QTH7=>nFCD9vvt`Oh3kexy6Qa6g-AzAK6}hn19I)M7 z5W(kBQ<*f$R~NWN|M8lxt=z)CrBOX~gR)3jf00o1V;aE*2$8G65+~=&ehkVuvo^N; zPVC4$i`0=@N9(U@k{Lt50G`}VoRFe@EeR9qM|9OgN-o)UOD5SD)*n_uE05_;5f6MF zv{g$Qk7Rp7{{wNZ7NFzjs+E}Wier;JpdKnqDKdYW$m$+l-6=k~Bs5wqgo#J|U{sg4rvw25Pj@Gil2mMF`|Bi=(5Iu zW~&6}BY-BOKZ5q@rY^CX?W8xfvlS5F%lTuob0eXoWo>XWPWL)`wiNJkhOw#E zH+&*7wHu~$8~dQ&p?xVT^8iG=a-Dab@7((KLv^SdQ`^WLlT+vf8Xn^Oa0-}~X7gy0 z$WKP=#2skd@N+clgS#?3>NWgHFHa_cxvA5!F__nZaRAUGX@PVq>$XAj-1|iZn2O<1kD6V&tmo*`k;MY&aF5f4`IMRymg#?wF?k6$L4f=~5hOo?WN(Tm z`NgC}2MwuP$obvkSp%7KfN8-EkY@_RtA2Uv>%sYod~m53BRaZU`dm{WmG8M!jZxBc z3hI744j?2x{~fOwtcuESz9QcO2{`32spR-n6i8@}O4Y$(J`VCR*1?8bw`{-b;06YR z?Gi89UQ$n5I1^!~wUuJ8tL{ya@OrrXtm#dQ{a4u((d+D+?-IC|4a8(yl74@>ZkIUr zlU@oxfTf+0Yyrhmm}p;Rd@l7{pYrx^*gl~!8%EN-o{UhVp)Rh)4Q#kf+F#Z9JmU-< z0P!v&JYeUwNcW#lC%{-jGMfj2Q2f=^5d@Vp*?$NKL=3 z(%X!xm+_NjPB*ULTl2Zd_w^o+TFJsgV6pPE5GLY-Tq=FvP#tNJlblQIqbUHRCJi5L zLFjhRS>1)?^tvnQM**-XUI&=3E0frQJ6tc!!Vd51FGHHW1!)RnURx5l+d-@wZo*4` z5gNQ-WuszR1zMoDtGU%xhP^X72XF(Qo;@B=7vr?1`CWz-LM8bppO_zcqqTks?8w4Y zI^d7pG%fpG$uwQJWy`9Q~5dL+rNiN^fz~-aPi#0EIwdn zzhRp6#ZwGaL^=MoyC%B;L-<~!+P{&UxKq0^|MDkS!0FsA#!Aw?N2Yg%5-qgPxkll( zZ*X9Gtim|V4_A{@_cp!qcsfRM6KVL=ID%~%Ogpw5?1rpe-cH~Ra7N<=sj?jW%SVwG zs6$I4ruy>mzzX}{U|Q)u`{oeHlT8}s zUs%aO0s&!i1#hA$Fa>=DB161OxqOS0F5gbi)~Nk_S!7QS_fTVwzrbLe^kTX$uT)@F z*SA-CceW!jl4kP-P=4rW5kRH`zSf{8&UNn|@>u8#g16nGkj`Uir?Bobz9$9*8X%76 zPdOXfuBeJ-G`8pQuCv@Ql3~xGc@d|4nY(r#{KKUKr$S63>IBA)G5}I3#f{J068VUi$2bP}CMKnp{L>AF|hT^``P3Z)jXyjuvDP$)vlHhFw zep$;@t;ay`Q~`~Za&CydB9bCbQxZ@m4l6Gu9aDT)k}~y&`e5oP!NuV)7K1AMEcYW3 zf)AU(ZyMDOhtp>X-969itRRGn>UKxm_5Mdbgd$?|IiN;1YZD_o>C$VFx>7D%)%tp8 zhx@+$Gdjl%?z*~mCVEVn(63MGb%P%ON(W~41d?Se z=3y=lJg}8q1eJ#^YTq>9HC+)Kuw7n0bg@HiCGGLU2mONxtfS^Jn3JVU6kFWv$!qp= z1?<2%fGvJC{vi0(1V8N|F*C#Cx|;;B*RbNHEHupAVo@apQl$)B4X2n%_Fw1&G88j# zKIg~Oz|PR6n&zXI5#zR@4YmoJE6RKKI7T%dJ%y~eo!_hfJn^+07|eW28pHeCnX|mX zJcEm09_Dw;@uMVK2D~liW=hQp`~8RpL(s`cVBQl1>&YlB0Ql4#?+8{2EnXbk&fK(d!A=>x6tP3xNvbdg7Z#u#*qAeY_6c(m zFXW1RYlLms5A}B*@DYby#^|fXU&g7TSYjqmW%HmZgghg3Vr=)1`Kq87;bVwnau{&T zjBPGm>P;L+Wb!Q(&Em72E>ztvU*l_>1t9nP7 zyvcTnt65u-6@TkfT`9j2H;C$TTKPIy7k=Ni(7SN{kL&?i-0vCiy*h8!WTcF>>l8H| z`62o)Ax=Xrdos|fkrJmTR1bgkV#z^Z;ZnXAWblTY?KnIk*e&U~otR-1?YZoQ_{UK& z0?PHHUDnBykuHCyMRPAC(`LXJH*7@w!ve!^1{UyVhqkxT@+vA}SoH4Pv#`r6!#CzFL`J zbSE274?oT!JH1g-OUU(k`yu}AQXap5YmNlP2!eH<8K~(dS#VUT5Ii;rQ#mO?2}0oh z_n*WfIGu(EOu6;~SgKOyb&nh$mOwdKZ*qP0%oe_A^CHMsRTtP-_56y#BYP-Dl#_3i zJ=fivK{_6cJz(y*;U8S%;Bv9rx^_{SgE#~w`JGz@ZG~u?D<_>ZJr=RWRGnRe{Tu6P z)nqM);sX*e7z{LeYqN%-TsNi+;yZmd4rG~ZIe-G*nyqorRpOR7uR{~ z0CmMX$$mAFJ=RTl_cdqQCq+f*!{~QDLlr?-VnV2OJ;LHMv?&or7|aCJ7Q1h*rFBZc z{2#~+tS~?vLH8*wd`%yDU4K~WFQ*yKZ_RT}08~eEPHKptK0dE}@VrE}xx$L?eXthF(#IaPxWbYP}G$>E}5wzh15I5u~odYTZR>!}&0 zCiQb#(bUqAZ$gTU0d7l&;nztAmp%su>iWTvO_CTj(%W3f@}!)%x|V zp4_nWvNo#soATi$0lq8+bagO{sjX)KcDxsBD-w{>No7mF1uA-d^mZRmr(U055FP4a#41Hq3L!YQiW@*xuZ0)rPKD(ZzEYM}|j*C{aN6SrWHw<_8{E_?Pp zGCbQp!noIdj>#QcmdIy0vud(82fEhnM%u8TW0hpfHh69FxNyz8J-CYWJu;>PE~{Je zj}h@z2lSG_PWs+z%OGLxYS>|a_v9#h_V*_pg_)8wpVnU-d`84^B~AP4ik8AZ)0+e} zZGPS!Z$1>nXyjt6>U>#Iklzp@TIsce?j4Xzd;se)N^9mTGjx|-Y@Lg3lrNXnox4Gi zZ;oixgf5AD^aTg963J~zoEmD^UXRn5cB(E=m01H+im9rS+2Uh*j=pbP=6~UCl{Q_u zW!s)u;I(+DuiLrdN28*c-kJ1e?^QAOYV`jMUZv%HsOM_>vynfjil#2{_FnP9Z;wmAaGn3^b;M`jHz?Nt87wvZ@5Z`+~8-aP1wCmy*$k{nbsx-lb%WLU8kD+ zPup~q4+RNHV@;1n1*0hHXo(AB5skVYR{YQ(&|m53xihbQGxVPWxpIKj`ZBM-YM{-T z$BM03+Ms>y^hmeWcx6A|>@QG1Mr}-3qX^zJvy7!7D(-6T`Xe7Qrw0vWuC2j~fV`U; z12f>9tI^ul@%ANlI)j)rKqGfT?P~K7&A|z|%2wll@ep_^+`cC#D}#>&P^|t!By>8v zEQ|Y(mT^Q0d)jW2cI3Lq9Qs)^)g@Qs+betY{0k(E2qqvE zQFJ7JYoBXcyrSDRc|+H9gxmZzr1nA8d>xihSM39ZSc z64H`zNn;Ddi65^kB#0Lz{cc8t1c4_ZxcKb`i(0-z>Kk7#RY>V;32^%v!5LNWQcg$(tlvz$oU8KpC z;lA)4$_iVD|B!so(V3`$OJ-#5HKFO<=N2{KYiSUd-9e3qsE3z9{KU}6O!$RKw?h2O zL%1XG6Vm@By6Bd-1$6jP)4|}57TZTnmOEH*)0MfS*+_do=nzD+e6>a|wr0HWGq~0H zp&vXYLau_k5Rfgf3Tvj6x0+1o;Zu6Jv9OL*j9-wJO#|}v%;WKY^^8}`8Qbe))`i@` zu+uqM6bXW#u9ycSH5Cw7DPRBSmK*&HeNkEA5wHuw^@RnN#h)q5v4m5x;9UX0(@Z z8`G+&B@7LE86^d3Y|;VvMoJHXm|7yhgYuk~{(w(Ow*Q?2u%)vfNqMk22Cg@w!uhEIWDah=;Zrxx8P;NJhc?}QwB*9KgxoO0O z$1WZ(8`M4Z_(b0!&aLlmiNfs0dvbMGwC%*AT$HYn1`sa)ia}D|>Tz46PfHSXlVx9L z%5NU|D1Q*Fji%L;2&eU>@B0wbcQF|r6_lq@h+5H@^wa=Z@29p26FT$AiqDdiXI^W2 z`KRhxK&F1e0LXIbNBG=GAn9da3#dm&l%Sg+kIT}38FC5`0%)vz`1MW(s@|5p#Bn#d zJMH}4dWUXRTYEaa%AsX51u_)tmASX5KJ~s=)psAn^V)+sauNeHkoGbSlRfF)5O|oX zn^#yc63$y}+J+U#b99{g>3+2!jZS3b_IBFhxhv0c=B@Hl*er1j}~B) zGtHOqW}>j`!~Ns(BdCF+@iox~m;1YX_Tc$T9r>pU$kSb?9rYQ%^rZ&G__b`AL8=Hb zo?!n@=SRr9Cw?CH%AoOG--UM5V%~&3Pa8a;Ox^3hdRH@qT>3WA&p~V;8er{_QXV{` z3{hNq$9HFtJW+D?z+Zm!FF>?j6X%WGI|?g6Mus^qpA7~s@79VSz}u1|8v*`92dF8T z`y;|>Ez@Aj+V&f8w=TKorejh($01Atvwb(9gnwp|Y(oao2@}1csgMjN#*0 zp)os59ca;8+^)7tafdZiA5Gbn?Fj1yEDyzFNo^E z=_#YYp&OcYNKnjzVL(x}%&2E%)r7KLUA-;R<2MS9e4#2p$dP@s{MInBA3j%f3A5&P zLxPU=tHh^jj#Yn`*kSREB?klFoeXa@Xj3xoN1EvD4?57=Yt}s0pwExMzB$WMMgF#4*tKd0 zNMT@A$<`{?x%2f^VnkNhWO&FdOFufD{H!t$ z=*nlhZ9E9Y3(aDsmNa)No2aY2r8}BT?ke$USSA6h!tIdoj z;5$Uv9tyqB?{L&?udgpRN;sH&{x56Msv$(OGS6OP71r-?IOMMcg4P#44anPFvZh|dm=Qj za$qiefX#;n6*1Fq`pDQ9EgEXlypoDKVM`gyZMyu?B>a`iXy62|q07IrD`W#UBu5pl zur+k>RW58;YqA=@VXgT0w&+acdQ@tk>hIw)PEqqqbs4`72bNxg=@ znje7qfo?A2$Iqr3kN2jOgO6Jk&z0Wa_w)v;UqF6Ax?;L1IzW3@H0ioC;v71-rE4rw zbkD_km)Oj!`GF3M9a#L>xgGYKcQzZOH_mfpO#EholMRZtci@Onr)6v+Z>|F$s{*pTC77W?z{1Huu}eoHuvFu<6;aO5w62k@d%^ds66{0 zKaqoBW$?T*IgLNMbezPB2D?i-fCNies0a`+>JISv5EO&#(unOGktZgy6}bX`#rzH# zj^xe4+HXu{IxLD#FzJdNx7({6XAr=OKFG$Z85;TwGmE@|H_l)vw9{qaRIMj^&7 z-Fz3eBFNh~ir%#NfJwZPLl;=66@Bla3P=Z|E>fu6ANrDnNYd+TU>k1p)aCA_2U(XS zkGtBL8?Of2x<82JTx49cKu>5fb0WVf1ATeZZzmC!~B_-S4R zE0U4Thw%uadT%V5ZQ?6d>i10p_${PL>iT$opXZ`Tq6@lnJ+ zb$3bNVe=JJRu@%541z&a1KIy_zhh|T!O+^uueD23YTSlL_hOH2L?HnoWMh=2Z#`~# z3<8EYZN=Iz-+QW_u+W9)4W>Q3mZa-4A1_RPSJ?)6pP)lyoKB4IXPlN7@lvdlDu{vp zQh%JA>6}G-tH&Si__#|2Da2J*3IT4&M_ACVL1Z zieQ&K<}B)0@HI}Gzo%?5lt;bisHzG_E)lMe-~lbHinMpao@C?xfP}CIQ8=$Z#gUyU zr>LCX#wSkkRJp@oGJBl=DfNE{V7n^w?sC90Hau-D{9JFh-nh}mB9Vm#Z~vUC{V1jC z)q%?>$}0GLB(r`Qv#|E)Q=oXJ`8$`U*E@cD(&EnZdJjcO9W8!5^EspeYLe%0xl(r8 za5$v~&*i#^0_z*o4GbPB)^Fx0=VJMF2oj~m2ayZ%e$*+krX znV8O!XHcgk18hs`)-{tz1}1bNl1>FX`euDnl)YHhDXLv>;xSMgv|Doio+t?MwkhEZ z9%%`4=3GFX<;onZbHRnF8jBx8*UUcejGw$jKM-RFId@4v_A6*7-PQbScs`~ZJ%IY& zK)|`W4tZ03Sy4);+?j*VARx6OnV?4@FVhnb?S)F7*g)4I##O{=U|9^$KV$yuz zzxcS0&SuK>($o)%qxiZtRP`Yz-=EV3Am)R0ovr)dJJ0D2r4f^N)m3c z14Nx^DYA?k$$uF!p^tzy1H%VD(Bm=oJT#{kuu;5WWEJZc#fFd$MAQ03b=MR<{B|tslrKEH&c@s4g|36#Bo-lCZ#}|8m^5g+X}rOGT})TVU~Pa> zJJ(OUB8e3rh7c2~ucQ8l%*$RZqVb2NRU;0uxt~0ZePFij| zNOe_WD$La-1(K(*IQ&{9^vhn&%s|iDbD`_sfTd3~hkm4BG>gu(bE{UTd!SdBhs3$g zB|R2N=29xtQbDrG0V^}0J{E#rmUK~;d;(8xWYXZrCf_ICdM zv-m}eY{G$#g9kizqg!?#@c4UsPCv^fB10$sZQ#!ZNq~xWzI%z!?7PfI*Nz1fN~Fh% zj9Eg!hOCcNOoj3vIyx4{z!a1j*q<}1h{8C}C@ejOgWj_&H7vuYwv5-iWVd-lF5Ux3 zM_(^mOp^?xw}h3sZyCzlQzoS$I{_>lD4lN!k5lQAlLwijWp92b1uC|$Z1I~st8d&P zk>D_P?-VG%aKmVS&0f@*z5#Ky>Ly{5Fhi2f4`rieXRJo}FlW{jTcp;?Xt|kj^9LYI zi2%K(^nVeEZF=rj>LkgK8;Z#VnCKh7{qq(qXP1&!wjxIvZM=2*V(hsBpXj14nW0Mxj6AXXz z@i+*|X^hHK!p!i(Vp0KF04aSL^}l5oxNbE9P+%S7&F03@cgqe(p>0aOTfaB)dPcJo z>W-Jl0$h!l+MA^G_k$8(?NN~8bB`Y#5h2wS{Nwa8yCH1zl&8rX;}hXT4U#~8+UssX z_8nyq2cLIIXlu8NW0rqY*f5ABN6S`bb$_KwO3aaxbU84)3qgxN!%}ob0bBfAzyI}4 zO4O1qeo+?}J*1Rmm}aKAG;S6X?p*P8(|qyJy{*}be^FXvIl}X zQrmT7JjrPUhQ1_gr>?9RB#U^FDx@iPFLb04i^BN%p0DbMEJ`|j`Mu|<9 z$3kPNP2`Qu?D=0p(vz0Lc47m+G@KKmpsxB7-1PoeW&RQfnFCX?a)#4JoZdAw5Vk`+ zvp5ffp!BZTO&3nF*w?MsagvKnpd_+La#Uqyf|qeYO{aD}rdNJZccJ1X*?@V?z;OMV zVGzKL?m-L-B;s$!ci#S5t0DxB2Cb7E)v?4PF-0$K%gewSkah$QM%RQa4K$;QM$#3r z18t?lzWlsP5Pinr#>UtrOwVZ63sQ5vqw5KbQwQm=S~dus8SK!g;Sggoo7qq`#} zkR-y*AI$IvIRRhyVx*9rfghWcwQs+;i5M;XcTwSQ;U~L%i4v<$9{cqNWz53x&+yMP zhwQt(H+Sv3yc^MM_NYa<%Q52a;L^l?RxtqA6!P6q?3drnhdzZI>~I|@2UeL%Ygzx> z>P1`X z0cfPSMrllK0w-WAqb+;^3w+#0ps8Z$U+L3hHdmu@>qln2CU>#Xb!Q)t*F(YW2z5sd z;8R>$c~a;Cw(!Pxk^0CtAnAE)L^wl?=n7jb{Z$RN1gjQPx8^l!CV$`I0i zKIbr+4CiJ1s-bHK|4r-ar7s(>fj_GU0xIghA#vyaR7{e-=tIGhO?yKnhVzHMzx(WR z)#eV9OR9{M%+WY8=1814{X6BBPMHz4ZTFwyCl!g$$if({(wopvRdlF!S`>qh0MGA{28libD&AJY#FPGEv` z*p48(`TXv;oTfGB#Aw#OVR_JuEJ@e@^iOZs+_N{I2D>~=c^bBlzQ3jWCXySyk3-+W z#T!rBA#_&_4qiB>b-&x3)k`A%l9wQMETspDdpax&4njb(fCR)Uk#qtbKrSUS3mCEUR+| zXx*k@zfOcMRLS!uEHUyE+A7szOs@Bxt5K{En*t`X;Sa4E*4Cd0OYngY-ZvdoZyg?}xB zH`sjbJU705FV^JzX`g$s;+rP|JM@Z1rjTFOE))Y^pVX@^4Y+9Z>A^Dz!a?Bvi0D7! z`Zu?c{O*~Hu2IpQAv;3x{>2&=2@tmrUeSK$yzkP#^MulOFDP&IOd*$K--YaaUC?ma z#&qg1JjO9Q_(zE7$bToE>57BTX|zbuZsde@%g1dyn(9`6y% z(AUxW4F))mHC~|PC!Gh>CQya?O%es^Lt3?%2^Umr6GG8B3w7N0{?Eg(Qwrk1VZt3s zyVnb2ciDh;B@D=;%fN_PH7rYL(Lma;k127`YG@+Ua~VWpUv7V0&6(U^IF^3}IHzlBrwdT5{I&qwtqI0lN8?FI4< zqIZz(k-GTeecj7}68`Y(f7yrDD~l1Z1~|I+JS(v8B^yYd)CPxTFUa0xhU|529lH>8 zDU2eR;bLpjQ*jN=uvDjG0};#tbLMbEm>!{GmnP*W7S;X4eoUNH#QQdE$PkuD3}csn zh>qg5Cecih&mfHcQs^Fs!!B57VaR?kQ@%O!Kn&gh(`5&X{xXy@BKya5`{6VO?S5!X zxt|WIb(sNciBofaS)giOao+z?88B5&z7E^DY~EmQ7_B-O>}ouEsMY&#plO@~S3^$vRO-|a0W;pN>-z3RvxLIbZUJ29xr_MPB5qg} zBakBz{HE!uUr2{A)|;J9#&~d#$W^F>G=Q)I61Q$dkTb$(Sud=1Y|%f`nO&7uR{>)F zIipP3IHHC8-warNh4EcKMF5<0yaBB#2440uPM7YQhV68!$i+KN3N!v=3+J?bw!3zo z9w#-Ie@3`EmCDLNYARmhEuSk8%QCUOD@ax;jdkS9BP1m%D~^1w4YZ>f>Ng>lE6qXe z%C0+g#d{R2n=E^Na<*}e%GX7=;9kRXdf6M-JahRr=QjpEF|t<<3=Zq;{2Oy<(2nWc z5IfsBG9_=R&O76JOYNPB)PUWOF51VJ$V}l;c{i&Bd)y6Z6Qw)UHRix?7v)$N*<2|d zR`JS5zaHQZAowXQU$`j&V*W(au=Wgaieni;byPs^Mwn!PgmnfE2!n?K7A&D+!J_0P z<6iyhCv{k7X6JM@DZ0dRlkv!h06v%em1>P{1whFdG+fWm3bo?A8V`=`J`9F%H>H_H zRSW?oTx4mN)f@GR^M{8y=7(7vA(WGmFyrY;Sbj*{%1F3?c1$09UoZSx4n+IU>PuVV z{hX%atQTm@jsmsdixIpzIDTKfs8cMNNn&|#O@3tTmPWT17G#rY!J;EB;Dw(ADy33O zO#8b-CJN8B4_P!vNLe~Hg^o{Df7mqrqvM&fi6LyX)k6dsw{olN>&w96hXh21@&BRC2vL57~AM_uDY3y$ZFZC5KtpJ1LJ z+UJ14mp=We_im&61gekR?3kIH(lx7K?s?!W{h6P0`59eYsz7^ zk4+ykUt2KJ{uHcu-BmV0U%MR>N$e9Gxkrotp3VlNrd0mHV{i!7*K6n1TuW`z zGpUOkujBCRfB8?dFi>=)|F>%7Yge$ck+{*WUcI!gwXL@YQrg5j*V54RF5&es#ILQ< z5d^ObZZ-hMU<8Gzwq!i?wzF*xnF3ig-%7mj(rr=h4P1$y_uF95vs+X+vo*|F9 zQ}%R7zS}#@-LqcA0aCgWR{uN@Rw-W8D2}Z!pENnP(fu)e>UpN|3Gk?waFZuTLa_!W z@AKUE;cLvs3eI2t3-K(DZXFP_zQ7K~u_M`ypE4(tL$VJf#BZ5Dg-t*Bqj3?x>(Pav zSIx1;ac%BHA_1BDfOk3RJ|G-?ti;N4OWzV2dE>^2{`KDUD16mA)}VBX^o`c{(~}?W z-)=PFT5{Si!*$o%h1j>GDEZwBI_hh@@p0p&iLF=%fmC4g_4<-2tTlXV`JML?s(!r8 z?n;M05V%IjEWeXlt)yWV#Ix+aTq9S^IZrTZE@JLXz~k658}H&KK4lL?OH^v7egcqzbAU=$U(csxB1Po;-&MRuedC*@w#%@|>m!_6vm#gOx zgMv)(q7?)hk!4plvOmxf*-S~yY zj9r?UD$LdLu0r>Sd6gGgE9*Yu55~c<4yWg9s!p`JKOO^4JSGWMM?ixM z3A79AEnS)LrA*EqAn9JbBw>u%j?#zu1Hrf8(5omuGSxo4PU^(4*~fxEm^m$6w?oB&-Ldh}?P^A{b^3;YJaMQ?h*+bLq}z}g zC<3CBK%?~H5uENU&6R@QdU+<4Q1|6ORT>Cg2VMLpP|cNRPcG6(M`_Fp}K74wjX zWDu}MpsZ&^zK~We6WtP`4Yti6k{*dWq2F_CYEouPk!MB{M2XI&K}Pl`e4c&;ceLD$ z^MbD+-3P95=S(*Z;997z$3c;fMEDOPLMBLpjU?`8z| zklDhi6P+O*U_1CW;U&Z|=8)9rh0XzRqM=Uz0Xv=1epkk`cn=IuIpzM9s*n{3-!=bNr)xn6y!VIF{|b#XcJb?JtHB=H zX+ctGm%{UxGp0$WGtNt*!C7?El#|`pxL`CZ7d}zWK-Z&aVpn#|Ro2cTZVu4kxfMp> zX873&U9kuyuc=g46oFmfPsopGWlS~?jFi6@zPs9CNE(k}{(GJ#0OcpptK`J7Kxl~^ z^5%wf!*6HM>ogZHGcR0WKY2d;`fmn>=)uh|#{Q9it`>5t0ew#U2#Hv3yHPsIzcmK1 z+WxuUmpJ_1+Ms9j=e7_QLW<41?~toB?JJ+<0w#ZvjQCEfUoQGp&Snz`CrwP(ZQ%L$+S;%sQIKRb-0SpCw2XN{?u3= zZ|~Oe0d*0^d$TJyvVd_Ur*J>&j`3I?z$YF=y?KZ_&^YE@;t5W^3JH4(^Z@Aw{+uU? z^5UCVO|RV;u`Jdjx#dBEEXs8Ad!3Mt^#QO#WdBB*)IASY(|Adc1 zu*b5vJxZ2p{66}j_L}@i$E|t4)?!qve5zH=DPNd{UuNK0<_qfpipkoY_daG(tRt;6 znnTkIiJJpKSzMlwFHJC zVa2#Q&&(g#mZ^QEsI7WYOzWmG`Ah%*XxP7?YG+hHHJ6Ud{yF!MGMqsDZ*)IkF9x*bUs zT)nsB4iQyb0AShXO@ zscM2mayDot^)(A6otIGZm6tZ~WmyXm20b^Z$2x;>gTi*XHE(B_1fx%@4i=Xuwtf;~ zGq{?G<7^}K2|5LHiC}5rX`E*~c2<}PNZhgfMs>(3K$~VhdBzaOxDhM7WJ8h5Fe;67LT4+Z>3r%3`Er)aDqKQHA z(u?(N>Y;|8dQY+Wkt`v$0ZOTQ@jKg_^Wyyk0aLUz^X&negsC067KT6NS5V@jYHdF zx^&&*@K_)Cj?fa`F9|PX&`H3CRd^g0tt$K>5_G78%qm6sF`3*`j_)8i?;(ki&8OV3 z_6PXe?{TBrb8$xp?5LCBuymuv-${)i5lq?PAZqYBvzPwIH7_XUymt7&#f=_`%R!o} zAj)VIP5!2UbvNc}T->}_aVbbM(s8rt75D4XKId6HhyLX${h`0CoBT#brG&mu2e!08 z9{@Rm8-<{M1+?MC1GK6Wd}Ii<;>Sejm&35j-=>gyJxadthA`RaahdCvyy0{~PVmT7 zr{R!INy7Tq+QW3A>AAjj`7&hty)iORbaTfbWOchyFEYnR)G=3yU6_~7IN$G*g>X-C z$@e@jcWkLk$FL*XG9(w# zc^3res<2R)UwJhZ+oaWZ_xdoPs_0?IMgTHbep-kaagXC%u z`%{0BwNroGG0qqVDGRteLQ79x``@HvxRMvi-8@t&W#I>u9fk^2ISX#fyB-;|<58u~ z%qVGdr8vKk6Yf{HHvX3p@A95W*rL?%oEm=;EINv$O**3so6zSV^RB4TNXTMU*UUV> zJ|>VS0I9Rmyq#ThtkavOfDwhDv!@NBa?-^?x=W$`-e+xpXM|=HdG2h9K?6?h7;L+; zEh{Tq#fB>>ob`k7#G*7-GpOIKIVw1N~G z-2724ScK<%g5Lc(x}z@Z#({U0S1~CFnmaPr8F<}c(NfJy9;9;KJ|RbJu|^}f(uv;; zfG<5mA)}`1qMfq!o`KG4@K#rirO%OkQ?8W zqWO&cComYCS(SoPJ{rKR18o5ptPa;%E%>*PwozC>)nPge%da}bo+lZDs6Vg-v^&Js z780JOa8~P{n%p?;W1&*wvsY2eMjAa!mvksnlt|8lRNFj?QIk~X#V`NPV1rn<@$Ckd zxNBMG){Nf>3=^* zF`v|;8p!|^5e-_|3DRi_T5Vfc8n^yUn<7w;*tH@%Y;?ZWGj?s)@+c-ugt{>R0J@N; z4Nb%L1N@85|E$huome`<-nXw0{T9xER?2+W+YD_rK0;;ejd}&gu8VGM=M0e zQ{U0`OMe~Zw~TnZ!yBJEnZAg)%y{L4nW74lAo?^I@fwxR44hB{&Th2!pFksbDTWw7 z0f|~uM08`^j36VfIysh}e+og!^c+Ki;(OD4FkB@Z&GK1(m{muTW@s@j{F8LA*IfFa z?n%X*V#bFI8Tut6R8yg^m*bA`$pQa~1xQmHDUK7^mP&=J0u2=-<0kY@=Gjt;5NYb5 zB%(b;>)W(WUwcst%!3v+L^|C0*Az-jC90GX@tpnK9fKB&B8)@kIC+LaRF^c?q|41} z3d}jca1hj7mpu+d9B6reAguY6WD~Kbs7u(b(9Ia)<8VTIQ0#Q#!G{Y*_xWOmCZ(rpPez&wMfq=#zRSc~M2J}r^*z1CttK1GUTSoYseE_vN4PebsK zIAqIQ7c|Yzd{(%dka>0)(6E$Y9Iy8Uw4_zwI0*!r#Lw{n8HosuYRoLSN)}{Oq4=k4 zGwvl576J3ZyfQp)#O@M7W#|OvTGs~5UDm=rF9`| zCaa~uD3qx^A2^?%M+$x1x)l>^{2ATtQ9V4o%B!H#5k{ae5CZ7`LoYMI-x zcAbjZnN-(%Z~fZgbY$>!I=EZEsX^JfWka%@wp=2@sr0}O?ahqK?ZE)9rOLfZ>DG;6eDdp_rs575zXf@pIsJvt?VnF^1jsScaU+MUB zxlW(Dwb^93nkg9oM3TW`=%mW^7R&U54GC^^OJIl_Ea(EtBXOa!uMk1X(C(N}pd62n(MjqXy#p6+5i+yB2x*6m%uIXhnfnowv3q90f$C#3$L=ANWe{mC~`w9t`-CW8X`6@U+MGeFy^QVWwPR3#7NJa{=_iOBCKO9zOZ&Eg*@m_p+9ufG$%13O-{ zvKvPt0P*?=ppl`rKX8p3Alo>qQfDJ#)lkr=ySRoIWnQU8RkXFJ2XfAH#`1sml|Q)zoOYx}(y_lIr|$kgxYp6&XTnR90C z?xhYd5eHEq0Vx$uF4;2$6sdflguCGu*E=Q)E*U6c-Rqz79J$CmG zqvf;39h}DVFyks}Mep+a#q4r8<3u zR#j^yk02#iu;-JEtL+SQ;Nfc6%gp(Zd&s51$qbdo0b+A=0 zw9NZ30*HizsD=VO>hkxWAA=$9We=j9qg5pD9Ff+?c}->Wb}XH-J{Hnt9%FY__v@r} zgYwy14`yq{Ij0G>u2(tNn-cd!B;wIWGP=_hawPXbccOds`SreAma}|PH4AJ-&d?Mg zv`7d;0ok=Wni{c2L!~HDcIjsITJ=kq*G8;vM)W)S{XBHXu&|-qD!T>IN6QrHLn5!4 zZf5bSY-+&O&rH2e<|HWT2r&?oO5`tp4vmG2F8HZNroQllp_#u6)S&Oyj^w8J93pOW z!-kFm^aj`khfAFUDm?^>+7QL?*=GA(ZmU1p1nIHb#i%mi<7~Wf(JTUbGw7!<#gRG9 z$e>_hkCp09#C2@!9nsEw)KU6w?R*?q2A=3NM@WII{3G_g)cSH3MQwjim}3fDl};)8 zWWfHJe$avhraAf)gN7pTbdAcOVZ`7EdgaXTV!FY}ktHR~k3n6KRDv|X*TbGz0uGxX zxl($>=Nvv7>QwEaHtY?q;8QSWoykIjDydo=OTNGI9=a7nUK2+-k{+by7+lxx6l6F4 zI!*n4GJfHis88vj(?17La`}yl{ml>DaKFTJZXS7mhjap&ntoQR+SYT?{bA_V^{BS) zqgji4KBb#O!C-MZpDg%FhKMYJdx|pqr1b%IB63zUsi^Uk`(@=Y8_sQ~RXu2q62-61 z3|d4pEQ|v53x>{iJtwycMCa_XTMD{QfhAOWX&#jB1US-sAKIPw2Fqo^LHu{P@Pa$6 zuGiffd@I#RwD7AI)#)Q=BU$CU$Hh!OI0GM<(vn4=e~O<$Kyf9wu2)ltvuCHuh2eMo zz;m<6BO#C%N&Y4cxK%7|#_C78+6R_*s{Yui2H0+LAh8BD=bRFA++aP#ezCdV)gv7w zjpadCWw$B@FP-}}zutJlV3~44_P<3-wR_@D-uEaifhBYU6Z)xJ%kckp&jP7z;j&I=nm`iE_!L9tR4S>XgQR6wN(~l$-#~y!= zOnUhf@2RkefhcPQq9C#2r0>>eu5{ygpNoeeO+ z_HdX8c?NQ*vzF@nniS6{d8A$Pn=*ArolYvLx1erP$K*bd_wNLk?QB3H7TD5Ijc(mF zc9zUOXeSLg(5FelW&eBen@m2xR}J3qgmPg)kjbsFO8KT7I!wSXF*Y@`z`U?%{OI++ z!TpZCxbIcKQe#7WE~h{+btA@NRP-R4UH9--pZ;sPjk%!!0^PZ?ZyVyT2Lt!f7E=2zSqp zW-03A{U@-O0+~+^>LMX4;j=mmND^yr3U*4%gRNVl1*qXTb1+DxUzr#eI}38F`iV}3 z-yQaX4}y;Cq_+JGb!2$Z><0J`$W;&;c^<-T@>!~zg2G^HLyhpgzVe@| z7(HQIf1HARFzc_*Mf0oBx#$KsNEiQ}%FL;hqMIqN_b}@z)fzHRa-|Fn8dUm}`w`4k zr&899Qn}(hFjib}x#QOgVMo@!t*m)eLjTUMFZ;D33+&0KwGfN@Z5;z|qn2Cb67NaD zA5H-A2=b=GY&9?o6qCVN8^1pkj%SgeK9)4%y0%s+WrkH<(B^v?k}bcK7Ko?>SmV-w z-zekY!lo#$=8N%^wA+I9?TtUHx^kfqOW13J*9*rB+q8@w3WUT~2A5^rC(dj(cppj@0+#jsQp*37)awbxYs z6t)Z2$adI_DDk0k4;d}~*JeypC;9yGy3E-n)f#el!pt;mFMfXchNDOLvwWzOIxY7* zL*MRKannl^FQ!oNF@9QQN#oeU>f~+XqP*nY*eTis!cs6T2pA%rhc3o1=yuDIrV5j-?5xC&I zFPW~kv)!~OyGKHYkT5im!5#rD(+t0?uows>y3MX!eZK2>i_~#Fhs*j zgN5HEF6=N#c7nhF4FA0aEOb>UOL*x7gALc!4-A5Y5UImaeO+|tF+YM`j3e6kF<1EBz z{Wizvkm9&DkZQM78U-wNR0M2OIGB;31FIpnYG4o3;#S4tn3IEBA=N++wmL2~64xde zfT&zDWP;BHH7H$T1$wVTQwCJxXBEdQDf(*PDI z?wO{Wg|?pp6uZ(*XP3aU>+tru=}g2{XlW52g6bdzVl+eWCpC1OjeZk05PY20km<6_ z4qdi^O%~)5xQU&ybL8Wl+lUKL9!wEwi)IS-aF_u zITG04hAB){*;y4pyMD58s7j@d(GYUJ`*JSyQ~IK(nsXus$g5M_(_D)4An32m^ueS;vg^^~X~r3L`5Cxdt<;Y*T%BhZPB zu2P)pCs~2PL8oT0%Ogh@YZQx>zv~f?bYEad-uiKOL>OD3Ppdkv!m8L+zjUD1X94Vm z?LSv0{QAi#=YsnWePXS!0(Vy>lav2RZ)Re#+E zF)l!U6J34bvSZ{9ryCK``W7f4GxE0#WzuSgq!^;bl z^|h~?&;_Tz)=Ji0%I~zx&#Od|rtFeIozgBrMLvifqV*=Taben5arLg9Q~>@%J$}oC zy|rVczu?Q?R+C#97cP2gXygc-KX^l;Q3&RFd(XfB6rX=~s$k10IeX`+z4jtHkQjYR4sX$sWzi8)6Gq_m zYZk@3WCcoB|D}E-6?F~_8$xZKPRUODNLSoT%PH?LqFU{o>l%3Y`0?7wH}Mx^bxLvF z$L#M8#N}}}4j)`w@uRGsjWs<-Hd3f8jA^{K1=G=tSpJbE;5XpI(u zCR9ez^&NcvHQn6aL^?gf2QMJX*E+>ljJPC;04b2A(J(`H87qc!Cl=4*L%Pcq8z8ho zcx$EA2@o;atw|b*&I7A9#r?i@q{uK*U)Gn*?_py?VU8e%Msshu1zN%JEt$7N+Mr|K z7PoIf_lI+DUX}l|($1hF8$wQGLs)NiZ%gZ~d<&s5UztdSP5>jG5lD7ahCgGE76l1W zBq}=(p06R|?OKu;w=ZryvVAe$bfm?-R!bQFmtaW-Il(7zn0ZhlDZX={DdbDZs5oVY z@X_wK$Zsjqu`lZ{>H60e4MVGVJoEi?!a0_vG$lsD?LVV-Grz}n)`oS1uEEX-Kxe>EPl$5*yB|NPL8!47=uPdshk4I<;PH|wK z^#u<6e#p9C_AyAZF^X+H#joBW5u40t1=U6UFIY&*vCcknDAh@?#3U1yFa$QTsm4ug zKkJ}WkK3t!#=*=G4-=|E=dJy@e2CvWl6ZI)`&QqGD+$Hx;w)PHu+T zW^QsyH%gsyv&^Oxxd~b2X67bB#zsP87>-Rg+ZdaBzt_(9cU}EcS1xStymzM4+pF~=pxP%Z zqIdrbZYc2=D31vuK+2_lA-zY^38zzEziAEv$yP4(sqd^Lvf<>|Tz}|2^^RV=4a%l4 z@;%c{;{fz2?g3$(tk2#faf8Z-ph(l{l`(ZkgVCikF8HQd7!Q*SHdwarZQdTcnd{$C zqefHoR>o{Tp=&ncQ@NIJ3R@lGC6)#eQQ0}$aH#_W$Cv)PS=(KPXi!KJcv=G!3(hL$ z*?McNRZqCGpDWBa663}3)U=VMqRa%RD4`%fodr6^v?0WfSy=O+xB$%f(OkpW9UsEfr>B-FPKi8 z7-PnUSPF+*5UHj_SMO)sOw*N;D_7+bcyPGZ`<(vMbzHxp*K@S zu*75n1BtH%2SN&V%vMwl`ymuBZ4(3Tw9SHC*b*J=97+RQp*HAd#340J@GbJds1rbu^tEJIzQB2N z(tY)QO-!12bIIzcd0$T4@N)sOACx2V3&OPp&9meus-l-E$S%*NMz=oC)ftp=m4Noa ziMf9dJtxP1*Glf&)}6hulTpqF6<6=L%3nH2!3t;=ip|Ymj=VGz=|-9@3>bhq&57pU z#0vGcZPv^sV~0&lXS3J$`rK2me9c+?yfT@^XJ$p_C8=H29btZwdk+STUk*u;xC-lt z$iIgYcbp)3g%NetES4Ur=j$YdgEHwOE@B}JF-pJI+Ml1bAHvIAqbS^(o@9b`TXq+Y zAt)emDbY?pHvVr`>q(rh#GVDA8;3ee*K$lkZKP_@Bv6m)t_j{)Gy4T`n^|}5vM>lEkpSAiJ~MwTuRS+Gf?91kqx|3Fu=DWNFWNi8(GA~(W0Bxi|85mO znTIytXY1&!&iww%VegT@o*)z7a`wqLHZgx|{hIN953Ry(ZEBDS@B^vh@; zz@wo~;O<;EXtq?3?|*e|w*3{IZO_`8?lWRaPl+CG?u%o0lQP#Oz^@dTog$m2ALaP! zf9^5~^^EMwjD3s#cPHPIV(uhzeDsW_9!U;4^YHZnZdJt|9Ye)-}jksb}%mvSt@?z2;o*^jtEm!ebq9OTjc)YrS@*m7i@W@6yUP6f$u@yY(az}fI`UZ!X1wRzB0$cG>JS->)y-!=#Vq`mN97YH{t6`oaL5YFC#?t4d0`B&Av%_ z?qB0C^K8NKX`pNawFbB;$*OvYcg<|RWh(RL;YD&VxD^H#n|YTNaZMKG^U(CK9tZJj zCZ>cc1UT3PSpv$~o&dpp$LSV>v(9R_^X;TMbjcy495 zH|xO%gaQQF!psqspk29Ub`MCI+;0t<){IR1LAPN*Y5S1rc;YrjmXSSvub$#GF|4VQ z2jM95Cli^mmne(*p0B;>oFMF^4R zFM3uy&rs9(k@w#%K8I!O$5}`*#AI2iYyN_l{7Ct!9zu=k46t#(HYuFhaTA>mQD)f( zK$eudDc>LLHYza8?vn1u4B@bG1tk@OE!TQFHI0RPqZ0#819HH%7DgC)Q0dD#hIIxt z2m5(m!SmHERHwwfA+e!ITd1)WJh;gu>&C0-*g8j*A?8q^e+N7`m8+v2T1Ts|>=jsC zg1X6*N$Umhq5Kg#__|Ji<>6G2=9)(kIi860dC0ztP70PV<4#iMxz1=58$G)Z99Z~; zoB*e%XvsrnIczBPDKc2yP1yDoP$n0A<=$Z(haqpZrCBUie;2HjU#W6yWCPuN<{8Q1 z?wXA+gWGL8GN0izX9`75A@{8kYoSwA_q0)2V}-Vu8079fi@JN(gazJ%3dkYL$tgp7 zJhGr#V$S8&S8k7sxiFft1OBfmdHg90K^C@)tQS)AB^#hTtu-5e?LcGJAc+^skzIqa5Ysl$v@Q5I6i=e$CE8+iX)z zk8WY~{UR^jeeujT33;SZbVD(v&Bp~rlAA$e{O|M^zq@kx;H(WABL%Z1waetJn zJ}|g>B6&e08aiIF18iKpuF8L++=jDG;wUm7{H~>DNAVw%&mZvydKwH%o?xKu{;DLm zHvydHd;8FE&J(Nr$@I zoVvK6X7R!ouEXKzq@}NB<%x$@zTqJy^mlxx#Ww~v)gUI&|4^aJ!O=E&4lp>lNa{#1 zbH51JHx?V>&W;alQ|^de<$3RaQ8?}a{*Ql(zYn2p!pGZ>=+n&?QM|$Dudv#1@^tt` zMr0+Z*a*BAwPqF!;(?RUnS@9GjBz+RrjxHF;jVq`XWV&&y|^Ws@o># zZ{~h-M@V!B^Beupdg(0V88M7N5cj|T@#R%aIg}tqoK5J-y-wX+D(E@C0naLbz)yOG zOdy5^gO9J>u7qLyvP65TYy=Hl_`YEpL7l1uL4brl zh;zk!NpHBZWcLW?PD{!Pd%5pbtmyqKCq=2x?fhv5-y05k#;qX}<^+6OTgXxqqE8dU z7aWqHd09rlg2+IPX8N?rDz9RuuJ}l@?I23adovZIFU#O_j%$Q(p*W3h`Yd?Fv*+7W%3+Nque+2-w^MI5ARJ#zls0F%!!Ym_3+7zKyP+nt9V_1s`*0>Os!JO< z24M05VfKz%oU!;~USGbgzF!{t#@rZxy%08{A=I&gkJ*%Dgxm3CNc)ni7E4?xj~cy0 zzN>9qpb%e@Da@7mi?Sk2 z{~UPdV7F!I#`)MyA#45Xk?iXrB;clFX30VKrEjm8vKA78iyoT1Z068vBP)L3A;kz0 z)OOYuo1tjCBqE0JR?eKTaZ!!mDKW5H4zG$t}#A$X-?pd9wzrl{?&s!z|CFldZ zpFFI2o|a)nbc!gUaGoxYr})ngZ4UZEAnlt7yqVr^ud;hhOSy_2_K-kwfcA=LTggzl z9^Aa*yH3t?FIG6_JskY4`Qs$gW@BAU5()&zFLc!UD#?A9T+Ie6803uMO2n1*-GexA z6kQxO67RAh{f?5AHi)~_y^tKI#vmsV{otMc^C`<9U`>JRdvq&cg|@~Cvb55maGUsw z?7)5lO)D4~dV5D-E+MQMUieFaI zD@|s?e`1Glx$BRd9*#`c%j6&D%~gL82AOfd0`ad^#76rlOf)u$piZ>%GOKcUW=07K zet^j`9Q>Lk@OqQ8#*x3`XeZJqTsbV$^A_9N2CokoJCUSF7e zO*9?DQpK%U@GmN&C!QM2S3R$QK1cHZTNo%8=zO>oOgTjZ5~mVBWkpHyLNUa1tj@0V zp^BzIgnL?-wO>zt(4Z`tQ?xO(X^n%P{6`;8@P?RJl5O~2)f;rvk8cPSY#P3=SmOQB zmji$62^9LQcaCx$^}&9>3W#HYe+uD6?@1#A%>C%jNDdd|jf(kiz>)EXTeP}zuc0pPgImadI}mO%?IJbzTr zmcygzQj_RdZQ0#+wLj@`e>`1WFJ(P$6T?Wf#y1pSI#W_L*k03gCLvUW>Uzila%#5`!9&{MNK z;*&pAxJ%;Eyog}y3ei96s$NqB^WCF?MqWu@!hhi2^--q(O4p#AB^6{GEe+9|d1IOI?4cHeI)zhF3%+_8JX}UoA zE;NPhi2&&k54a|M=W$4oBo5Y^r=oz>*?@k}<G| zEbicjasNo4Zj5f0?xH=*@=aH!ZcGr?rbGU-m%1X4;;;vk>rD*1>5IMvM)Y#-T4W^S&-*8rYrID6EPpHA2+PXyiFlT%>b!h zMwajW2AMmOInd@b8lw*F0R)d6f2zP2-3x|Mzb&%zA)mehdDQ*MAjUs9SyJ)bOkV%d zmBmYE(Q}3rd%S6TB(zMpTN;0r_pd`82^s-cmrCA&48Yda-@n1tV7PxN?8u~n) z%wjj}29qT3BD7LSI+2RJ1n4)xH%p|oa^8k}GPOf8cjp5fM|JC3_@hVt2*#9Q=J9TM z9s2WE^mu-F(38Su^yUUQPqEUpz0YD*slmflMypDrNo9l4(8OWwPoXB5L!;v1INXegBItsd{>cG+CyuGbN{!pXg2Mb71yn{(Pcoa~VZf zp^hrJADn?b*9egzFX1gd!maCxBr$~MtWShgW8^(lMp$r3vn?t?K28}uxQ41?{@65z z_+WHI-M%HJy)1U!P)_S~EB?UQj7wjp-*}aF)6Di2R!IL&WMJ<$L`0+6O_4|F7$sDb zMU1Sc3T3b3dGYMN{Icw6@6COqH2r9?iNB~ip4xuh3Uo#)E#D{}KPA0=G{3n@^|pk{ zTJAK~G!(YcS;%0GW@Tnlj?VMBIxQ zSv^IOo;7=oW#pJjkbamM_>7fm$8W@aKH(e@$71=9#YQi|c0{6$QYzl=8jXFM5x~CVzse*Hjrr=Yw)1(~Aj}msm)I*^SCzxIhO%Iov(kws4`;ZnY~dI-`Bf z1+-qYm3qGDlxqwz7+D}Icu#Gcz*VlP1zDe=#g22?)1oW*1CR``s#t}_mPl<~QRXPa z1uOO8RDGVo=eWME`&Ym&P%MC9U-%nYwE;G}X6BR8-kCEAJdl4{SXJA4CI|h^O9`)I zZwM)Z@?nqvYuFDhn+Mz1(uY*hJt5JRIoO{?P@ZLbL=-qlcWVub|A0b)n@W-wlWj;m zK!n}R2bVTI%rq@ZN8B$IcTMUVMGKDn2Si5rTgZ2M{>IuMv>puyWBj>QYV!&KHHVF} z_1q$eTV3uqc*#wUE*=#zP=Uo@mxx)%168;#xd8tQ81z_QOFVktQqMfgg~H+Z*H}rT z2a5E}uswQxMqj#;^UKbq4(Mb(EPloYv)7483^)+|-{qY|(_70f{Z_RndLn6Y7ks^_JA^sMq;ab{TXlrMsr4uvLutG-7rVw9wPXhcft6qAN+!)%}j#%>TBv!38T7W zROMy2vSf2WPyRDz%MOrBKgxPGkUfF8W*pko*QI^omz?e)CpzJRcqI1noNoF< z3p`SF+bPS@0oAVWLzZ;;=7163?^RQOq!_Cl=MYnUli(bQYzE+ChryWjV1GGE{8zr_t9b?)FZY2W+C$L zCc+d!v_zsWe=Id4qa<#b+Yj!?*OTO4b_{panwe--$n#u%8`!@&U}+@xwQhJsMw^j+ z0}Y*|J_Ak1Fg8k{a)&U8Bw*o1?IrN3Ie9jAm&_hVd+($X?QT)tD`8M5=HNqs^B(vB z6Pd&+l9A^?_D)tTlz3z*Oi98_e)c1Bq!e)Xs1*iCg9^imv7)R-^x9LvEeL^kB&#~D z?7{xrWw!znq!fJ}%udJaQTtQA88?}61jqwcOHvf7BSzBSTGlOo#{|r_)*9tQI+k`% z=56|u6d8c-#oio5x41cE+a;bfBk&FAkZ6lX>P@q5_}Rm$J_3(Vl)A@bvMX+q4P{xo ziBHeYRz8|7;Qf{>*`OSZzISwJi3C3>TGIb(Y2S4FV{s0(=VyTjUxK)z8+c86EQzoM z>JZW}S4y5OYRL8idGTwb#dJ`!!Y)E@UY;vUp;5PpXevepKQsx!U=j4mRu9@`yXjYq zS?j&A`?^Qn(V&eUh^u}$r+9F*W>EX^8&`j?F%gb27^}0tC%xO_;Y{tyXvorGEhLM( z4Q)h7lwPz<SV#NJ*lvz9s)*Bn3WdI~cac0Ut!6mT&eB>Ee8cE8?NIF2dDmko zN00)P`2-WROtTAxY2G-Lx0ilQJLtS)aC&1fZFl_-ly&%`WPfNy5^#zor*?8RL35I3 zXCn!I#7X4)p^~w=olMXthV{@opxkOZ zcf+>{&a_z-LGY_tjvpcgfd@dYRnp^5`gtP>4*Y{~{wF)+3$*M!Uofo~q>Pr&gHAQT zo@UH%KP$hnh8G%KahFxldW7S+QSlX7=V+PR8VP;V42HqErF6XN1{MeLF{fyuI3Txv247@RW~L8SxR7r3=cT&&w+ z$aq_YA)|lT9sFKyE<=--(k^-Kh=modVazW7@^#FR7zP)-B0s_St>YA+$UYw&lbM}R0hW4JrP~yBv5Nlxvw;O*63S#B`#|rCi4Fw9U zr8NFh_~LtG9<6*!*s>vSlwV%)wK!@%$+`Mc+u$^IufqcPvOXPGV_#mK)= zxj-rDfZ^N>c4$B?jOX;<^`Ck67k1Z!`Gnxqs2^f={^eyPNOjU*e~}=9l`rl&-|wG0 zIvhJbW)nxEJc=$(-+dJ2*bu0-3}l%KJK>bR&iR6$Vn0>|yi*?R(rnx^>bR@lFIDD2 zoV~q`pk$SUY*f~cXcoq z!8up{jg=?QBuMYe37$luY|xo#sL8En*Ee?ncLtbFH+F_p7oc+g<;b_3YHV3^7NH;E zyB63dg3nz^x{ufg)Y*+NudxiU_>oPsQmvBs+i;iTh+fnf>B5(JI31HMNulq@1AFGf z+0U6z5W=zh3oSw#U~}mCwx9><21u{+`!YDsEBW%D7$S7lN3NcYnm?cPmkNu?EoiBl z&@_T^B>)A!MNw$6sXBHcZ%DAxA9r@Cc6FU&VKV?rPo&^e7sWm(*>0>SNR;>{2(dVU z0o`TLMWB=j+`oW4T}eGHS~>}SY--|9A7$x(T|Bz zn0sWmW>{vnw)l-FMV%>w4x%!@iF*G2%u!t_C87UT<`ryHF1492%=BE=Kmj8>!X z4L0A3d6S1v0Ez3K+j{}#*Ui*g5Dys$myCdV^oWCj4B4Av_t zFSVI0Hg_5-w!fxgj)zuyb&s{x17ieGVOli8#@i2%fetDW5`JL`^vlg2;qMj^T;Dxv z*A3QuSHjh?-Q0l`F^+wW1k2-3q8KMVd8j-ltzq`e(mbxx8UQ0}L91MCQSP8>*eI@o zGv6LE==(okyT*#PtUWeeU}E}yKgUtzyXe@!{jwUl_%5Ydp+9gfh~M9_Jzo;_g-~hx z>Dvx&72T%4ldd08Q*b-rS;3AK+2B!cUBKudM+qM;rqIeBMHZq8iY`PLp(mxXP*d#) z-}a}mT>juFtH7WmU+W+QxNfRnw{W0lM1_LD@FN-Bjy6U!*WgHFQzj6KjiHX=OQ_K|>LKO%7r)X3(nx;+}zsTOZ^3w)X0wBOya$sd+RWEC*eB zG(fB|QT}=!yWg65X*3B}bAE!#8Y?R>+o71;J99CkeeJV7Mm}&%+Vclrw#c8A48Dvf zV;Kqb7fv16j|&q((aTyt9us+Pg5Q8sp4q`Iqrtwe6?XX|^59q|p66D*K?tWDvw;W$ z^))q``AK4&X66;_qP7k6DHnvRM&+zVeC#PQt8k#hMD)&n5*BP!HyU!^O^ne8f-<`&N1?n5X=4!Spl0npl_d3pGPZ>zJrz~H z;HaW~sJpvb|6wvDuH}eLWAYs8MOM(ix*Lcc()MKO@KR^_Dz0Dg7eA|A#}uP%^{FYN zPCsJ4fpr*;T-{uT60yjkDH_Yk{6-s03ny-7lM-Na*Ee{ovYu%iFoXSB??1CUe0F1- zVm(p%L;R9vKw_jmWJHlw9`#14GrAkat^Ai!2LfHo3O3G6ZiNT?J$nos-dXCvV-}nvJY5JkQ=dfWVZ-=y0(NbmZ|C4+TPiw_2zdQdW zFy${Ja>vl#h$DBzsgH{q5H+1ZtqaZ@ln%uUL|Bhmxl$PzdI}wbSqC7>oy+ufSR(PF zh5tt4p!!|XJBXtQ*B`#6Ma(b){S@d|k2)d02&Eu8W=fJ$EuH^s)y|P}NN@5;y-)2z zx7s*v%9zCpHpBxNnr#^>11Ass(m;#EY^+_oa~-_ahOPM_9T3`rw(#VjqJehHl0q|$ zC2@C*-CacUZ@C2`-fzx&Y+8la{!hBag$$`|?Sk^+Szy1s6>#A=rcHXxY?VyZm%U+%Q)hneu)383Kn|ID?#?E;f;Sw~3Q3mC#Jp@On1 zc_i_z8~QR+P&wSHS_yFjiFe@!H%OveO|W$u0QwzKA1;YPj3d>d0OEA0)h57HzIu}`wwcH z@&GY8LZ=GwmL}(dje=tJ72{4IU@9~&!nyyX4XO};w8JM;cKhw>e{iz^!DgtZrgQ()NQP_9)n)#80(U<$R`sv^x(~;dcI9V8)7~pirOhBc5*E?H6&v=p zFVI=8!%hm#(g`5rjnpB!E%dG>JNxj$dYl&ND%eFxX$dwP@PdH7$ou)IDt{SJ;Kk*Sg+M=3kS~=UnhD?(z69|L{C0wvOVl8ulUXO$h*K zL)+m~l`U=KmBfKw2>ZSLB;uwkVwn->bD`e*7!6Zmdl&A{0qMS7c6> zm0)l7e{Ekc!6toN$kE=n#}nd;i>&k<3DA_UV=!T_Ulxofteh4>uwrm8sOc_gNamP> zYzi4Ma@+U~s4kgWweQzs*E>6jZ>g0OdKvQj@ld-HaRnyDkTB>0MLkva5@H4(<}%tc zRV~P)_7eTxh{Ezt_2@f*Xd**V=K0C8>unHOSQ51kPNZ*2N0lVWK_S%{VJC>YA4wkO z-2#6BLJ_%rg#oph=sU=mew_D4vAR;yFTAbTh6M7bpm4VA)m_>7e|YXZd={typTtWI zRTA~VF2|Oo$VAPjEBbTSUkCQhJzWU?hSVb+)5QZmEO-)U)%9vYcwU!r(12X|%(Tr4 zgd!mx;L+{Ae3A|rsWYFHQNbSp=U_lTRH#YWAm+OaXOa02#$I)_V&a!i*jRqbx#r-jw{|DX33&CPj(;qi#A? zKh0Z`=ip(9!urL&Fb4R2l?8N=S;mVT?79^YHTMD#w*Ieb{uu*LaQ$yhrcMxt#5Obi zMRQ`PF;%j=@1)=6ieC8CzQPi3Qi$CdVp8Ps;MSuhMbeB&q%P=2m+nWL9#h$Kcxt}NNdZx*m7l-{)I z^)XW32>k!Uy=?o=_6I6ti!FUWf@k)1M7B0qQkm~0miZ>YHCe{vhOQt0oc=X33*6K< zrK}`^rwcx}wfJ|D1iKvbHs_RYT00of(sr_kPDToPp}|{5!$EfZORewUOkJw#H;0D} zUWdGHZjAY~XRxbpEIp*XI0b;~y#Iu$Pa{tb*e(1iY`|i09YkvH2i9tiWL355`+BeR zwgLilVvR$a*f9lj@Ea6im{Vb|uC}EIq`2Vu^H6zC4jho>70pN{Ope<&`I?LZE+GCz zJGQ_l7$0w_OlEO0PrlMg+;1w}-x?M=o_JrpUK!Id26>yfy7cWOs3`2+5CBTcYnO+D z@!6&~2)$g}EIo;Ckl;QzPcaQ<=9_AtE4h{{$(o7X+v+2`30doBFi$G6!;W6AHYFlI zdfwo{ss;@q0G}jHCWAYblXW$P_NiS&B?c=Bbc`O(96qHOK2^sa2ZRZ>TNj(8LH3d! zUro`nDcorscKVN*ZljptGpCce_`bP%YF9%|)V`E|(Uq}AlPq7k$O?$DX;ddfPK~tQ zYd^LpGkq639q6Ee8*wLZuB|FJ$JukGvJjBQNrazrC6(YLGn`Zi@pWSz1LKFE#ZO!oY(}iZn2;0W z7MtvbGZipvp8&iXrheN@cu~Z4V&+{o6&;3(l2FMD-Ajy7e*u)#XSVm>99eB=+(=Gb zh?C#9LnXw^xujove4M5FEw+=FDpK~xB8Stxmtcxc-CBI1J%pYz2jlps74liZcesV7 zK}EZR*gWaO+3sfg{ANru4ikqZ6XlIp)>RPm2NHHfLk2{(gR=Kg<0@xc<`#k+1>~JoEXzbf?~OcDyOeK&VzXfp zg=b9#{wvez>84F%Cv1FhL*KBk?P?+&?gmWIsDG}%QIOyzP|8n?J!lw{?adi9eIlik zo&r(#XkUZb3SEGdx~C56m1)(Cw{m6H2F@+IvP-!Yj$$Hn#%8jAd)$oLv@=c(UiHx{ z&94dgjlvfN8J~vz$8gH~mp#{+1uof!*FK&>XX=-cJ`;n`YF~LMr*i;afful&nIB+G zL7~Y19pY1dhbiFJLrllBc1`SRdAn$OAEf~(tqf2eS2lVrDUYG={VO*l*dHdh)&S2*4z z^mh89K-3)KQ{_+b%O6;!O)-`Gt)TJ0L!G-otg7x#sRLtsPKnHvg ztTmDX5$yYweWcXbUIwiP3nh?ra&IU%TAzc@1S0{N^>7U;q09vKWe(1yRkG&7URt_3 zBxr31`EM+q)%usEq?-9EDUpCiTr9$M_|>F<OF6U-_n5{&hpn395MTO>gZ2uw+_Estbu!!0gTfYSry85=;UnL(mye?yJFuL zT875{TyuH&u5nHLlmOgMzVIL?1XuEFe_q>p;-UE3W4V>VR$2;^7u{o6C!zBaL;*FO1tEAY) zHhj(7g9J$9{8OUy3%~HW;j;wKI-KP$E(ypavTItO!Zhg{La{G@T#0OC&fQLVj&gv= zhJ_oe=GJY6^NWDxOCm!627;HxyuBF1)84c%CdN&?P1{Ydd40cv_?agN86gGWJh3a1 zfKGW-dAZF@We!7}z)j!W0_#)52VHnv{k?>6pqLLbAp=(&)WJ z2>;3PUp4zvgvdUFio05JxMj{HnJ1h4s%&^EJM0BaHi>VcLMqR|6}@i$H@B~o_rOj2 zk)1vl?=zqMy0&1AlwZrOH!gvX-;02igGLHp9-wMN89xpNT9*9K0umF|S-x(ZI|Vr~ zvAa;$0o8GGzj+O}gy-#Smwxz8aNj8Xg*|hTd--4Z2b>HSKI8BNbtwh^V9dj8c^o+j z*UY_ZFI~)lz=s%9xA@oEX<*W0W%$EiI)C~#7wAAM^z89rO`&-fq5qjVW}fnoqs#|4 zP1!YEgX(4-7JJ@M)B|^;pL*W$*J_A;QVC;Y2e#9cc=}Q<;YFvl2)VM9=sfpAtxQs| z#|Y3bqxUVj2(aC7dtYV!ZKf6pyC^mkMUjyRAtcW4@t@*P?oAnt*?MNy(x^tdRvUl; z(k<$ii&*Tf^9K`nEzPZpZWoRv-Z}71`TD{bE2TYQa8nsN_Ewz)<8M&0j8PQT4ag53 zNVK&l;?-N*pBlEiMX5K<_5M+Z;92rau<4u)Zxr$qo`e^z4`-(%-xKA^^s(8EW=aRA zi$Ye>8fi)l&b)&0>OCm7oO1{{T>s8+X8JpE>R#PhEIsim{40<@Rl5rd0fJUqH{MpG z0*pXZ)CxiSx@p5T(#mA;q=Ggt_jG^q zuZ!$j6l-8QE&Uodw3iht%}}#zL*HMQ@N@USQoaoRE-caCtl28B@C{S&&Efnfx^q}h3EXS3^)bjED5Cu{wzN+Ne>zg~%|ex+ zc{p_7DicV8B(`qd+baW~6C(9$f6`fX!2ZM)nc&r~8GS|(d3K)XQwI!dSgJH~`~PA; zaX~2V-zDC1gDsBf`e-IXP2ci!L*+rfyW{QogGa-u*#H&e$@Cplf`R6L?O;8FtaR(l zl0hYSznduYtWB z@U+UJ2BiMyRd$cGJXnq_2Iz3;Mj0r*s)Z0+FD+a8qwscxWs@aNbd8 za?A$ADFuCA1^2l}Fk43G{8X?iNL|c+nQGY00}^KlNvWgx0#OdB#+%=yR|l!$S~rF= z`Jfa*>x;j-X`q*6A*$sq^`g5j+Wf5%?La{?1VqL^V-q+8Wiy7@iZ&moI$l}#xj8vW zC;Iz3R_iRHtXI#$Cy0H`S(RvMK%VzM6c)EU-+Loi>Y^A!qEO_@>no2Ge1m>eEHV$SzlWgE%Uyn@3ZQ!tESvIE4L@8SdmP2eN; z{b+d4Vt5LxZR3zB$;;u(>3_?2vgP+hXAMP7SYgbeMEB*F@6n*i|ml6iY59r$Asr21MB99%llz?T;Q zP*UE;cm@lm?kG*xsl7SKA1ttn|`3H zfK@?26os)9lU9^Y0_vAZJc5Zp0&iPoL49-2;#?W;WeV*@DKpYi2zBJm`3{qnbk`0i zz6@M~CDX9>Y<`UReN^KHg8V9m4GgU!r1 zHGZ{nKzt}EWS&0kxJEIW8F~xB8#TFTbT0T_o!=lp>x?i0hi;0g$o6Ng50MLeTiQ%w z`#?+n*ER&7j*hNbnYA0TGpW*x7FYi;I*kL@a2d~XgpxY~_s)O($Dx%u(F-%WXJ%-5 zpMDEdy%_h|Nj>hC=QndEnJf&vbm#^y>U6?t-*kWvKn);DZ~E|ZMz^{2_$(~qOpf<5 z4zUisB`3C9cfsY{F9HSy-5v(5NI_g7W|w>@;jaMad+(!@029D~t4D;wvJEs(1i~(F zaY$jW^@zoH`i`C;{c~kx^LU&4$LJ;j$>3JyJ|3j4#rv2z@v?D+W54!`kpLNVm#mkM zw1E(=B7=YrrIPUYPWjjL>|E$`Z4%_Uhmms zd!4)B%J}xU_aASE!8g)kRp0G2JK3%8fDnR4L!-)Mjtq#uS&5pCF%Vd=EBLb>{SqJN zpvS|~0Tml>Mjkc@_;TN2;w7o#NxleywH1 zDgP*F1Y69)Co1tv_x z!C};yV`50P-H(IAEX$*+%T0pczg5ihol6F$?x$bdmhRV6Gxv!|B+{9~@(~ajCx zJaCDiei5j8tR$7`%RM3Jw^R+0$r?jFfWMsTq|j`hz+^vDw01& z$D8KeDQZ}>HUgrMadbhlRdYik`mk3A(^IX=oKWDl<-{#lq|Z{2h)>_1<^ai2uO8bY zXJ!E$Gd`XpdI4jJ5-L3%bB@0~;Ps5b*3s&d5yVe^rx>OZxZrGC&feWmj%ua!s08Wp zr{zN`F!%$w4ztBR4zf{a3ALD~mK77xn=p;qF1S~?w zHz;g{Cvo1U>8~X-iO$g^ih#?zVpr9T3Q5uTkqtq^e1B5wMq0Zat{0}&t6f^Es-OLR2(i zWbWGrZz_YbV6!^*MO~VNCT8+A@q;Gk@_rBjKa`$?t;PI-j~=A%=8eEFOvS8hmXzz+ zphN5NpBvhocy6YV?v&B(tC97A;1_8F0|Wnga>VIhSN}ZhWy{c&?2L6T_TK;LV@GdC z<#nxY7PKe5%}b7Qb6DDU_(R=juNcnb{-)VH8!1k?{ehO#sxV)~wy;J_!uY;X97**oe=o#a;UPfBkoLrmn>d|hZb%dnhx?M9#cgbHIo{i+& zqdfVC503hNtj9(LlYtzf8N?iDqwu<_TkHPAQ0b@u{^1aR6t51hJs!W9#-L|2*CfKA6T+?_1h*7|X86;?v97E}^Mt0U7 zdUEehf@Cmd4{j#Bj)=SmLS2#|^j>rJl}+WD1IvK$&w)ZrALp=S;qJWKAt%E;P7-BWpZcii1hnF!g9eoSE2E7ebdcQ7UFd(zrGEu z^nhvvbsvNi?e-{S!F`qMFe=OB$rK$of$$2Sv^P>h9R$AhC?OFZI#@8Cl`$X%Mk>E8 z3!2zuQrhe^K3|X(zq-vk?zp`da2XF%w*sxL z1CYxDnO5}8==C6}w3&E%gz6LsR5l;XY(fY9qu@&i zbsbXTv+^mZ506uDdCpNaD2nOa_K;V-9wQdK^`Fls1qX0W12Lhl_+eo=S#{LGVh8h6 zprz+BoGVK{F>tp?Q-w{U(gj`z3K!@*C4_8}YYAcuI^!Bu_jq9UfMGc>ipVe*nZz=r zYO}n|q!{D_Cj)m>d!ffoaR$ba0xB3@+KLMmi$SPyh0!s>zz3OQwkcx|v(vD^)}mMI zevJKQMr=6KfmYV(^#iaaSD(2#nXcD17wq&LahIqoe+ft>PyYHccTCcGPn2UJ2tH5y z_GKQSj&TP}t9)*U^<7o}(27W{a$cRj;R^t;g2(>NV_K-Sb?N)j>61W2QFhgGDOCEL z>v@@3DMPSW?~UF^TL4usAok-f=P}5AZTN}?1k*1!G##al+i?hsGu`nkRbEzL_(>D^ zVgCQIbmn16=Ia~J%$U?8Gp8}*_{%`H7k$Hj1Cc7-}o;c#;hc{R`6maqx@oDnExTc82ZGxT{n#XQ=bsy zl_JaX1VM4jWg9BA+{j$1E=!+<2oxQhl?Ub_Yo7o_W@l8cki6CcxJ+auYpe1vh zoEckU6wjY!GHH`=xxJ1H86mB7 zgFP))U)cKCpx&EE7>AYq!0MpCT?zKnNbcEWBZ>!RBI{a(cm^UZ&KQ75b9j8K=+w$r z`7E~M9_7&IjEWb_ivD1Z(1)}a%?Q;Sv9-1kKf7(=ovT%5uNH0MFDl@5V2QCm$~3tz zJc)@A*bW{JRa*3ijr~6dS1rt3!rYMQtAB|5m;jWc|4s{9KTQH|Zx8CBU&X z&?r_Hc}JB^uaut#mwwqA(;72WBwp(v9p677$}9YbCMxvkzS{;lJ0r0#x)bKj5snH{D)O>N21RoH}*2=lmseB%$M z{qJD5<27n5po7J!n(b#?nR=C}+eVA6$5I1>*GXhJpf+_OJeP{Mgk;DC`!p1fVX@q@VMqPOW@9U`ewimH8}t zx?{_iA$e>eruv_Abr-0ky!n5nY^>M27Mq8LtS=YzUNkk(DkSb#5ej^bN1cUN|BBa# zpC?IcRl2ImxE}mqIDmXwyJn0V6d3we=y&9ImafY40n1UdO`pqjOVhNpKdy)``Pcl2 zfs-z0U9T#WpH$=IH-nOxgAQ=3sH`rij90PP4#dZ8ucfRdToMZ?+ZPhM&6c~+sm4t zAM3A3vk#=`NvUzdQAOQK&!|&2s61q@>k0{It*EXIKBU*+M+1-4a43ueiU$|FUAYB+ zJS(L>ZelWK@1k&fbfp^Lh}0AK(C`@}l-<|?y=$RW8>tI5g;^~euZXOcBsCTY@EPoL z-*v*4-g+h{THUxstCP-tl*k2GNm1#YS2W8wchfLo`obwJf z8fq)csgGskxKsnhR`H#c$%{ zD4)rfSgsPOe7>aEo7i$X6DpDm*DLzi>t?SUg{jTTTO?SKU2oqUpB|h3yRrM+;uIb# z%`$VJWa`d<7mer|vq{_m4-lkXUpxr9{zp?G-M0=yfW&9Zns3Um>N!p5M0?PBY9xa3 ziqJJ~;dzM@mBu_5Xm#Al^KGF$tZX!$<}gmQ-72Q5AzNS+JNJY5vZ-Zug@Fo2EJrR) zu;Y*wb;V5}xhuQr_V(wXS-v6RV9&f(oWU791`}%cGjnnF(CIp`s6aSdLcP z*~us33BXKxoid?IgU##j;&0L-qahfnE!nyGXoAopzzRm4YVurd5Tr@$*z!%MOhYLU zIlg)b^BU!SCAUC4lVlyc?-D`Q<0!cz{#X3WHZ-q+_MJS-1L?&ttmIkIOi^Ga8dds1 z{>drQ8{&+YCu(qiUX%=@SW#+OU0O>G&b>sRx<^?(}rew32SS*UTL0XG{ z>xGJh9Lj$6c(}|rl8sp_DI2vlj+P|iUKyYfL zw3th^GK|&vx`n&$XKmS^yeo+5^Op5hfhnTL6`sI|ZLxVf?g@5~T)rolHCjWde}icK z|3{wDRvJ5i(?Na~xnX(iC{MEiu-G~zeGg?hRdS`UWBx#gAE4`brwdKcs9h+BaTGZPE76DKS7%D z?Ijx@cbA=`d{J|57Hfz>k5vd#R)#OPvc!~AnDaznSxf$?8*TsCCc#ab>cg{H!ulgVcuxgc*Hoy2iL~%OH7UU!S zVHAFdrDXe8q=zD5#4X5~zplpv)zL-gjM;1t6JSKh3~PAH3}pCv5YmMSwH#SBuf(6L z4wotu5H&B1!kWM9kJctNcO}`8Pum_(W43r!7KEMVGB*O4tQdrN@HS4mv6ntjdVi4D zt=}@U$A+F`FkQF0t-k>gA5GDFQ53>GY3^RWv67c#s&Yio<=;*={s08cy@>v9f3_5Dr!1Iqu+oBHMR*#xhq^EjA;-UBM9j zLXgBn&XhM+{k2XkDhB=@{gcn^Wm?XMmC>87mII9}a32!I8{8|y8#*m_T%bg~B|OXq zlHiuDY8Gpg9qhpVfeX^`~VDxJ*1zcu5Wp4dU>O>4Am6)c&N zHhB^?0N18`c%beBj`~UXYw}uUz3!-^ogiUP{}z)9-tJ_w;Il{7a*neT=}`K`=!MIh zW4G!<*)?s3=c)+6wc6e|Uh_W!dECDEMkn(M6E9{o3e33yAcmROlh{GrldKD*kC`yv zw&xJNZ8MfDX|!hFrUfkg!q&pt&5F|;TU7I-wB?wjJahiH;yuTYy`0sIPbB-#h5KFk zSeAV;#UY&jU8n=bg<05?XIm`{i)8E06lVuK*cjqYE!6b_|BpEpRrBN|R3%@8HM&Zs zo8U8^;4l*Uy`(d5?T~=NX$(+zRfZ+2XPwB0Y24NMw6+tPJ@%4HPTw|M z=%$+Hp~LbT8DX>Z=n@M$ z>z)mX47k_B@Nh61z4KCQZlE&W&VxC*J#36o+=^QHH(;`e8M%P?V>ThMIj1gV-ZGh@ z&xH}b=~}*S8cFTgo;s^+K9G8Dn=j@4#4l@{c61_6XE`aJyO-PIB)=bHYNsrM)Tmo4 zzV;}7)6}@z8uSqPo=3P<|MLxH)3T&rEF8&;V@-QoEt zynPJimxnt*W)v|mSb)ju1}D9BxD%?-D=L?d?Zs0Lj})|&q2!^{CLy^)7fJ`=UMSeH zr*8E8@#5PfmRHH2xaVB_^3oI2wOkhaMygsKT^QUyu;s*l>bik~!@WJJT;QT1ntg0B z-g&~sJ5SVHB1YplUoQrxBl%IK(h$sa6`U4j1~We6Q_5zr(ikaRyhax9B@d5n7hf<< z&8m1Q6#Tu>G8kb}vwXrBpc&h^uFYy795_vp;hFpqruO4_1)o$gydvI~X{3LPIE8^* z%^SABSWJ7w@-)4HwZ`~bEGVT=vy~>#dORqz#x=TYWMr4WAk=XnL@F-~tjMsMC$iFr6A3egK+Kg~*&ZT=#~E;dh6 zryDLf0_rTg&=|(TWrJ;Qu@;q?4{MRwB15&Es$=1^+!|$(LEdD@x#O{hz(+ zEm~-cjsC~1a*#T#fY^%XI>v&M3Br-K>;eGha$9T^ ziQF|f8%9n7$6RgA;OwT)X6wgH_M800D(Nk^*S(K?ADQY@cVcBL@21$YDCch{_r)lV zO~FmKp9Bcc#Y6jr#$9og_4!pC%DZKz5zReZ-T^%BO%efgUs{0?8mL~sL5V|D|7!^n2y+DWbX z@3UZCA;>8nYhLWa-oX#147q`m_5%Ff9x?bl2R+n{UR8{*h3Y@*#-Afl@RNoy0bGHx z67*z^+2Ee%1bWtE!TY85iKB4{!dvJ_TW3HxEo#6>inT3q3m+9rtIMCBR(42LTEqA5?0#8< zd+jxJ*(SQmCfdZIkD9%|W2vh&EQvoEO8kop^t)Z%?{>WRz+?9Fyfrcb8+g~e z)|Dq6l1the*9~Zve+`FzV_yTpe<9bWJr%}J&5%YE(*imu~;194q~bbJ&}MtVx?1d?nBXLu{da6 zn0Jz1kP=>hTlJz}w%x@6v!Sjw&ccy5d_lQ(bYq$4;Q@o+HmTWt=9OvWU?x z{{*=0!4q$Y&#rZ-<+jyeS>4_Bg>jRh?z@kbMrBqUC|$BWPl6nt+>vT? z?i^S)>>nu-M6W!Znq!iOWCFzMdDT6W#aF+^Ph}_a!cyjZxTJ?In|q3C4(dYNd!s0; z6~;_o!b`DcQ}OeSCPB(7-r1Lq9|5|_hq;1R{elC60GR_xjHc0pp*d#o%g23Rp*b3J z%cM3$;r{VNx7%L1v)EVKR|iBr(#weUF@Vinrf4raLOot#X}xs3}!-xLzi4Zh7A1*D{JXTs37^T+N(=eT0)haN||165l%JnKh# zv29t@R1vW|f3mTBUN$U|Ws3LIttRWByl%?T@!KY2_3U`jcV>$Z&wY2>bG>jN*>-oi z(D5R*D98%wldL91G9BxKCkfdmg*F9v6g%19Q^gI4ES=Ia#Y#?lvn>!=BeTWOuGl9z ziij(W-O{mR<6}*h3MR`YesEG`p{nNlMNWw4`wqm|Q1&OP)*g*L;Z-SG)Zs7H3w2*- z8xpt-3{%Z9ca?B0TuU<-Qag;%(N0Rbi^Dx*&uVRHs>J=1&60!Kjg=xY1Lx*i$FpzM zloCRoePG5YROq0u$OQ`!%w7%+ZOkVmbj7ey~T zUNfO`{QN*s=O6NMH{w5gO-kP6Dr2Xv_1&veO=|IS^iO41GJ^($3F8j>eQoRJMsD4@ z9~TUW816LW@ze;T@g>Ge0SisTzPL8rJ7%kNgd54rGrO$Gmri3e0(JMaEexgi_jM88 zs6kP2X~h^hBj@->IW5OLxB9Le(jl~?O?{gtYbk{}GoQqfR$Fmqq9b|!Yc%3Rf5S~# z-yvf6(z1#M&3Ot}%D3{Ux7lo6@T{Z60hE@_VCUpi_nk1ycG8c#{zUmR+>s*tGX)oD zMd3S>>^v)l4wgtWZ2WHkv;)7Nhsk4UG*IC-ixYEI?>zg5S0&r3OB<@DuDI9SH;VY6 ze++WYyMGGew1tMgejr+G{S_SRsp@#2O-KY z?WX1M6W_M2klAgoo~Ai9Pqc37pvE+fi&4Bt7f6#Id+B(Il^^vLY}GVd^VQh+Q@-+h z+C9lIp#+J~rz?p{k^8ET>irM?e@^Zi=tMUn`*y3ASH}8>9T!R~0wt4#T^VNz>UmEO zmkI^NQ(8;TSKnm+vw)gpVHK5{$Wu7a2zB+BCp-(LKP-3VuId}u>i^vMVcCz`;;W$y z4NU8xzX2o}N*eN(iBZ;=6_;W5OXF5H7Y=5`{ic?&U2Vz^ z&5TXv&@hRVUy9&VZdwaS-^Rm_<{{~#QV%>_vK^WwjWFgKx~x_?H#PW*@u2zwnWWt3 zZ63KUcRrdn8?dzcgFf`Bi*o!kl zg;Z7BSwZa6RRXydvdSUUlnZei8Jc& z+Q#WNF&ioWd{CclEHr-;vGqn9P@6IWfWR55P?gSmVaCfme{K6$W#;cA$>(N;@$pNy z`kkC+`8vknv{$SlrFZX=3MXvjM}fR*GqC5!99ks~>ICnbau-F%69v=paap~X3FC?S z;33xsvGvAh&5NcTGO9S@Ims^WNgMxn4pRY=Cp~SG;|=>rx2G-V3yC4hzipzU(*DYY z#;IVfL#a+}#s9yGoExwFx5}^m5C|~+z<6%U$Fb&<^vnidI=`qLx(e{(Ec-RxB+j7i z?K&CUHMJbD&E6qa#_a|0D@>d|AS~62FH)w4BxC{zBV{1!|KHsSssk(0Daii)a`hBB z1B`}quL{G`4G|7e9iFo1lzNP9cSDTVP8EUtrrS?mxE(sMQQ6m>vJlEiml%9Hn!teH zw&%l`=>U_n;HMUG-0JPA0Q|C?j3D{hZ3nBbnEA1uUCdlU~_0QM!lWc;QANU5Auvb@69O0a-DaceRMjN9~J%oU=tG< zl&s1AOMf8tJ8l*u8LeaI&)#2RG9XJWD36bsZn8~YSCJVya`EYMm%nd^@yg8hRs1nP zE*wV+J&+-c4V4xTP{-5M8XftA`qfUSI|*@fDXf`< zK;Q?Z8f+GHtdTPt^IrVaZy*;ruIZ9cN=f% z#oquloP!z9b0ao~d*z?@m$uu(5Fur*Y`9kzyttDD-|4{#Qb=6|`u&OdAv0QQ8EdiH zvGiE96R!*AYe1f)`V2>TK9NKa_I8u}G5D9de50;Gw$YJ`HCXB1EAH)mFi3r<%dK*f zz0}ib`c{%chX^;~>1`r15=aO>2tS@Q$J)JNu+h#P^~vrT98hS2tq4KjvhaFfraQjh z1SjO9>O1uU0k3*HRpemxxY`Ycx03BDZ&WMK0Wbof?EZ*X8^o>DIvROjdrqDZt9b7H z!f7R7^%pDXw;78P`W4Jok=IG{(tlp@)0TS^#Sx{n9C2ZC)zleATY3|DLZ%Ce_IjD# zjH>{6`T%7VGVWJWhzV9sPmm$O_pJB_B%G9PLnC4GX1HsU{dP8L-U(&2SwOaOy=Jya z$glrEaky5fbex~s)l)Y@6chbpl+&%?1heL<9(n1x@H;(Bp^iuvexU(l{^wtG;U2f4& zy?1QmHm8p+{hYSjf!l7M07I%VKX=HL04;NDCh3%KZSwfzvTw?kZ_+wCO1S&e-QWE} zGTv?qj#TWUX&`>0HH6nk6)tS!-cN(+@-@P~P=?L>Z0+GslH>wh;{wsZryaLQSN7tf zVWaCUeZBEmfBfq9KHIt~`?LNqh6lm+izfRRDg}mfoZVMBG!WahL$-Wwd8ZrZJOgg} zrnOul=ugy}Y787rPmpDfGIRByndG1;XWA#J2=160oZ6X-Rxe_qhei)L%OF1|<98J{ z8e4mScISzWdMBbShDT0HL&Nlu%UY^Y6eFb-U1P=n&gWMNo=#$~p4DKHhOepqgCTQqf4VHLn-kM?+p-Y^q(ffe z6?ZVfJ)n)aRc2{s>i=I?2aRZR;_Iet>|TudCAOa)y$J%@EIHMQqV4RMb!=1CEf{^T z$3HslCoj^?L%)ZM4+u5gi9^@2V?oZz>FqT9f;)4uX#*P?+*Dpv(;^4G!N#D8w2+TZ zOQIwG(gYhH+jU$=W;q<`S1B!K{Y-7KVnSCkQru!w&*W&3vJ<$dj;b*uzSr(1wfEOWYmi)=B*$sM@fNOGxnM?Vx8YXP0qz>k78vsPV$iAP$;kV9xG zA=VHE5CGY@)10G5)~T6y9%V*q0&Ov5C^79cO;HCR11>Aa+=rCS{tJ;Udh9fEUe&iZ z=4AK1<0Cbpn-}i$fhV3ay+#s9TI0@yMby{;3}>@Cd}t16{CYT>%8y03#L#`yK#&+( za0gE(o#v9$wb)UIZJw+^K@>AyJ!pinD!q_Bxea zr=+BKG8AObYBJp;dGVZR!{pKJg&A4B&WE65^H!vr7o=A-F^fEQla3V>vavIt07C8 zPBY^_BP(m8zlN}J{cy_tCSQof_(SZq7XB|K#_AZDjIpe1sb}iuDy@RXs~f{AxmGKq zC&Z+<3GydBryFC4@bn{lo!q({94w`Y$$z-%n;Kn`M$o3oNKij3(Pr5&awG}G|6RHp zTj23Xq1-`fY8s~f>!VxBqQobv_^Kc2W9soKY!;qwJuq@m=n<_veyItBM?#Mb^=6o9 zpj&g^9~KSV)YD@KdScu+M$7lLH!W&%d_HyEJPSuUkD49u;w}JvidVjMQIvOz5G9>5 zAH5sT!MIWc*rW-^vyZ^H?J`1pG;}5IC3+`FuuYGq zum1L*t-KuF0Z(ibe*zBJQ1)iIPr=&QO`!1;nDr$_XPa`OG9R8= zam%Y)NOlRucuKQgRZIPPVxxIB%Flq;EFe$FV_dCD+Xn%2h^f8>H6Ff1e=irv)~s9316i!9q;?(hQ#c11$w9@)7-{J~ESM+S>1zC-E{j%oxwG�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

d4T#BOOH251{0^5UjqD=BSrn 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 literal 0 HcmV?d00001 diff --git a/visualizer/show3d_balls.py b/visualizer/show3d_balls.py index 2c9eaa3c9..230994322 100644 --- a/visualizer/show3d_balls.py +++ b/visualizer/show3d_balls.py @@ -1,12 +1,17 @@ +""" Original Author: Haoqiang Fan """ import numpy as np import ctypes as ct import cv2 import sys +import os + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) showsz = 800 mousex, mousey = 0.5, 0.5 zoom = 1.0 changed = True + def onmouse(*args): global mousex, mousey, changed y = args[1] @@ -15,98 +20,97 @@ def onmouse(*args): mousey = y / float(showsz) changed = True -# cv2.namedWindow('show3d') -# cv2.moveWindow('show3d', 0, 0) -# cv2.setMouseCallback('show3d', onmouse) -dll = np.ctypeslib.load_library('render_balls_so', '.') +cv2.namedWindow('show3d') +cv2.moveWindow('show3d', 0, 0) +cv2.setMouseCallback('show3d', onmouse) + +dll = np.ctypeslib.load_library(os.path.join(BASE_DIR, 'render_balls_so'), '.') -def showpoints(xyz,c_gt=None, c_pred = None, waittime=0, - showrot=False, magnifyBlue=0, freezerot=False, background=(0,0,0), - normalizecolor=True, ballradius=10): + +def showpoints(xyz, c_gt=None, c_pred=None, waittime=0, showrot=False, magnifyBlue=0, freezerot=False, + background=(0, 0, 0), normalizecolor=True, ballradius=10): global showsz, mousex, mousey, zoom, changed - xyz=xyz-xyz.mean(axis=0) - radius=((xyz**2).sum(axis=-1)**0.5).max() - xyz/=(radius*2.2)/showsz + xyz = xyz - xyz.mean(axis=0) + radius = ((xyz ** 2).sum(axis=-1) ** 0.5).max() + xyz /= (radius * 2.2) / showsz if c_gt is None: - c0 = np.zeros((len(xyz), ), dtype='float32') + 255 - c1 = np.zeros((len(xyz), ), dtype='float32') + 255 - c2 = np.zeros((len(xyz), ), dtype='float32') + 255 + c0 = np.zeros((len(xyz),), dtype='float32') + 255 + c1 = np.zeros((len(xyz),), dtype='float32') + 255 + c2 = np.zeros((len(xyz),), dtype='float32') + 255 else: c0 = c_gt[:, 0] c1 = c_gt[:, 1] c2 = c_gt[:, 2] - if normalizecolor: c0 /= (c0.max() + 1e-14) / 255.0 c1 /= (c1.max() + 1e-14) / 255.0 c2 /= (c2.max() + 1e-14) / 255.0 - c0 = np.require(c0, 'float32', 'C') c1 = np.require(c1, 'float32', 'C') c2 = np.require(c2, 'float32', 'C') show = np.zeros((showsz, showsz, 3), dtype='uint8') + def render(): - rotmat=np.eye(3) + rotmat = np.eye(3) if not freezerot: - xangle=(mousey-0.5)*np.pi*1.2 + xangle = (mousey - 0.5) * np.pi * 1.2 else: - xangle=0 - rotmat = rotmat.dot( - np.array([ - [1.0, 0.0, 0.0], - [0.0, np.cos(xangle), -np.sin(xangle)], - [0.0, np.sin(xangle), np.cos(xangle)], - ])) + xangle = 0 + rotmat = rotmat.dot(np.array([ + [1.0, 0.0, 0.0], + [0.0, np.cos(xangle), -np.sin(xangle)], + [0.0, np.sin(xangle), np.cos(xangle)], + ])) if not freezerot: yangle = (mousex - 0.5) * np.pi * 1.2 else: yangle = 0 - rotmat = rotmat.dot( - np.array([ - [np.cos(yangle), 0.0, -np.sin(yangle)], - [0.0, 1.0, 0.0], - [np.sin(yangle), 0.0, np.cos(yangle)], - ])) + rotmat = rotmat.dot(np.array([ + [np.cos(yangle), 0.0, -np.sin(yangle)], + [0.0, 1.0, 0.0], + [np.sin(yangle), 0.0, np.cos(yangle)], + ])) rotmat *= zoom nxyz = xyz.dot(rotmat) + [showsz / 2, showsz / 2, 0] ixyz = nxyz.astype('int32') show[:] = background dll.render_ball( - ct.c_int(show.shape[0]), ct.c_int(show.shape[1]), - show.ctypes.data_as(ct.c_void_p), ct.c_int(ixyz.shape[0]), - ixyz.ctypes.data_as(ct.c_void_p), c0.ctypes.data_as(ct.c_void_p), - c1.ctypes.data_as(ct.c_void_p), c2.ctypes.data_as(ct.c_void_p), - ct.c_int(ballradius)) + ct.c_int(show.shape[0]), + ct.c_int(show.shape[1]), + show.ctypes.data_as(ct.c_void_p), + ct.c_int(ixyz.shape[0]), + ixyz.ctypes.data_as(ct.c_void_p), + c0.ctypes.data_as(ct.c_void_p), + c1.ctypes.data_as(ct.c_void_p), + c2.ctypes.data_as(ct.c_void_p), + ct.c_int(ballradius) + ) if magnifyBlue > 0: - show[:, :, 0] = np.maximum(show[:, :, 0], np.roll( - show[:, :, 0], 1, axis=0)) + show[:, :, 0] = np.maximum(show[:, :, 0], np.roll(show[:, :, 0], 1, axis=0)) if magnifyBlue >= 2: - show[:, :, 0] = np.maximum(show[:, :, 0], - np.roll(show[:, :, 0], -1, axis=0)) - show[:, :, 0] = np.maximum(show[:, :, 0], np.roll( - show[:, :, 0], 1, axis=1)) + show[:, :, 0] = np.maximum(show[:, :, 0], np.roll(show[:, :, 0], -1, axis=0)) + show[:, :, 0] = np.maximum(show[:, :, 0], np.roll(show[:, :, 0], 1, axis=1)) if magnifyBlue >= 2: - show[:, :, 0] = np.maximum(show[:, :, 0], - np.roll(show[:, :, 0], -1, axis=1)) + show[:, :, 0] = np.maximum(show[:, :, 0], np.roll(show[:, :, 0], -1, axis=1)) if showrot: - cv2.putText(show, 'xangle %d' % (int(xangle / np.pi * 180)), - (30, showsz - 30), 0, 0.5, cv2.cv.CV_RGB(255, 0, 0)) - cv2.putText(show, 'yangle %d' % (int(yangle / np.pi * 180)), - (30, showsz - 50), 0, 0.5, cv2.cv.CV_RGB(255, 0, 0)) - cv2.putText(show, 'zoom %d%%' % (int(zoom * 100)), (30, showsz - 70), 0, - 0.5, cv2.cv.CV_RGB(255, 0, 0)) + cv2.putText(show, 'xangle %d' % (int(xangle / np.pi * 180)), (30, showsz - 30), 0, 0.5, + cv2.cv.CV_RGB(255, 0, 0)) + cv2.putText(show, 'yangle %d' % (int(yangle / np.pi * 180)), (30, showsz - 50), 0, 0.5, + cv2.cv.CV_RGB(255, 0, 0)) + cv2.putText(show, 'zoom %d%%' % (int(zoom * 100)), (30, showsz - 70), 0, 0.5, cv2.cv.CV_RGB(255, 0, 0)) + changed = True while True: if changed: render() changed = False - # cv2.imshow('show3d', show) + cv2.imshow('show3d', show) if waittime == 0: cmd = cv2.waitKey(10) % 256 else: @@ -119,18 +123,18 @@ def render(): if cmd == ord('t') or cmd == ord('p'): if cmd == ord('t'): if c_gt is None: - c0 = np.zeros((len(xyz), ), dtype='float32') + 255 - c1 = np.zeros((len(xyz), ), dtype='float32') + 255 - c2 = np.zeros((len(xyz), ), dtype='float32') + 255 + c0 = np.zeros((len(xyz),), dtype='float32') + 255 + c1 = np.zeros((len(xyz),), dtype='float32') + 255 + c2 = np.zeros((len(xyz),), dtype='float32') + 255 else: c0 = c_gt[:, 0] c1 = c_gt[:, 1] c2 = c_gt[:, 2] else: if c_pred is None: - c0 = np.zeros((len(xyz), ), dtype='float32') + 255 - c1 = np.zeros((len(xyz), ), dtype='float32') + 255 - c2 = np.zeros((len(xyz), ), dtype='float32') + 255 + c0 = np.zeros((len(xyz),), dtype='float32') + 255 + c1 = np.zeros((len(xyz),), dtype='float32') + 255 + c2 = np.zeros((len(xyz),), dtype='float32') + 255 else: c0 = c_pred[:, 0] c1 = c_pred[:, 1] @@ -144,21 +148,78 @@ def render(): c2 = np.require(c2, 'float32', 'C') changed = True - if cmd==ord('n'): - zoom*=1.1 - changed=True - elif cmd==ord('m'): - zoom/=1.1 - changed=True - elif cmd==ord('r'): - zoom=1.0 - changed=True - elif cmd==ord('s'): - cv2.imwrite('show3d.png',show) - if waittime!=0: + if cmd == ord('n'): + zoom *= 1.1 + changed = True + elif cmd == ord('m'): + zoom /= 1.1 + changed = True + elif cmd == ord('r'): + zoom = 1.0 + changed = True + elif cmd == ord('s'): + cv2.imwrite('show3d.png', show) + if waittime != 0: break return cmd + if __name__ == '__main__': - np.random.seed(100) - showpoints(np.random.randn(2500, 3)) \ No newline at end of file + import os + import numpy as np + import argparse + + parser = argparse.ArgumentParser() + parser.add_argument('--dataset', type=str, default='../data/shapenet', help='dataset path') + parser.add_argument('--category', type=str, default='Airplane', help='select category') + parser.add_argument('--npoints', type=int, default=2500, help='resample points number') + parser.add_argument('--ballradius', type=int, default=10, help='ballradius') + opt = parser.parse_args() + ''' + Airplane 02691156 + Bag 02773838 + Cap 02954340 + Car 02958343 + Chair 03001627 + Earphone 03261776 + Guitar 03467517 + Knife 03624134 + Lamp 03636649 + Laptop 03642806 + Motorbike 03790512 + Mug 03797390 + Pistol 03948459 + Rocket 04099429 + Skateboard 04225987 + Table 04379243''' + + cmap = np.array([[1.00000000e+00, 0.00000000e+00, 0.00000000e+00], + [3.12493437e-02, 1.00000000e+00, 1.31250131e-06], + [0.00000000e+00, 6.25019688e-02, 1.00000000e+00], + [1.00000000e+00, 0.00000000e+00, 9.37500000e-02], + [1.00000000e+00, 0.00000000e+00, 9.37500000e-02], + [1.00000000e+00, 0.00000000e+00, 9.37500000e-02], + [1.00000000e+00, 0.00000000e+00, 9.37500000e-02], + [1.00000000e+00, 0.00000000e+00, 9.37500000e-02], + [1.00000000e+00, 0.00000000e+00, 9.37500000e-02], + [1.00000000e+00, 0.00000000e+00, 9.37500000e-02]]) + BASE_DIR = os.path.dirname(os.path.abspath(__file__)) + ROOT_DIR = os.path.dirname(BASE_DIR) + sys.path.append(BASE_DIR) + sys.path.append(os.path.join(ROOT_DIR, 'data_utils')) + + from ShapeNetDataLoader import PartNormalDataset + root = '../data/shapenetcore_partanno_segmentation_benchmark_v0_normal/' + dataset = PartNormalDataset(root = root, npoints=2048, split='test', normal_channel=False) + idx = np.random.randint(0, len(dataset)) + data = dataset[idx] + point_set, _, seg = data + choice = np.random.choice(point_set.shape[0], opt.npoints, replace=True) + point_set, seg = point_set[choice, :], seg[choice] + seg = seg - seg.min() + gt = cmap[seg, :] + pred = cmap[seg, :] + showpoints(point_set, gt, c_pred=pred, waittime=0, showrot=False, magnifyBlue=0, freezerot=False, + background=(255, 255, 255), normalizecolor=True, ballradius=opt.ballradius) + +