diff --git a/.gitignore b/.gitignore index 7bbc71c..65a1202 100644 --- a/.gitignore +++ b/.gitignore @@ -99,3 +99,13 @@ ENV/ # mypy .mypy_cache/ +/.idea +/checkpoint +/conda_venv +/output +/pretrained +/train_data_viz +/conda_venv.tf1.0 +/checkpoint.sav +/checkpoints_worth_saving +/temp diff --git a/README.deckard.md b/README.deckard.md new file mode 100644 index 0000000..aa6590a --- /dev/null +++ b/README.deckard.md @@ -0,0 +1,49 @@ +# How to do training + * checkout the code with my modifications + * create a python2.7 conda venv per environment.py2.7_tf1.14.yml + * copy our own training dataset at s3://deckard-data-science-assets-us-east-2/curated_datasets/street_number_recognition + * train.tfrecord and val.tfrecord + * see s3://deckard-data-science-assets-us-east-2/curated_datasets/street_number_recognitionreadme.txt for how these + tfrecord files are prepared + * if you want to try different trunk DNN, update model_type/feat_layers/strides values in config.py + * source set_env.sh + * basically add the pylib/src to PYTHONPATH + * ./scripts/train.sh 0,1 24 + - 0,1 means use GPU0 and 1 + - 24 means for each batch, each GPU handle 24 images + * the artifacts of the run will be created at ./checkpoint + * open another terminal and run + ``` + python scripts/eval_fscore.py + ``` + This script will periodically evaluate the recall/precision/f1_score of the + latest checkpoint in ./checkpoint and save the result as tf_summary, which can be viewed with + tensorboard with other metrics saved by the main training process + + Notice it will also save a fscore.csv in ./checkpoint. + * If you want to use tensorboard to visualize the metrics, + ```tensorboard --logdir checkpoint``` + +# How to test the trained model on new images + * If you need to get the detected boxes as data + - first, run + ```./scripts/test.sh ${GPU_ID} ${checkpoint_folder}/model.ckpt-xxx ${image_dir}``` + The script will output the detected boxes as IC15 label file (one for each image) in + ${checkpoint_folder}/test/model.ckpt-xxx/txt/*.txt + + - then optionally, you can use street_num_spotting/src/street_num_spotting/convert_pixellink_test_result_to_our_json_format.py + to convert the IC15 label files to a json file (in our own label format) + + - then you can use street_num_spotting/notebook/recognition_result_reviewer.ipynb to review + the detection result. You need to update following config values accordingly + - config.image_base_dir, set to image_dir + - config.recognition_result_json_fpath, set to the json label file created in the step above + + * If you just want to see the detected text boxes rendered on the original images + ```./scripts/test_any.sh ${GPU_ID} ${checkpoint_folder}/model.ckpt-xxx ${image_dir} ${output_dir}``` + The script will output jpgs with bounding box rendered into output_dir + + +# About the metrics saved in tf.summary + * pixel_link_loss = prediction_loss_on_clones + regularization_loss + * clone0/xxx_loss, the prediction loss component on clone 0 \ No newline at end of file diff --git a/config.py b/config.py index 7ebac20..d562c53 100644 --- a/config.py +++ b/config.py @@ -8,6 +8,8 @@ import pixel_link slim = tf.contrib.slim +# optimizer = 'Adam' +optimizer = 'Momentum' #===================================================================== #====================Pre-processing params START====================== # VGG mean parameters. @@ -27,7 +29,7 @@ area_range = [0.1, 1] flip = False using_shorter_side_filtering=True -min_shorter_side = 10 +min_shorter_side = 6 max_shorter_side = np.infty #====================Pre-processing params END======================== #===================================================================== @@ -39,7 +41,7 @@ #====================Post-processing params START===================== decode_method = pixel_link.DECODE_METHOD_join min_area = 300 -min_height = 10 +min_height = 12 #====================Post-processing params END======================= #===================================================================== @@ -58,12 +60,28 @@ #pixel_neighbour_type = pixel_link.PIXEL_NEIGHBOUR_TYPE_4 -#model_type = pixel_link_symbol.MODEL_TYPE_vgg16 -#feat_layers = ['conv2_2', 'conv3_3', 'conv4_3', 'conv5_3', 'fc7'] -#strides = [2] +# model_type = pixel_link_symbol.MODEL_TYPE_vgg16 +# feat_layers = ['conv2_2', 'conv3_3', 'conv4_3', 'conv5_3', 'fc7'] +# strides = [2] + model_type = pixel_link_symbol.MODEL_TYPE_vgg16 +# with 512x512 input, +# conv3_3 output is 128x128x256 +# conv4_3 output is 64x64x512 +# conv5_3 output is 32x32x512 +# fc7 output is 32x32x1024 +# all output is after Relu feat_layers = ['conv3_3', 'conv4_3', 'conv5_3', 'fc7'] strides = [4] +# +# model_type = pixel_link_symbol.MODEL_TYPE_mobilenetv2 +# # with 512x512 input, +# # layer_4 output is 128x128x24 +# # layer_7 output is 64x64x32 +# # layer_14 output is 32x32x96 +# # layer_19 output is 16x16x1280 +# feat_layers = ['layer_4', 'layer_7', 'layer_14', 'layer_19'] +# strides = [4] pixel_cls_weight_method = pixel_link.PIXEL_CLS_WEIGHT_bbox_balanced bbox_border_width = 1 @@ -184,7 +202,7 @@ def print_ckpt(path): if not print_ckpt(flags.train_dir): print_ckpt(flags.checkpoint_path) - + pprint(flags.__flags, stream=stream) print('\n# =========================================================================== #', file=stream) diff --git a/datasets/dataset_factory.py b/datasets/dataset_factory.py index 9bc26d7..9302d1c 100644 --- a/datasets/dataset_factory.py +++ b/datasets/dataset_factory.py @@ -1,60 +1,88 @@ """A factory-pattern class which returns classification image/label pairs.""" from datasets import dataset_utils + class DatasetConfig(): def __init__(self, file_pattern, split_sizes): self.file_pattern = file_pattern self.split_sizes = split_sizes - + + icdar2013 = DatasetConfig( - file_pattern = '*_%s.tfrecord', - split_sizes = { - 'train': 229, - 'test': 233 - } + file_pattern='*_%s.tfrecord', + split_sizes={ + 'train': 229, + 'test': 233 + } ) icdar2015 = DatasetConfig( - file_pattern = 'icdar2015_%s.tfrecord', - split_sizes = { - 'train': 1000, - 'test': 500 - } + file_pattern='icdar2015_%s.tfrecord', + split_sizes={ + 'train': 1000, + 'test': 500 + } ) td500 = DatasetConfig( - file_pattern = '*_%s.tfrecord', - split_sizes = { - 'train': 300, - 'test': 200 - } + file_pattern='*_%s.tfrecord', + split_sizes={ + 'train': 300, + 'test': 200 + } ) tr400 = DatasetConfig( - file_pattern = 'tr400_%s.tfrecord', - split_sizes = { - 'train': 400 - } + file_pattern='tr400_%s.tfrecord', + split_sizes={ + 'train': 400 + } ) scut = DatasetConfig( - file_pattern = 'scut_%s.tfrecord', - split_sizes = { + file_pattern='scut_%s.tfrecord', + split_sizes={ 'train': 1715 } ) synthtext = DatasetConfig( - file_pattern = '*.tfrecord', -# file_pattern = 'SynthText_*.tfrecord', - split_sizes = { + file_pattern='*.tfrecord', + # file_pattern = 'SynthText_*.tfrecord', + split_sizes={ 'train': 858750 } ) +street_number = DatasetConfig( + file_pattern='%s.tfrecord', + split_sizes={ + 'train': 576, + 'val': 144, + } +) + +synthesized_149k_and_street_number_train = DatasetConfig( + file_pattern='*.tfrecord', + split_sizes={ + 'train': 149107 + 576, + } +) + +synthesized_149k = DatasetConfig( + file_pattern='bbox_clipped_within_image_boundary.tfrecord', + split_sizes={ + 'train': 149107, + } +) + + datasets_map = { - 'icdar2013':icdar2013, - 'icdar2015':icdar2015, - 'scut':scut, - 'td500':td500, - 'tr400':tr400, - 'synthtext':synthtext + 'icdar2013': icdar2013, + 'icdar2015': icdar2015, + 'scut': scut, + 'td500': td500, + 'tr400': tr400, + 'synthtext': synthtext, + 'street_number': street_number, + 'synthesized_149k_and_street_number_train': synthesized_149k_and_street_number_train, + 'synthesized_149k': synthesized_149k, } @@ -76,4 +104,4 @@ def get_dataset(dataset_name, split_name, dataset_dir, reader=None): dataset_config = datasets_map[dataset_name]; file_pattern = dataset_config.file_pattern num_samples = dataset_config.split_sizes[split_name] - return dataset_utils.get_split(split_name, dataset_dir,file_pattern, num_samples, reader) + return dataset_utils.get_split(split_name, dataset_dir, file_pattern, num_samples, reader) diff --git a/datasets/dataset_utils.py b/datasets/dataset_utils.py index c8777fc..677988e 100644 --- a/datasets/dataset_utils.py +++ b/datasets/dataset_utils.py @@ -137,6 +137,7 @@ def get_list(obj, idx): 'image/encoded': bytes_feature(image_data)})) return example + def get_split(split_name, dataset_dir, file_pattern, num_samples, reader=None): dataset_dir = util.io.get_absolute_path(dataset_dir) @@ -144,6 +145,7 @@ def get_split(split_name, dataset_dir, file_pattern, num_samples, reader=None): file_pattern = util.io.join_path(dataset_dir, file_pattern % split_name) else: file_pattern = util.io.join_path(dataset_dir, file_pattern) + # Allowing None in the signature so that dataset_factory can use the default. if reader is None: reader = tf.TFRecordReader diff --git a/datasets/ic15_like_data_to_tfrecords.py b/datasets/ic15_like_data_to_tfrecords.py new file mode 100644 index 0000000..f781e0a --- /dev/null +++ b/datasets/ic15_like_data_to_tfrecords.py @@ -0,0 +1,92 @@ +# encoding=utf-8 +import argparse +import os + +import numpy as np +import tensorflow as tf +import util +from dataset_utils import int64_feature, float_feature, bytes_feature, convert_to_example +import config + + +def cvt_to_tfrecords(output_path, data_path, gt_path): + image_names = util.io.ls(data_path, '.jpg') # [0:10] + print "%d images found in %s" % (len(image_names), data_path) + with tf.python_io.TFRecordWriter(output_path) as tfrecord_writer: + for idx, image_name in enumerate(image_names): + oriented_bboxes = [] + bboxes = [] + labels = [] # a mask for labels_text list to indicate whether it is a 'ignored' label text or not + labels_text = [] + path = util.io.join_path(data_path, image_name) + print "\tconverting image: %d/%d %s" % (idx, len(image_names), image_name) + image_data = tf.gfile.FastGFile(path, 'r').read() + + image = util.img.imread(path, rgb=True) + shape = image.shape + h, w = shape[0:2] + h *= 1.0 + w *= 1.0 + image_name = util.str.split(image_name, '.')[0] + gt_name = 'gt_' + image_name + '.txt' + gt_filepath = util.io.join_path(gt_path, gt_name) + lines = util.io.read_lines(gt_filepath) + + for line in lines: + line = util.str.remove_all(line, '\xef\xbb\xbf') + gt = util.str.split(line, ',') + oriented_box = [int(gt[i]) for i in range(8)] + oriented_box = np.asarray(oriented_box) / ([w, h] * 4) + # notice in the case of syntehsized data, + # it is possible some bbox will extend through the image boundary. # + oriented_box = np.clip(oriented_box, 0.0, 1.0) + + oriented_bboxes.append(oriented_box) + + xs = oriented_box.reshape(4, 2)[:, 0] + ys = oriented_box.reshape(4, 2)[:, 1] + xmin = xs.min() + xmax = xs.max() + ymin = ys.min() + ymax = ys.max() + bboxes.append([xmin, ymin, xmax, ymax]) + + # might be wrong here, but it doesn't matter because the label is not going to be used in detection + labels_text.append(gt[-1]) + ignored = util.str.contains(gt[-1], '###') + if ignored: + labels.append(config.ignore_label) + else: + labels.append(config.text_label) + example = convert_to_example(image_data, image_name, labels, labels_text, bboxes, oriented_bboxes, shape) + tfrecord_writer.write(example.SerializeToString()) + + +def main(args): + output_dir = os.path.dirname(args.output_tfrecords_path) + if not os.path.exists(output_dir): + os.makedirs(output_dir) + cvt_to_tfrecords(output_path=args.output_tfrecords_path, data_path=args.image_dir, gt_path=args.label_dir) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument("-i", "--image_dir", default='/home/victor/workspace/datasets/scene_text/street_number_recognition/train') + parser.add_argument("-l", "--label_dir", default='/home/victor/workspace/datasets/scene_text/street_number_recognition/train_ic15_format_label') + parser.add_argument("-t", "--output_tfrecords_path", default='/home/victor/workspace/datasets/scene_text/street_number_recognition/train.tfrecord') + args = parser.parse_args() + main(args) + + # root_dir = util.io.get_absolute_path('~/workspace/datasets/scene_text/ICDAR2015/detection/') + # output_dir = util.io.get_absolute_path('~/workspace/pixel_link/tfrecord/icdar2015/') + # util.io.mkdir(output_dir) + # training_data_dir = util.io.join_path(root_dir, 'ch4_training_images') + # training_gt_dir = util.io.join_path(root_dir, 'ch4_training_localization_transcription_gt') + # + # test_data_dir = util.io.join_path(root_dir, 'ch4_test_images') + # test_gt_dir = util.io.join_path(root_dir, 'ch4_test_localization_transcription_gt') + # cvt_to_tfrecords(output_path=util.io.join_path(output_dir, 'icdar2015_test.tfrecord'), data_path=test_data_dir, + # gt_path=test_gt_dir) + # + + diff --git a/datasets/icdar2015_to_tfrecords.py b/datasets/icdar2015_to_tfrecords.py deleted file mode 100644 index 2133292..0000000 --- a/datasets/icdar2015_to_tfrecords.py +++ /dev/null @@ -1,68 +0,0 @@ -#encoding=utf-8 -import numpy as np; -import tensorflow as tf -import util -from dataset_utils import int64_feature, float_feature, bytes_feature, convert_to_example -import config - - -def cvt_to_tfrecords(output_path , data_path, gt_path): - image_names = util.io.ls(data_path, '.jpg')#[0:10]; - print "%d images found in %s"%(len(image_names), data_path); - with tf.python_io.TFRecordWriter(output_path) as tfrecord_writer: - for idx, image_name in enumerate(image_names): - oriented_bboxes = []; - bboxes = [] - labels = []; - labels_text = []; - path = util.io.join_path(data_path, image_name); - print "\tconverting image: %d/%d %s"%(idx, len(image_names), image_name); - image_data = tf.gfile.FastGFile(path, 'r').read() - - image = util.img.imread(path, rgb = True); - shape = image.shape - h, w = shape[0:2]; - h *= 1.0; - w *= 1.0; - image_name = util.str.split(image_name, '.')[0]; - gt_name = 'gt_' + image_name + '.txt'; - gt_filepath = util.io.join_path(gt_path, gt_name); - lines = util.io.read_lines(gt_filepath); - - for line in lines: - line = util.str.remove_all(line, '\xef\xbb\xbf') - gt = util.str.split(line, ','); - oriented_box = [int(gt[i]) for i in range(8)]; - oriented_box = np.asarray(oriented_box) / ([w, h] * 4); - oriented_bboxes.append(oriented_box); - - xs = oriented_box.reshape(4, 2)[:, 0] - ys = oriented_box.reshape(4, 2)[:, 1] - xmin = xs.min() - xmax = xs.max() - ymin = ys.min() - ymax = ys.max() - bboxes.append([xmin, ymin, xmax, ymax]) - - # might be wrong here, but it doesn't matter because the label is not going to be used in detection - labels_text.append(gt[-1]); - ignored = util.str.contains(gt[-1], '###') - if ignored: - labels.append(config.ignore_label); - else: - labels.append(config.text_label) - example = convert_to_example(image_data, image_name, labels, labels_text, bboxes, oriented_bboxes, shape) - tfrecord_writer.write(example.SerializeToString()) - -if __name__ == "__main__": - root_dir = util.io.get_absolute_path('~/dataset/ICDAR2015/Challenge4/') - output_dir = util.io.get_absolute_path('~/dataset/pixel_link/ICDAR/') - util.io.mkdir(output_dir); - - training_data_dir = util.io.join_path(root_dir, 'ch4_training_images') - training_gt_dir = util.io.join_path(root_dir,'ch4_training_localization_transcription_gt') - cvt_to_tfrecords(output_path = util.io.join_path(output_dir, 'icdar2015_train.tfrecord'), data_path = training_data_dir, gt_path = training_gt_dir) - - test_data_dir = util.io.join_path(root_dir, 'ch4_test_images') - test_gt_dir = util.io.join_path(root_dir,'ch4_test_localization_transcription_gt') - cvt_to_tfrecords(output_path = util.io.join_path(output_dir, 'icdar2015_test.tfrecord'), data_path = test_data_dir, gt_path = test_gt_dir) diff --git a/detect_street_number_with_pixellink_model.py b/detect_street_number_with_pixellink_model.py new file mode 100644 index 0000000..6b3afff --- /dev/null +++ b/detect_street_number_with_pixellink_model.py @@ -0,0 +1,227 @@ +# encoding = utf-8 +""" +Input: + * house_front_image_listing_csv, mandatory column: image_fpath, + the path is relative to the image_base_dir below + * image_base_dir + * checkpoint_path, the checkpoint file of trained model + * detection_result_output_json_path, notice it is a line json file, i.e. each line is a json obj + * optional detection_result_image_detection_result_image_output_dir + +Output: + * create detection_result_output_json file at detection_result_output_json_path + Notice that only images which have some street number detected will appear in this file + + * optionally output images with detected bounding boxes rendered in detection_result_image_detection_result_image_output_dir. + Notice that only images which have some street number detected will be output here +""" +import glob +import json +import os + +import numpy as np +import tensorflow as tf +import pandas as pd + +import pixel_link +from preprocessing import ssd_vgg_preprocessing +from nets import pixel_link_symbol +import config +import util + +slim = tf.contrib.slim + + +tf.app.flags.DEFINE_string( + 'house_front_image_listing_csv', + '/home/victor/workspace/str_image_match/sample_data/mono/airbnb_house_front_image.excluding_duplicated_8_times_or_more.mono.csv', + 'csv which list all house front image files in image_base_dir' +) +tf.app.flags.DEFINE_string( + 'image_base_dir', + '/home/victor/workspace/str_image_match/images/airbnb_image', + 'base folder where the images files reside' +) +tf.app.flags.DEFINE_string( + 'checkpoint_path', + 'runs/checkpoint_0.879/model.ckpt-20677', + 'the path of pretrained model to be used' +) +tf.app.flags.DEFINE_string( + 'detection_result_output_json_path', + '/home/victor/workspace/street_num_spotting/data/street_number_detection_result.mono.json', + 'the target path to create the output json' +) +tf.app.flags.DEFINE_string( + 'detection_result_image_output_dir', + './output', + 'The directory where the output images should be saved.' +) + +tf.app.flags.DEFINE_integer('eval_image_width', 1280, 'resized image width for inference') +tf.app.flags.DEFINE_integer('eval_image_height', 768, 'resized image height for inference') +tf.app.flags.DEFINE_float('pixel_conf_threshold', 0.5, 'threshold on the pixel confidence') +tf.app.flags.DEFINE_float('link_conf_threshold', 0.5, 'threshold on the link confidence') + +tf.app.flags.DEFINE_float('gpu_memory_fraction', -1, + 'the gpu memory fraction to be used. If less than 0, allow_growth = True is used.') + +tf.app.flags.DEFINE_bool('using_moving_average', True, 'Whether to use ExponentionalMovingAverage') +tf.app.flags.DEFINE_float('moving_average_decay', 0.9999, 'The decay rate of ExponentionalMovingAverage') + +FLAGS = tf.app.flags.FLAGS + + +def config_initialization(): + # image shape and feature layers shape inference + image_shape = (FLAGS.eval_image_height, FLAGS.eval_image_width) + + if not FLAGS.image_base_dir: + raise ValueError('You must supply the dataset directory with --image_base_dir') + + tf.logging.set_verbosity(tf.logging.DEBUG) + + config.init_config(image_shape, + batch_size=1, + pixel_conf_threshold=FLAGS.pixel_conf_threshold, + link_conf_threshold=FLAGS.link_conf_threshold, + num_gpus=1, + ) + + +def create_model(): + with tf.name_scope('evaluation_%dx%d' % (FLAGS.eval_image_height, FLAGS.eval_image_width)): + with tf.variable_scope(tf.get_variable_scope(), reuse=False): + image = tf.placeholder(dtype=tf.int32, shape=[None, None, 3]) + processed_image, _, _, _, _ = ssd_vgg_preprocessing.preprocess_image(image, None, None, None, None, + out_shape=config.image_shape, + data_format=config.data_format, + is_training=False) + b_image = tf.expand_dims(processed_image, axis=0) + + # build model and loss + net = pixel_link_symbol.PixelLinkNet(b_image, is_training=False) + masks = pixel_link.tf_decode_score_map_to_mask_in_batch( + net.pixel_pos_scores, net.link_pos_scores) + return image, masks, net + + +def prepare_session_config(): + sess_config = tf.ConfigProto(log_device_placement=False, allow_soft_placement=True) + if FLAGS.gpu_memory_fraction < 0: + sess_config.gpu_options.allow_growth = True + elif FLAGS.gpu_memory_fraction > 0: + sess_config.gpu_options.per_process_gpu_memory_fraction = FLAGS.gpu_memory_fraction; + + +def draw_bboxes(img, bboxes, color): + for bbox in bboxes: + points = np.reshape(bbox, [4, 2]) + cnts = util.img.points_to_contours(points) + util.img.draw_contours(img, contours=cnts, + idx=-1, color=color, border_width=1) + + +def list_image_in_folder_recursively(image_base_dir): + return [os.path.relpath(f, image_base_dir) + for f in glob.glob(image_base_dir + '/**/*.jpg', recursive=True)] + + +def convert_to_json(bbox): + return { + "label": "", + "points": [[int(round(v[0])), int(round(v[1]))] for v in bbox.reshape([4, 2])] + } + + +def save_detection_result_as_line_json(bboxes_det, image_rel_path, output_json_file): + """ + + :param bboxes_det: a list of 1D numpy array. Each array is of size 8, which gives the x,y of 4 points + :param image_rel_path: identify the original image + :param output_json_file: file object + :return: + """ + json_obj = dict( + image_fname=image_rel_path, + detected_numbers=[convert_to_json(bbox) for bbox in bboxes_det] + ) + output_json_file.write(json.dumps(json_obj) + '\n') + + +def run_inference(global_step, image, masks, net): + variables_to_restore = get_variables_to_restore(global_step) + saver = tf.train.Saver(var_list=variables_to_restore) + + with tf.Session() as sess: + saver.restore(sess, util.tf.get_latest_ckpt(FLAGS.checkpoint_path)) + tf.logging.info('model restored') + + with open(FLAGS.detection_result_output_json_path, 'w') as output_json_file: + src_image_listing_df = pd.read_csv(FLAGS.house_front_image_listing_csv) + total_image_num = len(src_image_listing_df) + + for i, image_rel_path in enumerate(src_image_listing_df['image_path']): + file_path = util.io.join_path(FLAGS.image_base_dir, image_rel_path) + + image_data = util.img.imread(file_path, rgb=True) + link_scores, pixel_scores, mask_vals = sess.run( + [net.link_pos_scores, net.pixel_pos_scores, masks], + feed_dict={image: image_data}) + + image_idx = 0 # only one image in the batch + mask = mask_vals[image_idx, ...] + bboxes_det = pixel_link.mask_to_bboxes(mask, image_data.shape) + + if len(bboxes_det) == 0: + continue + + save_detection_result_as_line_json(bboxes_det, image_rel_path, output_json_file) + + if FLAGS.detection_result_image_output_dir: + save_image_with_rendered_bboxes(bboxes_det, image_data, image_rel_path) + + print 'processed %d/%d\r images' % (i, total_image_num), + + print('Done') + + +def save_image_with_rendered_bboxes(bboxes_det, image_data, image_rel_path): + draw_bboxes(image_data, bboxes_det, util.img.COLOR_RGB_RED) + output_img_path = os.path.join(FLAGS.detection_result_image_output_dir, image_rel_path) + output_dir = os.path.dirname(output_img_path) + if not os.path.exists(output_dir): + os.makedirs(output_dir) + util.sit(image_data, path=output_img_path) # save image to the output file + + +def get_variables_to_restore(global_step): + # Variables to restore: moving avg. or normal weights. + if FLAGS.using_moving_average: + variable_averages = tf.train.ExponentialMovingAverage( + FLAGS.moving_average_decay) + variables_to_restore = variable_averages.variables_to_restore( + tf.trainable_variables()) + variables_to_restore[global_step.op.name] = global_step + else: + variables_to_restore = slim.get_variables_to_restore() + return variables_to_restore + + +def main(_): + config_initialization() + + if FLAGS.detection_result_image_output_dir: + if os.path.exists(FLAGS.detection_result_image_output_dir): + os.system('rm -rf %s' % FLAGS.detection_result_image_output_dir) + os.makedirs(FLAGS.detection_result_image_output_dir) + + global_step = slim.get_or_create_global_step() + image, masks, net = create_model() + prepare_session_config() + run_inference(global_step, image, masks, net) + + +if __name__ == '__main__': + tf.app.run() + diff --git a/environment.py2.7_tf1.14.yml b/environment.py2.7_tf1.14.yml new file mode 100644 index 0000000..4280963 --- /dev/null +++ b/environment.py2.7_tf1.14.yml @@ -0,0 +1,105 @@ +name: /home/victor/workspace/pixel_link/conda_venv +channels: + - menpo + - defaults +dependencies: + - _libgcc_mutex=0.1=main + - _tflow_select=2.1.0=gpu + - absl-py=0.8.1=py27_0 + - astor=0.8.0=py27_0 + - backports=1.0=py_2 + - backports.functools_lru_cache=1.5=py_2 + - backports.weakref=1.0.post1=py_1 + - backports_abc=0.5=py27_0 + - blas=1.0=mkl + - c-ares=1.15.0=h7b6447c_1001 + - ca-certificates=2019.10.16=0 + - certifi=2019.9.11=py27_0 + - cudatoolkit=10.1.243=h6bb024c_0 + - cudnn=7.6.4=cuda10.1_0 + - cupti=10.1.168=0 + - cycler=0.10.0=py27_0 + - cython=0.28.5=py27hf484d3e_0 + - dbus=1.13.12=h746ee38_0 + - enum34=1.1.6=py27_1 + - expat=2.2.6=he6710b0_0 + - fontconfig=2.13.0=h9420a91_0 + - freetype=2.9.1=h8a8886c_1 + - funcsigs=1.0.2=py27_0 + - functools32=3.2.3.2=py27_1 + - future=0.18.2=py27_0 + - futures=3.3.0=py27_0 + - gast=0.3.2=py_0 + - glib=2.63.1=h5a9c865_0 + - google-pasta=0.1.7=py_0 + - grpcio=1.16.1=py27hf8bcb03_1 + - gst-plugins-base=1.14.0=hbbd80ab_1 + - gstreamer=1.14.0=hb453b48_1 + - h5py=2.9.0=py27h7918eee_0 + - hdf5=1.10.4=hb1b8bf9_0 + - icu=58.2=h9c2bf20_1 + - intel-openmp=2019.4=243 + - jpeg=9b=h024ee3a_2 + - keras-applications=1.0.8=py_0 + - keras-preprocessing=1.1.0=py_1 + - kiwisolver=1.1.0=py27he6710b0_0 + - libedit=3.1.20181209=hc058e9b_0 + - libffi=3.2.1=hd88cf55_4 + - libgcc-ng=9.1.0=hdf63c60_0 + - libgfortran-ng=7.3.0=hdf63c60_0 + - libpng=1.6.37=hbc83047_0 + - libprotobuf=3.9.2=hd408876_0 + - libstdcxx-ng=9.1.0=hdf63c60_0 + - libuuid=1.0.3=h1bed415_2 + - libxcb=1.13=h1bed415_1 + - libxml2=2.9.9=hea5a465_1 + - linecache2=1.0.0=py27_0 + - markdown=3.1.1=py27_0 + - matplotlib=2.2.3=py27hb69df0a_0 + - mkl=2019.4=243 + - mkl-service=2.3.0=py27he904b0f_0 + - mkl_fft=1.0.15=py27ha843d7b_0 + - mkl_random=1.1.0=py27hd6b4f25_0 + - mock=3.0.5=py27_0 + - ncurses=6.1=he6710b0_1 + - numpy=1.16.5=py27h7e9f1db_0 + - numpy-base=1.16.5=py27hde5b4d6_0 + - opencv=2.4.11=nppy27_0 + - openssl=1.1.1d=h7b6447c_3 + - pcre=8.43=he6710b0_0 + - pip=19.3.1=py27_0 + - protobuf=3.9.2=py27he6710b0_0 + - pyparsing=2.4.2=py_0 + - pyqt=5.9.2=py27h05f1152_2 + - python=2.7.17=h9bab390_0 + - python-dateutil=2.8.1=py_0 + - pytz=2019.3=py_0 + - qt=5.9.7=h5867ecd_1 + - readline=7.0=h7b6447c_5 + - scipy=1.2.1=py27h7c811a0_0 + - setuptools=41.6.0=py27_0 + - singledispatch=3.4.0.3=py27_0 + - sip=4.19.8=py27hf484d3e_0 + - six=1.12.0=py27_0 + - sqlite=3.30.1=h7b6447c_0 + - subprocess32=3.5.4=py27h7b6447c_0 + - tensorboard=1.14.0=py27hf484d3e_0 + - tensorflow=1.14.0=gpu_py27h39f1c70_0 + - tensorflow-base=1.14.0=gpu_py27he45bfe2_0 + - tensorflow-estimator=1.14.0=py_0 + - tensorflow-gpu=1.14.0=h0d30ee6_0 + - termcolor=1.1.0=py27_1 + - tk=8.6.8=hbc83047_0 + - tornado=5.1.1=py27h7b6447c_0 + - traceback2=1.4.0=py27_0 + - unittest2=1.1.0=py27_0 + - werkzeug=0.16.0=py_0 + - wheel=0.33.6=py27_0 + - wrapt=1.11.2=py27h7b6447c_0 + - xz=5.2.4=h14c3975_4 + - zlib=1.2.11=h7b6447c_3 + - pip: + - polygon2==2.0.8 + - setproctitle==1.1.10 +prefix: /home/victor/workspace/pixel_link/conda_venv + diff --git a/environment.yml b/environment.yml new file mode 100644 index 0000000..caec47e --- /dev/null +++ b/environment.yml @@ -0,0 +1,46 @@ +name: pixel_link +channels: +- menpo +- defaults +dependencies: +- certifi=2016.2.28=py27_0 +- cudatoolkit=7.5=2 +- cudnn=5.1=0 +- funcsigs=1.0.2=py27_0 +- libprotobuf=3.4.0=0 +- mkl=2017.0.3=0 +- mock=2.0.0=py27_0 +- numpy=1.12.1=py27_0 +- openssl=1.0.2l=0 +- pbr=1.10.0=py27_0 +- pip=9.0.1=py27_1 +- protobuf=3.4.0=py27_0 +- python=2.7.13=0 +- readline=6.2=2 +- setuptools=36.4.0=py27_1 +- six=1.10.0=py27_0 +- sqlite=3.13.0=0 +- tensorflow-gpu=1.1.0=np112py27_0 +- tk=8.5.18=0 +- werkzeug=0.12.2=py27_0 +- wheel=0.29.0=py27_0 +- zlib=1.2.11=0 +- opencv=2.4.11=nppy27_0 +- pip: + - backports.functools-lru-cache==1.5 + - bottle==0.12.13 + - cycler==0.10.0 + - cython==0.28.2 + - enum34==1.1.6 + - kiwisolver==1.0.1 + - matplotlib==2.2.2 + - olefile==0.44 + - pillow==4.3.0 + - polygon2==2.0.8 + - pyparsing==2.2.0 + - python-dateutil==2.7.2 + - pytz==2018.4 + - setproctitle==1.1.10 + - subprocess32==3.2.7 + - tensorflow==1.1.0 + - virtualenv==15.1.0 diff --git a/evaluation_script/gt.zip b/evaluation_script/gt.zip new file mode 100644 index 0000000..aaa2ac3 Binary files /dev/null and b/evaluation_script/gt.zip differ diff --git a/evaluation_script/readme.txt b/evaluation_script/readme.txt new file mode 100644 index 0000000..018bcaa --- /dev/null +++ b/evaluation_script/readme.txt @@ -0,0 +1,23 @@ +INSTRUCTIONS FOR THE STANDALONE SCRIPTS +Requirements: +- Python version 2.7. +- Each Task requires different Python modules. When running the script, if some module is not installed you will see a notification and installation instructions. + +Procedure: +Download the ZIP file for the requested script and unzip it to a directory. + +Open a terminal in the directory and run the command: +python script.py –g=gt.zip –s=submit.zip + +If you have already installed all the required modules, then you will see the method’s results or an error message if the submitted file is not correct. + +parameters: +-g: Path of the Ground Truth file. In most cases, the Ground Truth will be included in the same Zip file named 'gt.zip', gt.txt' or 'gt.json'. If not, you will be able to get it on the Downloads page of the Task. +-s: Path of your method's results file. + +Optional parameters: +-o: Path to a directory where to copy the file ‘results.zip’ that contains per-sample results. +-p: JSON string parameters to override the script default parameters. +The parameters that can be overrided are inside the function 'default_evaluation_params' located at the begining of the evaluation Script. + +Example: python script.py –g=gt.zip –s=submit.zip –o=./ -p='{" IOU_CONSTRAINT" = 0.8}' \ No newline at end of file diff --git a/evaluation_script/rrc_evaluation_funcs.py b/evaluation_script/rrc_evaluation_funcs.py new file mode 100644 index 0000000..ce296a4 --- /dev/null +++ b/evaluation_script/rrc_evaluation_funcs.py @@ -0,0 +1,392 @@ +#!/usr/bin/env python2 +# encoding: UTF-8 +import json +import sys; + +sys.path.append('./') +import zipfile +import re +import sys +import os +import codecs +import importlib +from StringIO import StringIO + + +def print_help(): + sys.stdout.write('Usage: python %s.py -g= -s= [-o= -p=]' % sys.argv[0]) + sys.exit(2) + + +def load_zip_file_keys(file, fileNameRegExp=''): + """ + Returns an array with the entries of the ZIP file that match with the regular expression. + The key's are the names or the file or the capturing group definied in the fileNameRegExp + """ + try: + archive = zipfile.ZipFile(file, mode='r', allowZip64=True) + except: + raise Exception('Error loading the ZIP archive %s.' % file) + + pairs = [] + + for name in archive.namelist(): + addFile = True + keyName = name + if fileNameRegExp != "": + m = re.match(fileNameRegExp, name) + if m == None: + addFile = False + else: + if len(m.groups()) > 0: + keyName = m.group(1) + + if addFile: + pairs.append(keyName) + + return pairs + + +def load_zip_file(file, fileNameRegExp='', allEntries=False): + """ + Returns an array with the contents (filtered by fileNameRegExp) of a ZIP file. + The key's are the names or the file or the capturing group definied in the fileNameRegExp + allEntries validates that all entries in the ZIP file pass the fileNameRegExp + """ + try: + archive = zipfile.ZipFile(file, mode='r', allowZip64=True) + except: + raise Exception('Error loading the ZIP archive %s' % file) + + pairs = [] + for name in archive.namelist(): + addFile = True + keyName = name + if fileNameRegExp != "": + m = re.match(fileNameRegExp, name) + if m == None: + addFile = False + else: + if len(m.groups()) > 0: + keyName = m.group(1) + + if addFile: + pairs.append([keyName, archive.read(name)]) + else: + if allEntries: + raise Exception('ZIP entry not valid: %s' % name) + + return dict(pairs) + + +def decode_utf8(raw): + """ + Returns a Unicode object on success, or None on failure + """ + try: + raw = codecs.decode(raw, 'utf-8', 'replace') + # extracts BOM if exists + raw = raw.encode('utf8') + if raw.startswith(codecs.BOM_UTF8): + raw = raw.replace(codecs.BOM_UTF8, '', 1) + return raw.decode('utf-8') + except: + return None + + +def validate_lines_in_file(fileName, file_contents, CRLF=True, LTRB=True, withTranscription=False, withConfidence=False, + imWidth=0, imHeight=0): + """ + This function validates that all lines of the file calling the Line validation function for each line + """ + utf8File = decode_utf8(file_contents) + if (utf8File is None): + raise Exception("The file %s is not UTF-8" % fileName) + + lines = utf8File.split("\r\n" if CRLF else "\n") + for line in lines: + line = line.replace("\r", "").replace("\n", "") + if (line != ""): + try: + validate_tl_line(line, LTRB, withTranscription, withConfidence, imWidth, imHeight) + except Exception as e: + raise Exception( + ("Line in sample not valid. Sample: %s Line: %s Error: %s" % (fileName, line, str(e))).encode( + 'utf-8', 'replace')) + + +def validate_tl_line(line, LTRB=True, withTranscription=True, withConfidence=True, imWidth=0, imHeight=0): + """ + Validate the format of the line. If the line is not valid an exception will be raised. + If maxWidth and maxHeight are specified, all points must be inside the imgage bounds. + Posible values are: + LTRB=True: xmin,ymin,xmax,ymax[,confidence][,transcription] + LTRB=False: x1,y1,x2,y2,x3,y3,x4,y4[,confidence][,transcription] + """ + get_tl_line_values(line, LTRB, withTranscription, withConfidence, imWidth, imHeight) + + +def get_tl_line_values(line, LTRB=True, withTranscription=False, withConfidence=False, imWidth=0, imHeight=0): + """ + Validate the format of the line. If the line is not valid an exception will be raised. + If maxWidth and maxHeight are specified, all points must be inside the imgage bounds. + Posible values are: + LTRB=True: xmin,ymin,xmax,ymax[,confidence][,transcription] + LTRB=False: x1,y1,x2,y2,x3,y3,x4,y4[,confidence][,transcription] + Returns values from a textline. Points , [Confidences], [Transcriptions] + """ + confidence = 0.0 + transcription = ""; + points = [] + + numPoints = 4; + + if LTRB: + + numPoints = 4; + + if withTranscription and withConfidence: + m = re.match( + r'^\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-1].?[0-9]*)\s*,(.*)$', line) + if m == None: + m = re.match( + r'^\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-1].?[0-9]*)\s*,(.*)$', + line) + raise Exception("Format incorrect. Should be: xmin,ymin,xmax,ymax,confidence,transcription") + elif withConfidence: + m = re.match(r'^\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-1].?[0-9]*)\s*$', + line) + if m == None: + raise Exception("Format incorrect. Should be: xmin,ymin,xmax,ymax,confidence") + elif withTranscription: + m = re.match(r'^\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*,(.*)$', line) + if m == None: + raise Exception("Format incorrect. Should be: xmin,ymin,xmax,ymax,transcription") + else: + m = re.match(r'^\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*,?\s*$', line) + if m == None: + raise Exception("Format incorrect. Should be: xmin,ymin,xmax,ymax") + + xmin = int(m.group(1)) + ymin = int(m.group(2)) + xmax = int(m.group(3)) + ymax = int(m.group(4)) + if (xmax < xmin): + raise Exception("Xmax value (%s) not valid (Xmax < Xmin)." % (xmax)) + if (ymax < ymin): + raise Exception("Ymax value (%s) not valid (Ymax < Ymin)." % (ymax)) + + points = [float(m.group(i)) for i in range(1, (numPoints + 1))] + + if (imWidth > 0 and imHeight > 0): + validate_point_inside_bounds(xmin, ymin, imWidth, imHeight); + validate_point_inside_bounds(xmax, ymax, imWidth, imHeight); + + else: + + numPoints = 8; + + if withTranscription and withConfidence: + m = re.match( + r'^\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*([0-1].?[0-9]*)\s*,(.*)$', + line) + if m == None: + raise Exception("Format incorrect. Should be: x1,y1,x2,y2,x3,y3,x4,y4,confidence,transcription") + elif withConfidence: + m = re.match( + r'^\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*([0-1].?[0-9]*)\s*$', + line) + if m == None: + raise Exception("Format incorrect. Should be: x1,y1,x2,y2,x3,y3,x4,y4,confidence") + elif withTranscription: + m = re.match( + r'^\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,(.*)$', + line) + if m == None: + raise Exception("Format incorrect. Should be: x1,y1,x2,y2,x3,y3,x4,y4,transcription") + else: + m = re.match( + r'^\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*$', + line) + if m == None: + raise Exception("Format incorrect. Should be: x1,y1,x2,y2,x3,y3,x4,y4") + + points = [float(m.group(i)) for i in range(1, (numPoints + 1))] + + validate_clockwise_points(points) + + if (imWidth > 0 and imHeight > 0): + validate_point_inside_bounds(points[0], points[1], imWidth, imHeight); + validate_point_inside_bounds(points[2], points[3], imWidth, imHeight); + validate_point_inside_bounds(points[4], points[5], imWidth, imHeight); + validate_point_inside_bounds(points[6], points[7], imWidth, imHeight); + + if withConfidence: + try: + confidence = float(m.group(numPoints + 1)) + except ValueError: + raise Exception("Confidence value must be a float") + + if withTranscription: + posTranscription = numPoints + (2 if withConfidence else 1) + transcription = m.group(posTranscription) + m2 = re.match(r'^\s*\"(.*)\"\s*$', transcription) + if m2 != None: # Transcription with double quotes, we extract the value and replace escaped characters + transcription = m2.group(1).replace("\\\\", "\\").replace("\\\"", "\"") + + return points, confidence, transcription + + +def validate_point_inside_bounds(x, y, imWidth, imHeight): + if (x < 0 or x > imWidth): + raise Exception("X value (%s) not valid. Image dimensions: (%s,%s)" % (xmin, imWidth, imHeight)) + if (y < 0 or y > imHeight): + raise Exception( + "Y value (%s) not valid. Image dimensions: (%s,%s) Sample: %s Line:%s" % (ymin, imWidth, imHeight)) + + +def validate_clockwise_points(points): + """ + Validates that the points that the 4 points that dlimite a polygon are in clockwise order. + """ + + if len(points) != 8: + raise Exception("Points list not valid." + str(len(points))) + + point = [ + [int(points[0]), int(points[1])], + [int(points[2]), int(points[3])], + [int(points[4]), int(points[5])], + [int(points[6]), int(points[7])] + ] + edge = [ + (point[1][0] - point[0][0]) * (point[1][1] + point[0][1]), + (point[2][0] - point[1][0]) * (point[2][1] + point[1][1]), + (point[3][0] - point[2][0]) * (point[3][1] + point[2][1]), + (point[0][0] - point[3][0]) * (point[0][1] + point[3][1]) + ] + + summatory = edge[0] + edge[1] + edge[2] + edge[3]; + if summatory > 0: + raise Exception( + "Points are not clockwise. The coordinates of bounding quadrilaterals have to be given in clockwise order. Regarding the correct interpretation of 'clockwise' remember that the image coordinate system used is the standard one, with the image origin at the upper left, the X axis extending to the right and Y axis extending downwards.") + + +def get_tl_line_values_from_file_contents(content, CRLF=True, LTRB=True, withTranscription=False, withConfidence=False, + imWidth=0, imHeight=0, sort_by_confidences=True): + """ + Returns all points, confindences and transcriptions of a file in lists. Valid line formats: + xmin,ymin,xmax,ymax,[confidence],[transcription] + x1,y1,x2,y2,x3,y3,x4,y4,[confidence],[transcription] + """ + pointsList = [] + transcriptionsList = [] + confidencesList = [] + + lines = content.split("\r\n" if CRLF else "\n") + for line in lines: + line = line.replace("\r", "").replace("\n", "") + if (line != ""): + points, confidence, transcription = get_tl_line_values(line, LTRB, withTranscription, withConfidence, + imWidth, imHeight); + pointsList.append(points) + transcriptionsList.append(transcription) + confidencesList.append(confidence) + + if withConfidence and len(confidencesList) > 0 and sort_by_confidences: + import numpy as np + sorted_ind = np.argsort(-np.array(confidencesList)) + confidencesList = [confidencesList[i] for i in sorted_ind] + pointsList = [pointsList[i] for i in sorted_ind] + transcriptionsList = [transcriptionsList[i] for i in sorted_ind] + + return pointsList, confidencesList, transcriptionsList + + +def main_evaluation(p, default_evaluation_params_fn, validate_data_fn, evaluate_method_fn, show_result=True, + per_sample=True): + """ + This process validates a method, evaluates it and if it succed generates a ZIP file with a JSON entry for each sample. + Params: + p: Dictionary of parmeters with the GT/submission locations. If None is passed, the parameters send by the system are used. + default_evaluation_params_fn: points to a function that returns a dictionary with the default parameters used for the evaluation + validate_data_fn: points to a method that validates the corrct format of the submission + evaluate_method_fn: points to a function that evaluated the submission and return a Dictionary with the results + """ + + if (p == None): + p = dict([s[1:].split('=') for s in sys.argv[1:]]) + if (len(sys.argv) < 3): + print_help() + + evalParams = default_evaluation_params_fn() + if 'p' in p.keys(): + evalParams.update(p['p'] if isinstance(p['p'], dict) else json.loads(p['p'][1:-1])) + + resDict = {'calculated': True, 'Message': '', 'method': '{}', 'per_sample': '{}'} + try: + validate_data_fn(p['g'], p['s'], evalParams) + evalData = evaluate_method_fn(p['g'], p['s'], evalParams) + resDict.update(evalData) + + except Exception, e: + resDict['Message'] = str(e) + resDict['calculated'] = False + + if 'o' in p: + if not os.path.exists(p['o']): + os.makedirs(p['o']) + + resultsOutputname = p['o'] + '/results.zip' + outZip = zipfile.ZipFile(resultsOutputname, mode='w', allowZip64=True) + + del resDict['per_sample'] + if 'output_items' in resDict.keys(): + del resDict['output_items'] + + outZip.writestr('method.json', json.dumps(resDict)) + + if not resDict['calculated']: + if show_result: + sys.stderr.write('Error!\n' + resDict['Message'] + '\n\n') + if 'o' in p: + outZip.close() + return resDict + + if 'o' in p: + if per_sample == True: + for k, v in evalData['per_sample'].iteritems(): + outZip.writestr(k + '.json', json.dumps(v)) + + if 'output_items' in evalData.keys(): + for k, v in evalData['output_items'].iteritems(): + outZip.writestr(k, v) + + outZip.close() + + if show_result: + sys.stdout.write("Calculated!") + sys.stdout.write(json.dumps(resDict['method'])) + + return resDict + + +def main_validation(default_evaluation_params_fn, validate_data_fn): + """ + This process validates a method + Params: + default_evaluation_params_fn: points to a function that returns a dictionary with the default parameters used for the evaluation + validate_data_fn: points to a method that validates the corrct format of the submission + """ + try: + p = dict([s[1:].split('=') for s in sys.argv[1:]]) + evalParams = default_evaluation_params_fn() + if 'p' in p.keys(): + evalParams.update(p['p'] if isinstance(p['p'], dict) else json.loads(p['p'][1:-1])) + + validate_data_fn(p['g'], p['s'], evalParams) + print 'SUCCESS' + sys.exit(0) + except Exception as e: + print str(e) + sys.exit(101) diff --git a/evaluation_script/script.py b/evaluation_script/script.py new file mode 100644 index 0000000..250f6da --- /dev/null +++ b/evaluation_script/script.py @@ -0,0 +1,333 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +from collections import namedtuple +import rrc_evaluation_funcs +import importlib + + +def evaluation_imports(): + """ + evaluation_imports: Dictionary ( key = module name , value = alias ) with python modules used in the evaluation. + """ + return { + 'Polygon': 'plg', + 'numpy': 'np' + } + + +def default_evaluation_params(): + """ + default_evaluation_params: Default parameters to use for the validation and evaluation. + """ + return { + 'IOU_CONSTRAINT': 0.5, + 'AREA_PRECISION_CONSTRAINT': 0.5, + 'GT_SAMPLE_NAME_2_ID': r'gt_(.*)\.txt', + 'DET_SAMPLE_NAME_2_ID': r'res_(.*)\.txt', + 'LTRB': False, # LTRB:2points(left,top,right,bottom) or 4 points(x1,y1,x2,y2,x3,y3,x4,y4) + 'CRLF': False, # Lines are delimited by Windows CRLF format + 'CONFIDENCES': False, # Detections must include confidence value. AP will be calculated + 'PER_SAMPLE_RESULTS': True # Generate per sample results and produce data for visualization + } + + +def validate_data(gtFilePath, submFilePath, evaluationParams): + """ + Method validate_data: validates that all files in the results folder are correct (have the correct name contents). + Validates also that there are no missing files in the folder. + If some error detected, the method raises the error + """ + gt = rrc_evaluation_funcs.load_zip_file(gtFilePath, evaluationParams['GT_SAMPLE_NAME_2_ID']) + + subm = rrc_evaluation_funcs.load_zip_file(submFilePath, evaluationParams['DET_SAMPLE_NAME_2_ID'], True) + + # Validate format of GroundTruth + for k in gt: + rrc_evaluation_funcs.validate_lines_in_file(k, gt[k], evaluationParams['CRLF'], evaluationParams['LTRB'], True) + + # Validate format of results + for k in subm: + if (k in gt) == False: + raise Exception("The sample %s not present in GT" % k) + + rrc_evaluation_funcs.validate_lines_in_file(k, subm[k], evaluationParams['CRLF'], evaluationParams['LTRB'], + False, evaluationParams['CONFIDENCES']) + + +def evaluate_method(gtFilePath, submFilePath, evaluationParams): + """ + Method evaluate_method: evaluate method and returns the results + Results. Dictionary with the following values: + - method (required) Global method metrics. Ex: { 'Precision':0.8,'Recall':0.9 } + - samples (optional) Per sample metrics. Ex: {'sample1' : { 'Precision':0.8,'Recall':0.9 } , 'sample2' : { 'Precision':0.8,'Recall':0.9 } + """ + + for module, alias in evaluation_imports().iteritems(): + globals()[alias] = importlib.import_module(module) + + def polygon_from_points(points): + """ + Returns a Polygon object to use with the Polygon2 class from a list of 8 points: x1,y1,x2,y2,x3,y3,x4,y4 + """ + resBoxes = np.empty([1, 8], dtype='int32') + resBoxes[0, 0] = int(points[0]) + resBoxes[0, 4] = int(points[1]) + resBoxes[0, 1] = int(points[2]) + resBoxes[0, 5] = int(points[3]) + resBoxes[0, 2] = int(points[4]) + resBoxes[0, 6] = int(points[5]) + resBoxes[0, 3] = int(points[6]) + resBoxes[0, 7] = int(points[7]) + pointMat = resBoxes[0].reshape([2, 4]).T + return plg.Polygon(pointMat) + + def rectangle_to_polygon(rect): + resBoxes = np.empty([1, 8], dtype='int32') + resBoxes[0, 0] = int(rect.xmin) + resBoxes[0, 4] = int(rect.ymax) + resBoxes[0, 1] = int(rect.xmin) + resBoxes[0, 5] = int(rect.ymin) + resBoxes[0, 2] = int(rect.xmax) + resBoxes[0, 6] = int(rect.ymin) + resBoxes[0, 3] = int(rect.xmax) + resBoxes[0, 7] = int(rect.ymax) + + pointMat = resBoxes[0].reshape([2, 4]).T + + return plg.Polygon(pointMat) + + def rectangle_to_points(rect): + points = [int(rect.xmin), int(rect.ymax), int(rect.xmax), int(rect.ymax), int(rect.xmax), int(rect.ymin), + int(rect.xmin), int(rect.ymin)] + return points + + def get_union(pD, pG): + areaA = pD.area(); + areaB = pG.area(); + return areaA + areaB - get_intersection(pD, pG); + + def get_intersection_over_union(pD, pG): + try: + return get_intersection(pD, pG) / get_union(pD, pG); + except: + return 0 + + def get_intersection(pD, pG): + pInt = pD & pG + if len(pInt) == 0: + return 0 + return pInt.area() + + def compute_ap(confList, matchList, numGtCare): + correct = 0 + AP = 0 + if len(confList) > 0: + confList = np.array(confList) + matchList = np.array(matchList) + sorted_ind = np.argsort(-confList) + confList = confList[sorted_ind] + matchList = matchList[sorted_ind] + for n in range(len(confList)): + match = matchList[n] + if match: + correct += 1 + AP += float(correct) / (n + 1) + + if numGtCare > 0: + AP /= numGtCare + + return AP + + perSampleMetrics = {} + + matchedSum = 0 + + Rectangle = namedtuple('Rectangle', 'xmin ymin xmax ymax') + + gt = rrc_evaluation_funcs.load_zip_file(gtFilePath, evaluationParams['GT_SAMPLE_NAME_2_ID']) + subm = rrc_evaluation_funcs.load_zip_file(submFilePath, evaluationParams['DET_SAMPLE_NAME_2_ID'], True) + + numGlobalCareGt = 0; + numGlobalCareDet = 0; + + arrGlobalConfidences = []; + arrGlobalMatches = []; + + for resFile in gt: + + gtFile = rrc_evaluation_funcs.decode_utf8(gt[resFile]) + recall = 0 + precision = 0 + hmean = 0 + + detMatched = 0 + + iouMat = np.empty([1, 1]) + + gtPols = [] + detPols = [] + + gtPolPoints = [] + detPolPoints = [] + + # Array of Ground Truth Polygons' keys marked as don't Care + gtDontCarePolsNum = [] + # Array of Detected Polygons' matched with a don't Care GT + detDontCarePolsNum = [] + + pairs = [] + detMatchedNums = [] + + arrSampleConfidences = []; + arrSampleMatch = []; + sampleAP = 0; + + evaluationLog = "" + + pointsList, _, transcriptionsList = rrc_evaluation_funcs.get_tl_line_values_from_file_contents(gtFile, + evaluationParams[ + 'CRLF'], + evaluationParams[ + 'LTRB'], + True, False) + for n in range(len(pointsList)): + points = pointsList[n] + transcription = transcriptionsList[n] + dontCare = transcription == "###" + if evaluationParams['LTRB']: + gtRect = Rectangle(*points) + gtPol = rectangle_to_polygon(gtRect) + else: + gtPol = polygon_from_points(points) + gtPols.append(gtPol) + gtPolPoints.append(points) + if dontCare: + gtDontCarePolsNum.append(len(gtPols) - 1) + + evaluationLog += "GT polygons: " + str(len(gtPols)) + ( + " (" + str(len(gtDontCarePolsNum)) + " don't care)\n" if len(gtDontCarePolsNum) > 0 else "\n") + + if resFile in subm: + + detFile = rrc_evaluation_funcs.decode_utf8(subm[resFile]) + + pointsList, confidencesList, _ = rrc_evaluation_funcs.get_tl_line_values_from_file_contents(detFile, + evaluationParams[ + 'CRLF'], + evaluationParams[ + 'LTRB'], + False, + evaluationParams[ + 'CONFIDENCES']) + for n in range(len(pointsList)): + points = pointsList[n] + + if evaluationParams['LTRB']: + detRect = Rectangle(*points) + detPol = rectangle_to_polygon(detRect) + else: + detPol = polygon_from_points(points) + detPols.append(detPol) + detPolPoints.append(points) + if len(gtDontCarePolsNum) > 0: + for dontCarePol in gtDontCarePolsNum: + dontCarePol = gtPols[dontCarePol] + intersected_area = get_intersection(dontCarePol, detPol) + pdDimensions = detPol.area() + precision = 0 if pdDimensions == 0 else intersected_area / pdDimensions + if (precision > evaluationParams['AREA_PRECISION_CONSTRAINT']): + detDontCarePolsNum.append(len(detPols) - 1) + break + + evaluationLog += "DET polygons: " + str(len(detPols)) + ( + " (" + str(len(detDontCarePolsNum)) + " don't care)\n" if len(detDontCarePolsNum) > 0 else "\n") + + if len(gtPols) > 0 and len(detPols) > 0: + # Calculate IoU and precision matrixs + outputShape = [len(gtPols), len(detPols)] + iouMat = np.empty(outputShape) + gtRectMat = np.zeros(len(gtPols), np.int8) + detRectMat = np.zeros(len(detPols), np.int8) + for gtNum in range(len(gtPols)): + for detNum in range(len(detPols)): + pG = gtPols[gtNum] + pD = detPols[detNum] + iouMat[gtNum, detNum] = get_intersection_over_union(pD, pG) + + for gtNum in range(len(gtPols)): + for detNum in range(len(detPols)): + if gtRectMat[gtNum] == 0 and detRectMat[ + detNum] == 0 and gtNum not in gtDontCarePolsNum and detNum not in detDontCarePolsNum: + if iouMat[gtNum, detNum] > evaluationParams['IOU_CONSTRAINT']: + gtRectMat[gtNum] = 1 + detRectMat[detNum] = 1 + detMatched += 1 + pairs.append({'gt': gtNum, 'det': detNum}) + detMatchedNums.append(detNum) + evaluationLog += "Match GT #" + str(gtNum) + " with Det #" + str(detNum) + "\n" + + if evaluationParams['CONFIDENCES']: + for detNum in range(len(detPols)): + if detNum not in detDontCarePolsNum: + # we exclude the don't care detections + match = detNum in detMatchedNums + + arrSampleConfidences.append(confidencesList[detNum]) + arrSampleMatch.append(match) + + arrGlobalConfidences.append(confidencesList[detNum]); + arrGlobalMatches.append(match); + + numGtCare = (len(gtPols) - len(gtDontCarePolsNum)) + numDetCare = (len(detPols) - len(detDontCarePolsNum)) + if numGtCare == 0: + recall = float(1) + precision = float(0) if numDetCare > 0 else float(1) + sampleAP = precision + else: + recall = float(detMatched) / numGtCare + precision = 0 if numDetCare == 0 else float(detMatched) / numDetCare + if evaluationParams['CONFIDENCES'] and evaluationParams['PER_SAMPLE_RESULTS']: + sampleAP = compute_ap(arrSampleConfidences, arrSampleMatch, numGtCare) + + hmean = 0 if (precision + recall) == 0 else 2.0 * precision * recall / (precision + recall) + + matchedSum += detMatched + numGlobalCareGt += numGtCare + numGlobalCareDet += numDetCare + + if evaluationParams['PER_SAMPLE_RESULTS']: + perSampleMetrics[resFile] = { + 'precision': precision, + 'recall': recall, + 'hmean': hmean, + 'pairs': pairs, + 'AP': sampleAP, + 'iouMat': [] if len(detPols) > 100 else iouMat.tolist(), + 'gtPolPoints': gtPolPoints, + 'detPolPoints': detPolPoints, + 'gtDontCare': gtDontCarePolsNum, + 'detDontCare': detDontCarePolsNum, + 'evaluationParams': evaluationParams, + 'evaluationLog': evaluationLog + } + + # Compute MAP and MAR + AP = 0 + if evaluationParams['CONFIDENCES']: + AP = compute_ap(arrGlobalConfidences, arrGlobalMatches, numGlobalCareGt) + + methodRecall = 0 if numGlobalCareGt == 0 else float(matchedSum) / numGlobalCareGt + methodPrecision = 0 if numGlobalCareDet == 0 else float(matchedSum) / numGlobalCareDet + methodHmean = 0 if methodRecall + methodPrecision == 0 else 2 * methodRecall * methodPrecision / ( + methodRecall + methodPrecision) + + methodMetrics = {'precision': methodPrecision, 'recall': methodRecall, 'hmean': methodHmean, 'AP': AP} + + resDict = {'calculated': True, 'Message': '', 'method': methodMetrics, 'per_sample': perSampleMetrics} + + return resDict; + + +if __name__ == '__main__': + rrc_evaluation_funcs.main_evaluation(None, default_evaluation_params, validate_data, evaluate_method) diff --git a/nets/mobilenet/README.md b/nets/mobilenet/README.md new file mode 100644 index 0000000..8b093f9 --- /dev/null +++ b/nets/mobilenet/README.md @@ -0,0 +1,138 @@ +# MobilenNet + +This folder contains building code for +[MobileNetV2](https://arxiv.org/abs/1801.04381) and +[MobilenetV3](https://arxiv.org/abs/1905.02244) networks. The architectural +definition for each model is located in [mobilenet_v2.py](mobilenet_v2.py) and +[mobilenet_v3.py](mobilenet_v3.py) respectively. + +For MobilenetV1 please refer to this [page](../mobilenet_v1.md) + +## Performance + +### Mobilenet V3 latency + +This is the timing of [MobileNetV2] vs [MobileNetV3] using TF-Lite on the large +core of Pixel 1 phone. + +![Mobilenet V2 and V3 Latency for Pixel 1.png](g3doc/latency_pixel1.png) + +### MACs + +MACs, also sometimes known as MADDs - the number of multiply-accumulates needed +to compute an inference on a single image is a common metric to measure the +efficiency of the model. Full size Mobilenet V3 on image size 224 uses ~215 +Million MADDs (MMadds) while achieving accuracy 75.1%, while Mobilenet V2 uses +~300MMadds and achieving accuracy 72%. By comparison ResNet-50 uses +approximately 3500 MMAdds while achieving 76% accuracy. + +Below is the graph comparing Mobilenets and a few selected networks. The size of +each blob represents the number of parameters. Note for +[ShuffleNet](https://arxiv.org/abs/1707.01083) there are no published size +numbers. We estimate it to be comparable to MobileNetV2 numbers. + +![madds_top1_accuracy](g3doc/madds_top1_accuracy.png) + +## Pretrained models + +### Mobilenet V3 Imagenet Checkpoints + +All mobilenet V3 checkpoints were trained with image resolution 224x224. All +phone latencies are in milliseconds, measured on large core. In addition to +large and small models this page also contains so-called minimalistic models, +these models have the same per-layer dimensions characteristic as MobilenetV3 +however, they don't utilize any of the advanced blocks (squeeze-and-excite +units, hard-swish, and 5x5 convolutions). While these models are less efficient +on CPU, we find that they are much more performant on GPU/DSP/EdgeTpu. + +| Imagenet Checkpoint | MACs (M) | Params (M) | Top1 | Pixel 1 | Pixel 2 | Pixel 3 | +| ------------------ | -------- | ---------- | ---- | ------- | ------- | ------- | +| [Large dm=1 (float)] | 217 | 5.4 | 75.2 | 51.2 | 61 | 44 | +| [Large dm=1 (8-bit)] | 217 | 5.4 | 73.9 | 44 | 42.5 | 32 | +| [Large dm=0.75 (float)] | 155 | 4.0 | 73.3 | 39.8 | 48 | 34 | +| [Small dm=1 (float)] | 66 | 2.9 | 67.5 | 15.8 | 19.4 | 14.4 | +| [Small dm=1 (8-bit)] | 66 | 2.9 | 64.9 | 15.5 | 15 | 10.7 | +| [Small dm=0.75 (float)] | 44 | 2.4 | 65.4 | 12.8 | 15.9 | 11.6 | + +#### Minimalistic checkpoints: + +| Imagenet Checkpoint | MACs (M) | Params (M) | Top1 | Pixel 1 | Pixel 2 | Pixel 3 | +| -------------- | -------- | ---------- | ---- | ------- | ------- | ------- | +| [Large minimalistic (float)] | 209 | 3.9 | 72.3 | 44.1 | 51 | 35 | +| [Large minimalistic (8-bit)][lm8] | 209 | 3.9 | 71.3 | 37 | 35 | 27 | +| [Small minimalistic (float)] | 65 | 2.0 | 61.9 | 12.2 | 15.1 | 11 | + + +[Small minimalistic (float)]: https://storage.googleapis.com/mobilenet_v3/checkpoints/v3-small-minimalistic_224_1.0_float.tgz +[Large minimalistic (float)]: https://storage.googleapis.com/mobilenet_v3/checkpoints/v3-large-minimalistic_224_1.0_float.tgz +[lm8]: https://storage.googleapis.com/mobilenet_v3/checkpoints/v3-large-minimalistic_224_1.0_uint8.tgz +[Large dm=1 (float)]: https://storage.googleapis.com/mobilenet_v3/checkpoints/v3-large_224_1.0_float.tgz +[Small dm=1 (float)]: https://storage.googleapis.com/mobilenet_v3/checkpoints/v3-small_224_1.0_float.tgz +[Large dm=1 (8-bit)]: https://storage.googleapis.com/mobilenet_v3/checkpoints/v3-large_224_1.0_uint8.tgz +[Small dm=1 (8-bit)]: https://storage.googleapis.com/mobilenet_v3/checkpoints/v3-small_224_1.0_uint8.tgz +[Large dm=0.75 (float)]: https://storage.googleapis.com/mobilenet_v3/checkpoints/v3-large_224_0.75_float.tgz +[Small dm=0.75 (float)]: https://storage.googleapis.com/mobilenet_v3/checkpoints/v3-small_224_0.75_float.tgz + +### Mobilenet V2 Imagenet Checkpoints + +Classification Checkpoint | MACs (M) | Parameters (M) | Top 1 Accuracy | Top 5 Accuracy | Mobile CPU (ms) Pixel 1 +---------------------------------------------------------------------------------------------------------- | -------- | -------------- | -------------- | -------------- | ----------------------- +[mobilenet_v2_1.4_224](https://storage.googleapis.com/mobilenet_v2/checkpoints/mobilenet_v2_1.4_224.tgz) | 582 | 6.06 | 75.0 | 92.5 | 138.0 +[mobilenet_v2_1.3_224](https://storage.googleapis.com/mobilenet_v2/checkpoints/mobilenet_v2_1.3_224.tgz) | 509 | 5.34 | 74.4 | 92.1 | 123.0 +[mobilenet_v2_1.0_224](https://storage.googleapis.com/mobilenet_v2/checkpoints/mobilenet_v2_1.0_224.tgz) | 300 | 3.47 | 71.8 | 91.0 | 73.8 +[mobilenet_v2_1.0_192](https://storage.googleapis.com/mobilenet_v2/checkpoints/mobilenet_v2_1.0_192.tgz) | 221 | 3.47 | 70.7 | 90.1 | 55.1 +[mobilenet_v2_1.0_160](https://storage.googleapis.com/mobilenet_v2/checkpoints/mobilenet_v2_1.0_160.tgz) | 154 | 3.47 | 68.8 | 89.0 | 40.2 +[mobilenet_v2_1.0_128](https://storage.googleapis.com/mobilenet_v2/checkpoints/mobilenet_v2_1.0_128.tgz) | 99 | 3.47 | 65.3 | 86.9 | 27.6 +[mobilenet_v2_1.0_96](https://storage.googleapis.com/mobilenet_v2/checkpoints/mobilenet_v2_1.0_96.tgz) | 56 | 3.47 | 60.3 | 83.2 | 17.6 +[mobilenet_v2_0.75_224](https://storage.googleapis.com/mobilenet_v2/checkpoints/mobilenet_v2_0.75_224.tgz) | 209 | 2.61 | 69.8 | 89.6 | 55.8 +[mobilenet_v2_0.75_192](https://storage.googleapis.com/mobilenet_v2/checkpoints/mobilenet_v2_0.75_192.tgz) | 153 | 2.61 | 68.7 | 88.9 | 41.6 +[mobilenet_v2_0.75_160](https://storage.googleapis.com/mobilenet_v2/checkpoints/mobilenet_v2_0.75_160.tgz) | 107 | 2.61 | 66.4 | 87.3 | 30.4 +[mobilenet_v2_0.75_128](https://storage.googleapis.com/mobilenet_v2/checkpoints/mobilenet_v2_0.75_128.tgz) | 69 | 2.61 | 63.2 | 85.3 | 21.9 +[mobilenet_v2_0.75_96](https://storage.googleapis.com/mobilenet_v2/checkpoints/mobilenet_v2_0.75_96.tgz) | 39 | 2.61 | 58.8 | 81.6 | 14.2 +[mobilenet_v2_0.5_224](https://storage.googleapis.com/mobilenet_v2/checkpoints/mobilenet_v2_0.5_224.tgz) | 97 | 1.95 | 65.4 | 86.4 | 28.7 +[mobilenet_v2_0.5_192](https://storage.googleapis.com/mobilenet_v2/checkpoints/mobilenet_v2_0.5_192.tgz) | 71 | 1.95 | 63.9 | 85.4 | 21.1 +[mobilenet_v2_0.5_160](https://storage.googleapis.com/mobilenet_v2/checkpoints/mobilenet_v2_0.5_160.tgz) | 50 | 1.95 | 61.0 | 83.2 | 14.9 +[mobilenet_v2_0.5_128](https://storage.googleapis.com/mobilenet_v2/checkpoints/mobilenet_v2_0.5_128.tgz) | 32 | 1.95 | 57.7 | 80.8 | 9.9 +[mobilenet_v2_0.5_96](https://storage.googleapis.com/mobilenet_v2/checkpoints/mobilenet_v2_0.5_96.tgz) | 18 | 1.95 | 51.2 | 75.8 | 6.4 +[mobilenet_v2_0.35_224](https://storage.googleapis.com/mobilenet_v2/checkpoints/mobilenet_v2_0.35_224.tgz) | 59 | 1.66 | 60.3 | 82.9 | 19.7 +[mobilenet_v2_0.35_192](https://storage.googleapis.com/mobilenet_v2/checkpoints/mobilenet_v2_0.35_192.tgz) | 43 | 1.66 | 58.2 | 81.2 | 14.6 +[mobilenet_v2_0.35_160](https://storage.googleapis.com/mobilenet_v2/checkpoints/mobilenet_v2_0.35_160.tgz) | 30 | 1.66 | 55.7 | 79.1 | 10.5 +[mobilenet_v2_0.35_128](https://storage.googleapis.com/mobilenet_v2/checkpoints/mobilenet_v2_0.35_128.tgz) | 20 | 1.66 | 50.8 | 75.0 | 6.9 +[mobilenet_v2_0.35_96](https://storage.googleapis.com/mobilenet_v2/checkpoints/mobilenet_v2_0.35_96.tgz) | 11 | 1.66 | 45.5 | 70.4 | 4.5 + +## Training + +### V3 + +TODO: Add V3 hyperparameters + +### V2 + +The numbers above can be reproduced using slim's +[`train_image_classifier`](https://github.com/tensorflow/models/blob/master/research/slim/README.md#training-a-model-from-scratch). +Below is the set of parameters that achieves 72.0% for full size MobileNetV2, +after about 700K when trained on 8 GPU. If trained on a single GPU the full +convergence is after 5.5M steps. Also note that learning rate and +num_epochs_per_decay both need to be adjusted depending on how many GPUs are +being used due to slim's internal averaging. + +```bash +--model_name="mobilenet_v2" +--learning_rate=0.045 * NUM_GPUS #slim internally averages clones so we compensate +--preprocessing_name="inception_v2" +--label_smoothing=0.1 +--moving_average_decay=0.9999 +--batch_size= 96 +--num_clones = NUM_GPUS # you can use any number here between 1 and 8 depending on your hardware setup. +--learning_rate_decay_factor=0.98 +--num_epochs_per_decay = 2.5 / NUM_GPUS # train_image_classifier does per clone epochs +``` + +# Example + +See this [ipython notebook](mobilenet_example.ipynb) or open and run the network +directly in +[Colaboratory](https://colab.research.google.com/github/tensorflow/models/blob/master/research/slim/nets/mobilenet/mobilenet_example.ipynb). + +[MobilenetV2]: https://arxiv.org/abs/1801.04381 +[MobilenetV3]: https://arxiv.org/abs/1905.02244 diff --git a/nets/mobilenet/__init__.py b/nets/mobilenet/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/nets/mobilenet/conv_blocks.py b/nets/mobilenet/conv_blocks.py new file mode 100644 index 0000000..d4d431e --- /dev/null +++ b/nets/mobilenet/conv_blocks.py @@ -0,0 +1,466 @@ +# Copyright 2018 The TensorFlow Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""Convolution blocks for mobilenet.""" +import contextlib +import functools + +import tensorflow as tf + +slim = tf.contrib.slim + + +def _fixed_padding(inputs, kernel_size, rate=1): + """Pads the input along the spatial dimensions independently of input size. + + Pads the input such that if it was used in a convolution with 'VALID' padding, + the output would have the same dimensions as if the unpadded input was used + in a convolution with 'SAME' padding. + + Args: + inputs: A tensor of size [batch, height_in, width_in, channels]. + kernel_size: The kernel to be used in the conv2d or max_pool2d operation. + rate: An integer, rate for atrous convolution. + + Returns: + output: A tensor of size [batch, height_out, width_out, channels] with the + input, either intact (if kernel_size == 1) or padded (if kernel_size > 1). + """ + kernel_size_effective = [kernel_size[0] + (kernel_size[0] - 1) * (rate - 1), + kernel_size[0] + (kernel_size[0] - 1) * (rate - 1)] + pad_total = [kernel_size_effective[0] - 1, kernel_size_effective[1] - 1] + pad_beg = [pad_total[0] // 2, pad_total[1] // 2] + pad_end = [pad_total[0] - pad_beg[0], pad_total[1] - pad_beg[1]] + padded_inputs = tf.pad(inputs, [[0, 0], [pad_beg[0], pad_end[0]], + [pad_beg[1], pad_end[1]], [0, 0]]) + return padded_inputs + + +def _make_divisible(v, divisor, min_value=None): + if min_value is None: + min_value = divisor + new_v = max(min_value, int(v + divisor / 2) // divisor * divisor) + # Make sure that round down does not go down by more than 10%. + if new_v < 0.9 * v: + new_v += divisor + return new_v + + +def _split_divisible(num, num_ways, divisible_by=8): + """Evenly splits num, num_ways so each piece is a multiple of divisible_by.""" + assert num % divisible_by == 0 + assert num / num_ways >= divisible_by + # Note: want to round down, we adjust each split to match the total. + base = num // num_ways // divisible_by * divisible_by + result = [] + accumulated = 0 + for i in range(num_ways): + r = base + while accumulated + r < num * (i + 1) / num_ways: + r += divisible_by + result.append(r) + accumulated += r + assert accumulated == num + return result + + +@contextlib.contextmanager +def _v1_compatible_scope_naming(scope): + if scope is None: # Create uniqified separable blocks. + with tf.variable_scope(None, default_name='separable') as s, \ + tf.name_scope(s.original_name_scope): + yield '' + else: + # We use scope_depthwise, scope_pointwise for compatibility with V1 ckpts. + # which provide numbered scopes. + scope += '_' + yield scope + + +@slim.add_arg_scope +def split_separable_conv2d(input_tensor, + num_outputs, + scope=None, + normalizer_fn=None, + stride=1, + rate=1, + endpoints=None, + use_explicit_padding=False): + """Separable mobilenet V1 style convolution. + + Depthwise convolution, with default non-linearity, + followed by 1x1 depthwise convolution. This is similar to + slim.separable_conv2d, but differs in tha it applies batch + normalization and non-linearity to depthwise. This matches + the basic building of Mobilenet Paper + (https://arxiv.org/abs/1704.04861) + + Args: + input_tensor: input + num_outputs: number of outputs + scope: optional name of the scope. Note if provided it will use + scope_depthwise for deptwhise, and scope_pointwise for pointwise. + normalizer_fn: which normalizer function to use for depthwise/pointwise + stride: stride + rate: output rate (also known as dilation rate) + endpoints: optional, if provided, will export additional tensors to it. + use_explicit_padding: Use 'VALID' padding for convolutions, but prepad + inputs so that the output dimensions are the same as if 'SAME' padding + were used. + + Returns: + output tesnor + """ + + with _v1_compatible_scope_naming(scope) as scope: + dw_scope = scope + 'depthwise' + endpoints = endpoints if endpoints is not None else {} + kernel_size = [3, 3] + padding = 'SAME' + if use_explicit_padding: + padding = 'VALID' + input_tensor = _fixed_padding(input_tensor, kernel_size, rate) + net = slim.separable_conv2d( + input_tensor, + None, + kernel_size, + depth_multiplier=1, + stride=stride, + rate=rate, + normalizer_fn=normalizer_fn, + padding=padding, + scope=dw_scope) + + endpoints[dw_scope] = net + + pw_scope = scope + 'pointwise' + net = slim.conv2d( + net, + num_outputs, [1, 1], + stride=1, + normalizer_fn=normalizer_fn, + scope=pw_scope) + endpoints[pw_scope] = net + return net + + +def expand_input_by_factor(n, divisible_by=8): + return lambda num_inputs, **_: _make_divisible(num_inputs * n, divisible_by) + + +def split_conv(input_tensor, + num_outputs, + num_ways, + scope, + divisible_by=8, + **kwargs): + """Creates a split convolution. + + Split convolution splits the input and output into + 'num_blocks' blocks of approximately the same size each, + and only connects $i$-th input to $i$ output. + + Args: + input_tensor: input tensor + num_outputs: number of output filters + num_ways: num blocks to split by. + scope: scope for all the operators. + divisible_by: make sure that every part is divisiable by this. + **kwargs: will be passed directly into conv2d operator + Returns: + tensor + """ + b = input_tensor.get_shape().as_list()[3] + + if num_ways == 1 or min(b // num_ways, + num_outputs // num_ways) < divisible_by: + # Don't do any splitting if we end up with less than 8 filters + # on either side. + return slim.conv2d(input_tensor, num_outputs, [1, 1], scope=scope, **kwargs) + + outs = [] + input_splits = _split_divisible(b, num_ways, divisible_by=divisible_by) + output_splits = _split_divisible( + num_outputs, num_ways, divisible_by=divisible_by) + inputs = tf.split(input_tensor, input_splits, axis=3, name='split_' + scope) + base = scope + for i, (input_tensor, out_size) in enumerate(zip(inputs, output_splits)): + scope = base + '_part_%d' % (i,) + n = slim.conv2d(input_tensor, out_size, [1, 1], scope=scope, **kwargs) + n = tf.identity(n, scope + '_output') + outs.append(n) + return tf.concat(outs, 3, name=scope + '_concat') + + +@slim.add_arg_scope +def expanded_conv(input_tensor, + num_outputs, + expansion_size=expand_input_by_factor(6), + stride=1, + rate=1, + kernel_size=(3, 3), + residual=True, + normalizer_fn=None, + split_projection=1, + split_expansion=1, + split_divisible_by=8, + expansion_transform=None, + depthwise_location='expansion', + depthwise_channel_multiplier=1, + endpoints=None, + use_explicit_padding=False, + padding='SAME', + inner_activation_fn=None, + depthwise_activation_fn=None, + project_activation_fn=tf.identity, + depthwise_fn=slim.separable_conv2d, + expansion_fn=split_conv, + projection_fn=split_conv, + scope=None): + """Depthwise Convolution Block with expansion. + + Builds a composite convolution that has the following structure + expansion (1x1) -> depthwise (kernel_size) -> projection (1x1) + + Args: + input_tensor: input + num_outputs: number of outputs in the final layer. + expansion_size: the size of expansion, could be a constant or a callable. + If latter it will be provided 'num_inputs' as an input. For forward + compatibility it should accept arbitrary keyword arguments. + Default will expand the input by factor of 6. + stride: depthwise stride + rate: depthwise rate + kernel_size: depthwise kernel + residual: whether to include residual connection between input + and output. + normalizer_fn: batchnorm or otherwise + split_projection: how many ways to split projection operator + (that is conv expansion->bottleneck) + split_expansion: how many ways to split expansion op + (that is conv bottleneck->expansion) ops will keep depth divisible + by this value. + split_divisible_by: make sure every split group is divisible by this number. + expansion_transform: Optional function that takes expansion + as a single input and returns output. + depthwise_location: where to put depthwise covnvolutions supported + values None, 'input', 'output', 'expansion' + depthwise_channel_multiplier: depthwise channel multiplier: + each input will replicated (with different filters) + that many times. So if input had c channels, + output will have c x depthwise_channel_multpilier. + endpoints: An optional dictionary into which intermediate endpoints are + placed. The keys "expansion_output", "depthwise_output", + "projection_output" and "expansion_transform" are always populated, even + if the corresponding functions are not invoked. + use_explicit_padding: Use 'VALID' padding for convolutions, but prepad + inputs so that the output dimensions are the same as if 'SAME' padding + were used. + padding: Padding type to use if `use_explicit_padding` is not set. + inner_activation_fn: activation function to use in all inner convolutions. + If none, will rely on slim default scopes. + depthwise_activation_fn: activation function to use for deptwhise only. + If not provided will rely on slim default scopes. If both + inner_activation_fn and depthwise_activation_fn are provided, + depthwise_activation_fn takes precedence over inner_activation_fn. + project_activation_fn: activation function for the project layer. + (note this layer is not affected by inner_activation_fn) + depthwise_fn: Depthwise convolution function. + expansion_fn: Expansion convolution function. If use custom function then + "split_expansion" and "split_divisible_by" will be ignored. + projection_fn: Projection convolution function. If use custom function then + "split_projection" and "split_divisible_by" will be ignored. + + scope: optional scope. + + Returns: + Tensor of depth num_outputs + + Raises: + TypeError: on inval + """ + conv_defaults = {} + dw_defaults = {} + if inner_activation_fn is not None: + conv_defaults['activation_fn'] = inner_activation_fn + dw_defaults['activation_fn'] = inner_activation_fn + if depthwise_activation_fn is not None: + dw_defaults['activation_fn'] = depthwise_activation_fn + # pylint: disable=g-backslash-continuation + with tf.variable_scope(scope, default_name='expanded_conv') as s, \ + tf.name_scope(s.original_name_scope), \ + slim.arg_scope((slim.conv2d,), **conv_defaults), \ + slim.arg_scope((slim.separable_conv2d,), **dw_defaults): + prev_depth = input_tensor.get_shape().as_list()[3] + if depthwise_location not in [None, 'input', 'output', 'expansion']: + raise TypeError('%r is unknown value for depthwise_location' % + depthwise_location) + if use_explicit_padding: + if padding != 'SAME': + raise TypeError('`use_explicit_padding` should only be used with ' + '"SAME" padding.') + padding = 'VALID' + depthwise_func = functools.partial( + depthwise_fn, + num_outputs=None, + kernel_size=kernel_size, + depth_multiplier=depthwise_channel_multiplier, + stride=stride, + rate=rate, + normalizer_fn=normalizer_fn, + padding=padding, + scope='depthwise') + # b1 -> b2 * r -> b2 + # i -> (o * r) (bottleneck) -> o + input_tensor = tf.identity(input_tensor, 'input') + net = input_tensor + + if depthwise_location == 'input': + if use_explicit_padding: + net = _fixed_padding(net, kernel_size, rate) + net = depthwise_func(net, activation_fn=None) + net = tf.identity(net, name='depthwise_output') + if endpoints is not None: + endpoints['depthwise_output'] = net + + if callable(expansion_size): + inner_size = expansion_size(num_inputs=prev_depth) + else: + inner_size = expansion_size + + if inner_size > net.shape[3]: + if expansion_fn == split_conv: + expansion_fn = functools.partial( + expansion_fn, + num_ways=split_expansion, + divisible_by=split_divisible_by, + stride=1) + net = expansion_fn( + net, + inner_size, + scope='expand', + normalizer_fn=normalizer_fn) + net = tf.identity(net, 'expansion_output') + if endpoints is not None: + endpoints['expansion_output'] = net + + if depthwise_location == 'expansion': + if use_explicit_padding: + net = _fixed_padding(net, kernel_size, rate) + net = depthwise_func(net) + net = tf.identity(net, name='depthwise_output') + if endpoints is not None: + endpoints['depthwise_output'] = net + + if expansion_transform: + net = expansion_transform(expansion_tensor=net, input_tensor=input_tensor) + # Note in contrast with expansion, we always have + # projection to produce the desired output size. + if projection_fn == split_conv: + projection_fn = functools.partial( + projection_fn, + num_ways=split_projection, + divisible_by=split_divisible_by, + stride=1) + net = projection_fn( + net, + num_outputs, + scope='project', + normalizer_fn=normalizer_fn, + activation_fn=project_activation_fn) + if endpoints is not None: + endpoints['projection_output'] = net + if depthwise_location == 'output': + if use_explicit_padding: + net = _fixed_padding(net, kernel_size, rate) + net = depthwise_func(net, activation_fn=None) + net = tf.identity(net, name='depthwise_output') + if endpoints is not None: + endpoints['depthwise_output'] = net + + if callable(residual): # custom residual + net = residual(input_tensor=input_tensor, output_tensor=net) + elif (residual and + # stride check enforces that we don't add residuals when spatial + # dimensions are None + stride == 1 and + # Depth matches + net.get_shape().as_list()[3] == + input_tensor.get_shape().as_list()[3]): + net += input_tensor + return tf.identity(net, name='output') + + +@slim.add_arg_scope +def squeeze_excite(input_tensor, + divisible_by=8, + squeeze_factor=3, + inner_activation_fn=tf.nn.relu, + gating_fn=tf.sigmoid, + squeeze_input_tensor=None, + pool=None): + """Squeeze excite block for Mobilenet V3. + + Args: + input_tensor: input tensor to apply SE block to. + divisible_by: ensures all inner dimensions are divisible by this number. + squeeze_factor: the factor of squeezing in the inner fully connected layer + inner_activation_fn: non-linearity to be used in inner layer. + gating_fn: non-linearity to be used for final gating function + squeeze_input_tensor: custom tensor to use for computing gating activation. + If provided the result will be input_tensor * SE(squeeze_input_tensor) + instead of input_tensor * SE(input_tensor). + pool: if number is provided will average pool with that kernel size + to compute inner tensor, followed by bilinear upsampling. + + Returns: + Gated input_tensor. (e.g. X * SE(X)) + """ + with tf.variable_scope('squeeze_excite'): + if squeeze_input_tensor is None: + squeeze_input_tensor = input_tensor + input_size = input_tensor.shape.as_list()[1:3] + pool_height, pool_width = squeeze_input_tensor.shape.as_list()[1:3] + stride = 1 + if pool is not None and pool_height >= pool: + pool_height, pool_width, stride = pool, pool, pool + input_channels = squeeze_input_tensor.shape.as_list()[3] + output_channels = input_tensor.shape.as_list()[3] + squeeze_channels = _make_divisible( + input_channels / squeeze_factor, divisor=divisible_by) + + pooled = tf.nn.avg_pool(squeeze_input_tensor, + (1, pool_height, pool_width, 1), + strides=(1, stride, stride, 1), + padding='VALID') + squeeze = slim.conv2d( + pooled, + kernel_size=(1, 1), + num_outputs=squeeze_channels, + normalizer_fn=None, + activation_fn=inner_activation_fn) + excite_outputs = output_channels + excite = slim.conv2d(squeeze, num_outputs=excite_outputs, + kernel_size=[1, 1], + normalizer_fn=None, + activation_fn=gating_fn) + if pool is not None: + # Note: As of 03/20/2019 only BILINEAR (the default) with + # align_corners=True has gradients implemented in TPU. + excite = tf.image.resize_images( + excite, input_size, + align_corners=True) + result = input_tensor * excite + return result + diff --git a/nets/mobilenet/g3doc/latency_pixel1.png b/nets/mobilenet/g3doc/latency_pixel1.png new file mode 100644 index 0000000..4fda9cf Binary files /dev/null and b/nets/mobilenet/g3doc/latency_pixel1.png differ diff --git a/nets/mobilenet/g3doc/madds_top1_accuracy.png b/nets/mobilenet/g3doc/madds_top1_accuracy.png new file mode 100644 index 0000000..a2f75bd Binary files /dev/null and b/nets/mobilenet/g3doc/madds_top1_accuracy.png differ diff --git a/nets/mobilenet/mnet_v1_vs_v2_pixel1_latency.png b/nets/mobilenet/mnet_v1_vs_v2_pixel1_latency.png new file mode 100644 index 0000000..5238a1f Binary files /dev/null and b/nets/mobilenet/mnet_v1_vs_v2_pixel1_latency.png differ diff --git a/nets/mobilenet/mobilenet.py b/nets/mobilenet/mobilenet.py new file mode 100644 index 0000000..31e7184 --- /dev/null +++ b/nets/mobilenet/mobilenet.py @@ -0,0 +1,475 @@ +# Copyright 2018 The TensorFlow Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""Mobilenet Base Class.""" + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +import collections +import contextlib +import copy +import os + +import tensorflow as tf + + +slim = tf.contrib.slim + + +@slim.add_arg_scope +def apply_activation(x, name=None, activation_fn=None): + return activation_fn(x, name=name) if activation_fn else x + + +def _fixed_padding(inputs, kernel_size, rate=1): + """Pads the input along the spatial dimensions independently of input size. + + Pads the input such that if it was used in a convolution with 'VALID' padding, + the output would have the same dimensions as if the unpadded input was used + in a convolution with 'SAME' padding. + + Args: + inputs: A tensor of size [batch, height_in, width_in, channels]. + kernel_size: The kernel to be used in the conv2d or max_pool2d operation. + rate: An integer, rate for atrous convolution. + + Returns: + output: A tensor of size [batch, height_out, width_out, channels] with the + input, either intact (if kernel_size == 1) or padded (if kernel_size > 1). + """ + kernel_size_effective = [kernel_size[0] + (kernel_size[0] - 1) * (rate - 1), + kernel_size[0] + (kernel_size[0] - 1) * (rate - 1)] + pad_total = [kernel_size_effective[0] - 1, kernel_size_effective[1] - 1] + pad_beg = [pad_total[0] // 2, pad_total[1] // 2] + pad_end = [pad_total[0] - pad_beg[0], pad_total[1] - pad_beg[1]] + padded_inputs = tf.pad(inputs, [[0, 0], [pad_beg[0], pad_end[0]], + [pad_beg[1], pad_end[1]], [0, 0]]) + return padded_inputs + + +def _make_divisible(v, divisor, min_value=None): + if min_value is None: + min_value = divisor + new_v = max(min_value, int(v + divisor / 2) // divisor * divisor) + # Make sure that round down does not go down by more than 10%. + if new_v < 0.9 * v: + new_v += divisor + return int(new_v) + + +@contextlib.contextmanager +def _set_arg_scope_defaults(defaults): + """Sets arg scope defaults for all items present in defaults. + + Args: + defaults: dictionary/list of pairs, containing a mapping from + function to a dictionary of default args. + + Yields: + context manager where all defaults are set. + """ + if hasattr(defaults, 'items'): + items = list(defaults.items()) + else: + items = defaults + if not items: + yield + else: + func, default_arg = items[0] + with slim.arg_scope(func, **default_arg): + with _set_arg_scope_defaults(items[1:]): + yield + + +@slim.add_arg_scope +def depth_multiplier(output_params, + multiplier, + divisible_by=8, + min_depth=8, + **unused_kwargs): + if 'num_outputs' not in output_params: + return + d = output_params['num_outputs'] + output_params['num_outputs'] = _make_divisible(d * multiplier, divisible_by, + min_depth) + + +_Op = collections.namedtuple('Op', ['op', 'params', 'multiplier_func']) + + +def op(opfunc, multiplier_func=depth_multiplier, **params): + multiplier = params.pop('multiplier_transform', multiplier_func) + return _Op(opfunc, params=params, multiplier_func=multiplier) + + +class NoOpScope(object): + """No-op context manager.""" + + def __enter__(self): + return None + + def __exit__(self, exc_type, exc_value, traceback): + return False + + +def safe_arg_scope(funcs, **kwargs): + """Returns `slim.arg_scope` with all None arguments removed. + + Arguments: + funcs: Functions to pass to `arg_scope`. + **kwargs: Arguments to pass to `arg_scope`. + + Returns: + arg_scope or No-op context manager. + + Note: can be useful if None value should be interpreted as "do not overwrite + this parameter value". + """ + filtered_args = {name: value for name, value in kwargs.items() + if value is not None} + if filtered_args: + return slim.arg_scope(funcs, **filtered_args) + else: + return NoOpScope() + + +@slim.add_arg_scope +def mobilenet_base( # pylint: disable=invalid-name + inputs, + conv_defs, + multiplier=1.0, + final_endpoint=None, + output_stride=None, + use_explicit_padding=False, + scope=None, + is_training=False): + """Mobilenet base network. + + Constructs a network from inputs to the given final endpoint. By default + the network is constructed in inference mode. To create network + in training mode use: + + with slim.arg_scope(mobilenet.training_scope()): + logits, endpoints = mobilenet_base(...) + + Args: + inputs: a tensor of shape [batch_size, height, width, channels]. + conv_defs: A list of op(...) layers specifying the net architecture. + multiplier: Float multiplier for the depth (number of channels) + for all convolution ops. The value must be greater than zero. Typical + usage will be to set this value in (0, 1) to reduce the number of + parameters or computation cost of the model. + final_endpoint: The name of last layer, for early termination for + for V1-based networks: last layer is "layer_14", for V2: "layer_20" + output_stride: An integer that specifies the requested ratio of input to + output spatial resolution. If not None, then we invoke atrous convolution + if necessary to prevent the network from reducing the spatial resolution + of the activation maps. Allowed values are 1 or any even number, excluding + zero. Typical values are 8 (accurate fully convolutional mode), 16 + (fast fully convolutional mode), and 32 (classification mode). + + NOTE- output_stride relies on all consequent operators to support dilated + operators via "rate" parameter. This might require wrapping non-conv + operators to operate properly. + + use_explicit_padding: Use 'VALID' padding for convolutions, but prepad + inputs so that the output dimensions are the same as if 'SAME' padding + were used. + scope: optional variable scope. + is_training: How to setup batch_norm and other ops. Note: most of the time + this does not need be set directly. Use mobilenet.training_scope() to set + up training instead. This parameter is here for backward compatibility + only. It is safe to set it to the value matching + training_scope(is_training=...). It is also safe to explicitly set + it to False, even if there is outer training_scope set to to training. + (The network will be built in inference mode). If this is set to None, + no arg_scope is added for slim.batch_norm's is_training parameter. + + Returns: + tensor_out: output tensor. + end_points: a set of activations for external use, for example summaries or + losses. + + Raises: + ValueError: depth_multiplier <= 0, or the target output_stride is not + allowed. + """ + if multiplier <= 0: + raise ValueError('multiplier is not greater than zero.') + + # Set conv defs defaults and overrides. + conv_defs_defaults = conv_defs.get('defaults', {}) + conv_defs_overrides = conv_defs.get('overrides', {}) + if use_explicit_padding: + conv_defs_overrides = copy.deepcopy(conv_defs_overrides) + conv_defs_overrides[ + (slim.conv2d, slim.separable_conv2d)] = {'padding': 'VALID'} + + if output_stride is not None: + if output_stride == 0 or (output_stride > 1 and output_stride % 2): + raise ValueError('Output stride must be None, 1 or a multiple of 2.') + + # a) Set the tensorflow scope + # b) set padding to default: note we might consider removing this + # since it is also set by mobilenet_scope + # c) set all defaults + # d) set all extra overrides. + # pylint: disable=g-backslash-continuation + with _scope_all(scope, default_scope='Mobilenet'), \ + safe_arg_scope([slim.batch_norm], is_training=is_training), \ + _set_arg_scope_defaults(conv_defs_defaults), \ + _set_arg_scope_defaults(conv_defs_overrides): + # The current_stride variable keeps track of the output stride of the + # activations, i.e., the running product of convolution strides up to the + # current network layer. This allows us to invoke atrous convolution + # whenever applying the next convolution would result in the activations + # having output stride larger than the target output_stride. + current_stride = 1 + + # The atrous convolution rate parameter. + rate = 1 + + net = inputs + # Insert default parameters before the base scope which includes + # any custom overrides set in mobilenet. + end_points = {} + scopes = {} + for i, opdef in enumerate(conv_defs['spec']): + params = dict(opdef.params) + opdef.multiplier_func(params, multiplier) + stride = params.get('stride', 1) + if output_stride is not None and current_stride == output_stride: + # If we have reached the target output_stride, then we need to employ + # atrous convolution with stride=1 and multiply the atrous rate by the + # current unit's stride for use in subsequent layers. + layer_stride = 1 + layer_rate = rate + rate *= stride + else: + layer_stride = stride + layer_rate = 1 + current_stride *= stride + # Update params. + params['stride'] = layer_stride + # Only insert rate to params if rate > 1 and kernel size is not [1, 1]. + if layer_rate > 1: + if tuple(params.get('kernel_size', [])) != (1, 1): + # We will apply atrous rate in the following cases: + # 1) When kernel_size is not in params, the operation then uses + # default kernel size 3x3. + # 2) When kernel_size is in params, and if the kernel_size is not + # equal to (1, 1) (there is no need to apply atrous convolution to + # any 1x1 convolution). + params['rate'] = layer_rate + # Set padding + if use_explicit_padding: + if 'kernel_size' in params: + net = _fixed_padding(net, params['kernel_size'], layer_rate) + else: + params['use_explicit_padding'] = True + + end_point = 'layer_%d' % (i + 1) + try: + net = opdef.op(net, **params) + except Exception: + print('Failed to create op %i: %r params: %r' % (i, opdef, params)) + raise + end_points[end_point] = net + scope = os.path.dirname(net.name) + scopes[scope] = end_point + if final_endpoint is not None and end_point == final_endpoint: + break + + # Add all tensors that end with 'output' to + # endpoints + for t in net.graph.get_operations(): + scope = os.path.dirname(t.name) + bn = os.path.basename(t.name) + if scope in scopes and t.name.endswith('output'): + end_points[scopes[scope] + '/' + bn] = t.outputs[0] + return net, end_points + + +@contextlib.contextmanager +def _scope_all(scope, default_scope=None): + with tf.variable_scope(scope, default_name=default_scope) as s,\ + tf.name_scope(s.original_name_scope): + yield s + + +@slim.add_arg_scope +def mobilenet(inputs, + num_classes=1001, + prediction_fn=slim.softmax, + reuse=None, + scope='Mobilenet', + base_only=False, + **mobilenet_args): + """Mobilenet model for classification, supports both V1 and V2. + + Note: default mode is inference, use mobilenet.training_scope to create + training network. + + + Args: + inputs: a tensor of shape [batch_size, height, width, channels]. + num_classes: number of predicted classes. If 0 or None, the logits layer + is omitted and the input features to the logits layer (before dropout) + are returned instead. + prediction_fn: a function to get predictions out of logits + (default softmax). + reuse: whether or not the network and its variables should be reused. To be + able to reuse 'scope' must be given. + scope: Optional variable_scope. + base_only: if True will only create the base of the network (no pooling + and no logits). + **mobilenet_args: passed to mobilenet_base verbatim. + - conv_defs: list of conv defs + - multiplier: Float multiplier for the depth (number of channels) + for all convolution ops. The value must be greater than zero. Typical + usage will be to set this value in (0, 1) to reduce the number of + parameters or computation cost of the model. + - output_stride: will ensure that the last layer has at most total stride. + If the architecture calls for more stride than that provided + (e.g. output_stride=16, but the architecture has 5 stride=2 operators), + it will replace output_stride with fractional convolutions using Atrous + Convolutions. + + Returns: + logits: the pre-softmax activations, a tensor of size + [batch_size, num_classes] + end_points: a dictionary from components of the network to the corresponding + activation tensor. + + Raises: + ValueError: Input rank is invalid. + """ + is_training = mobilenet_args.get('is_training', False) + input_shape = inputs.get_shape().as_list() + if len(input_shape) != 4: + raise ValueError('Expected rank 4 input, was: %d' % len(input_shape)) + + with tf.variable_scope(scope, 'Mobilenet', reuse=reuse) as scope: + inputs = tf.identity(inputs, 'input') + net, end_points = mobilenet_base(inputs, scope=scope, **mobilenet_args) + if base_only: + return net, end_points + + net = tf.identity(net, name='embedding') + + with tf.variable_scope('Logits'): + net = global_pool(net) + end_points['global_pool'] = net + if not num_classes: + return net, end_points + net = slim.dropout(net, scope='Dropout', is_training=is_training) + # 1 x 1 x num_classes + # Note: legacy scope name. + logits = slim.conv2d( + net, + num_classes, [1, 1], + activation_fn=None, + normalizer_fn=None, + biases_initializer=tf.zeros_initializer(), + scope='Conv2d_1c_1x1') + + logits = tf.squeeze(logits, [1, 2]) + + logits = tf.identity(logits, name='output') + end_points['Logits'] = logits + if prediction_fn: + end_points['Predictions'] = prediction_fn(logits, 'Predictions') + return logits, end_points + + +def global_pool(input_tensor, pool_op=tf.nn.avg_pool): + """Applies avg pool to produce 1x1 output. + + NOTE: This function is funcitonally equivalenet to reduce_mean, but it has + baked in average pool which has better support across hardware. + + Args: + input_tensor: input tensor + pool_op: pooling op (avg pool is default) + Returns: + a tensor batch_size x 1 x 1 x depth. + """ + shape = input_tensor.get_shape().as_list() + if shape[1] is None or shape[2] is None: + kernel_size = tf.convert_to_tensor( + [1, tf.shape(input_tensor)[1], + tf.shape(input_tensor)[2], 1]) + else: + kernel_size = [1, shape[1], shape[2], 1] + output = pool_op( + input_tensor, ksize=kernel_size, strides=[1, 1, 1, 1], padding='VALID') + # Recover output shape, for unknown shape. + output.set_shape([None, 1, 1, None]) + return output + + +def training_scope(is_training=True, + weight_decay=0.00004, + stddev=0.09, + dropout_keep_prob=0.8, + bn_decay=0.997): + """Defines Mobilenet training scope. + + Usage: + with tf.contrib.slim.arg_scope(mobilenet.training_scope()): + logits, endpoints = mobilenet_v2.mobilenet(input_tensor) + + # the network created will be trainble with dropout/batch norm + # initialized appropriately. + Args: + is_training: if set to False this will ensure that all customizations are + set to non-training mode. This might be helpful for code that is reused + across both training/evaluation, but most of the time training_scope with + value False is not needed. If this is set to None, the parameters is not + added to the batch_norm arg_scope. + + weight_decay: The weight decay to use for regularizing the model. + stddev: Standard deviation for initialization, if negative uses xavier. + dropout_keep_prob: dropout keep probability (not set if equals to None). + bn_decay: decay for the batch norm moving averages (not set if equals to + None). + + Returns: + An argument scope to use via arg_scope. + """ + # Note: do not introduce parameters that would change the inference + # model here (for example whether to use bias), modify conv_def instead. + batch_norm_params = { + 'decay': bn_decay, + 'is_training': is_training + } + if stddev < 0: + weight_intitializer = slim.initializers.xavier_initializer() + else: + weight_intitializer = tf.truncated_normal_initializer(stddev=stddev) + + # Set weight_decay for weights in Conv and FC layers. + with slim.arg_scope( + [slim.conv2d, slim.fully_connected, slim.separable_conv2d], + weights_initializer=weight_intitializer, + normalizer_fn=slim.batch_norm), \ + slim.arg_scope([mobilenet_base, mobilenet], is_training=is_training),\ + safe_arg_scope([slim.batch_norm], **batch_norm_params), \ + safe_arg_scope([slim.dropout], is_training=is_training, + keep_prob=dropout_keep_prob), \ + slim.arg_scope([slim.conv2d], \ + weights_regularizer=slim.l2_regularizer(weight_decay)), \ + slim.arg_scope([slim.separable_conv2d], weights_regularizer=None) as s: + return s diff --git a/nets/mobilenet/mobilenet_example.ipynb b/nets/mobilenet/mobilenet_example.ipynb new file mode 100644 index 0000000..a17880e --- /dev/null +++ b/nets/mobilenet/mobilenet_example.ipynb @@ -0,0 +1,445 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "colab_type": "toc", + "id": "aUVxY7xOGD1G" + }, + "source": [ + "\u003e[Prerequisites (downloading tensorflow_models and checkpoints)](#scrollTo=T_cETKXHDTXu)\n", + "\n", + "\u003e[Checkpoint based inference](#scrollTo=fxMe7_pkk_Vo)\n", + "\n", + "\u003e[Frozen inference](#scrollTo=PlwvpK3ElBk6)\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "T_cETKXHDTXu" + }, + "source": [ + "# Prerequisites (downloading tensorflow_models and checkpoints)" + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": { + "autoexec": { + "startup": false, + "wait_interval": 0 + }, + "base_uri": "https://localhost:8080/", + "height": 125, + "output_extras": [ + {} + ] + }, + "colab_type": "code", + "executionInfo": { + "elapsed": 31129, + "status": "ok", + "timestamp": 1521483961674, + "user": { + "displayName": "Mark Sandler", + "photoUrl": "//lh5.googleusercontent.com/-CjnV3zpGrlw/AAAAAAAAAAI/AAAAAAAABRU/dfjRy_tzX5M/s50-c-k-no/photo.jpg", + "userId": "108034853522252017283" + }, + "user_tz": 420 + }, + "id": "zo5GyseklSVH", + "outputId": "e12a8a80-c0d2-4ebc-9230-b170f11d236e" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Cloning into 'models'...\n", + "remote: Counting objects: 13504, done.\u001b[K\n", + "remote: Total 13504 (delta 2), reused 2 (delta 2), pack-reused 13501\u001b[K\n", + "Receiving objects: 100% (13504/13504), 422.07 MiB | 37.42 MiB/s, done.\n", + "Resolving deltas: 100% (7635/7635), done.\n", + "Checking out files: 100% (1946/1946), done.\n" + ] + } + ], + "source": [ + "!git clone https://github.com/tensorflow/models" + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "cellView": "both", + "colab": { + "autoexec": { + "startup": false, + "wait_interval": 0 + }, + "base_uri": "https://localhost:8080/", + "height": 35, + "output_extras": [ + {} + ] + }, + "colab_type": "code", + "executionInfo": { + "elapsed": 4504, + "status": "ok", + "timestamp": 1521493157017, + "user": { + "displayName": "Mark Sandler", + "photoUrl": "//lh5.googleusercontent.com/-CjnV3zpGrlw/AAAAAAAAAAI/AAAAAAAABRU/dfjRy_tzX5M/s50-c-k-no/photo.jpg", + "userId": "108034853522252017283" + }, + "user_tz": 420 + }, + "id": "obaW6O8bz3mA", + "outputId": "79b3fb23-caa7-4683-9575-ba7c2f55ebdb" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Successfully downloaded the checkpoint. It is available as mobilenet_v2_1.0_224.ckpt\n" + ] + } + ], + "source": [ + "from __future__ import print_function\n", + "from IPython import display \n", + "checkpoint_name = 'mobilenet_v2_1.0_224' #@param\n", + "url = 'https://storage.googleapis.com/mobilenet_v2/checkpoints/' + checkpoint_name + '.tgz'\n", + "print('Downloading from ', url)\n", + "!wget {url}\n", + "print('Unpacking')\n", + "!tar -xvf {base_name}.tgz\n", + "checkpoint = base_name + '.ckpt'\n", + "\n", + "display.clear_output()\n", + "print('Successfully downloaded checkpoint from ', url,\n", + " '. It is available as', checkpoint)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": { + "autoexec": { + "startup": false, + "wait_interval": 0 + }, + "base_uri": "https://localhost:8080/", + "height": 215, + "output_extras": [ + {} + ] + }, + "colab_type": "code", + "executionInfo": { + "elapsed": 1486, + "status": "ok", + "timestamp": 1521485010457, + "user": { + "displayName": "Mark Sandler", + "photoUrl": "//lh5.googleusercontent.com/-CjnV3zpGrlw/AAAAAAAAAAI/AAAAAAAABRU/dfjRy_tzX5M/s50-c-k-no/photo.jpg", + "userId": "108034853522252017283" + }, + "user_tz": 420 + }, + "id": "qZDfLegf3hpw", + "outputId": "334ed084-b90e-4bd0-bd5e-125434a9a30f" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "--2018-03-19 18:43:29-- https://upload.wikimedia.org/wikipedia/commons/f/fe/Giant_Panda_in_Beijing_Zoo_1.JPG\n", + "Resolving upload.wikimedia.org (upload.wikimedia.org)... 208.80.154.240, 2620:0:860:ed1a::2:b\n", + "Connecting to upload.wikimedia.org (upload.wikimedia.org)|208.80.154.240|:443... connected.\n", + "HTTP request sent, awaiting response... 200 OK\n", + "Length: 116068 (113K) [image/jpeg]\n", + "Saving to: ‘panda.jpg’\n", + "\n", + "panda.jpg 100%[===================\u003e] 113.35K --.-KB/s in 0.03s \n", + "\n", + "2018-03-19 18:43:30 (3.18 MB/s) - ‘panda.jpg’ saved [116068/116068]\n", + "\n" + ] + } + ], + "source": [ + "!wget https://upload.wikimedia.org/wikipedia/commons/f/fe/Giant_Panda_in_Beijing_Zoo_1.JPG -O panda.jpg" + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": { + "autoexec": { + "startup": false, + "wait_interval": 0 + } + }, + "colab_type": "code", + "id": "g0H2RDadndug" + }, + "outputs": [], + "source": [ + "# setup path\n", + "import sys\n", + "sys.path.append('/content/models/research/slim')" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "fxMe7_pkk_Vo" + }, + "source": [ + "# Checkpoint based inference" + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": { + "autoexec": { + "startup": false, + "wait_interval": 0 + } + }, + "colab_type": "code", + "id": "GrQemT66CxXt" + }, + "outputs": [], + "source": [ + "import tensorflow as tf\n", + "from nets.mobilenet import mobilenet_v2\n", + "\n", + "tf.reset_default_graph()\n", + "\n", + "# For simplicity we just decode jpeg inside tensorflow.\n", + "# But one can provide any input obviously.\n", + "file_input = tf.placeholder(tf.string, ())\n", + "\n", + "image = tf.image.decode_jpeg(tf.read_file(file_input))\n", + "\n", + "images = tf.expand_dims(image, 0)\n", + "images = tf.cast(images, tf.float32) / 128. - 1\n", + "images.set_shape((None, None, None, 3))\n", + "images = tf.image.resize_images(images, (224, 224))\n", + "\n", + "# Note: arg_scope is optional for inference.\n", + "with tf.contrib.slim.arg_scope(mobilenet_v2.training_scope(is_training=False)):\n", + " logits, endpoints = mobilenet_v2.mobilenet(images)\n", + " \n", + "# Restore using exponential moving average since it produces (1.5-2%) higher \n", + "# accuracy\n", + "ema = tf.train.ExponentialMovingAverage(0.999)\n", + "vars = ema.variables_to_restore()\n", + "\n", + "saver = tf.train.Saver(vars) " + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": { + "autoexec": { + "startup": false, + "wait_interval": 0 + }, + "base_uri": "https://localhost:8080/", + "height": 666, + "output_extras": [ + {}, + {} + ] + }, + "colab_type": "code", + "executionInfo": { + "elapsed": 1422, + "status": "ok", + "timestamp": 1521493723379, + "user": { + "displayName": "Mark Sandler", + "photoUrl": "//lh5.googleusercontent.com/-CjnV3zpGrlw/AAAAAAAAAAI/AAAAAAAABRU/dfjRy_tzX5M/s50-c-k-no/photo.jpg", + "userId": "108034853522252017283" + }, + "user_tz": 420 + }, + "id": "TJbLYo_FCxXy", + "outputId": "17a4fbaa-dec0-4997-b233-15ba69806083" + }, + "outputs": [ + { + "data": { + "image/jpeg": "/9j/4AAQSkZJRgABAQEAYABgAAD/4SjTRXhpZgAASUkqAAgAAAAKAA8BAgASAAAAhgAAABABAgAK\nAAAAmAAAABIBAwABAAAAAAAAABoBBQABAAAAogAAABsBBQABAAAAqgAAACgBAwABAAAAAgAAADEB\nAgALAAAAsgAAADIBAgAUAAAAvgAAABMCAwABAAAAAgAAAGmHBAABAAAA0gAAAIwDAABOSUtPTiBD\nT1JQT1JBVElPTgBOSUtPTiBEODAALAEAAAEAAAAsAQAAAQAAAFBpY2FzYSAzLjAAADIwMDc6MTE6\nMTggMTM6MTM6MDcAKACaggUAAQAAALgCAACdggUAAQAAAMACAAAiiAMAAQAAAAIAAAAniAMAAQAA\nAEAGAAAAkAcABAAAADAyMjEDkAIAFAAAAMgCAAAEkAIAFAAAANwCAAABkQcABAAAAAECAwACkQUA\nAQAAAPACAAAEkgoAAQAAAPgCAAAFkgUAAQAAAAADAAAHkgMAAQAAAAIAAAAIkgMAAQAAAAAAAAAJ\nkgMAAQAAAAAAAAAKkgUAAQAAAAgDAACGkgcALAAAABADAACQkgIAAwAAADEwAACRkgIAAwAAADEw\nAACSkgIAAwAAADEwAAAAoAcABAAAADAxMDABoAMAAQAAAP//AAACoAMAAQAAALgCAAADoAMAAQAA\nAGUCAAAFoAQAAQAAAG4DAAAXogMAAQAAAAIAAAAAowcAAQAAAAMAAAABowcAAQAAAAEAAAACowcA\nCAAAADwDAAABpAMAAQAAAAAAAAACpAMAAQAAAAAAAAADpAMAAQAAAAAAAAAEpAUAAQAAAEQDAAAF\npAMAAQAAAEUAAAAGpAMAAQAAAAAAAAAHpAMAAQAAAAIAAAAIpAMAAQAAAAAAAAAJpAMAAQAAAAAA\nAAAKpAMAAQAAAAAAAAAMpAMAAQAAAAAAAAAgpAIAIQAAAEwDAAAAAAAACgAAAIgTAABuAAAACgAA\nADIwMDc6MTE6MTggMTM6MTM6MDcAMjAwNzoxMToxOCAxMzoxMzowNwACAAAAAQAAAAAAAAAGAAAA\nMAAAAAoAAADMAQAACgAAAEFTQ0lJAAAAICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg\nICAgAAIAAgECAAEBAAAAAQAAAGQzMzk2MmE2YzhmOWMwZTZmNDY5ZmQ5OWQ3NmE0ZTFhAAACAAEA\nAgAEAAAAUjk4AAIABwAEAAAAMDEwMAAAAAAGAAMBAwABAAAABgAAABoBBQABAAAA2gMAABsBBQAB\nAAAA4gMAACgBAwABAAAAAgAAAAECBAABAAAA6gMAAAICBAABAAAA4SQAAAAAAABIAAAAAQAAAEgA\nAAABAAAA/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAUDBAQEAwUEBAQFBQUGBwwIBwcHBw8LCwkM\nEQ8SEhEPERETFhwXExQaFRERGCEYGh0dHx8fExciJCIeJBweHx7/2wBDAQUFBQcGBw4ICA4eFBEU\nHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh7/wAARCACQ\nAKADASIAAhEBAxEB/8QAHQAAAgMAAwEBAAAAAAAAAAAABQYDBAcBAggACf/EAD0QAAIBAwMCBQIE\nBQEHBAMAAAECAwQFEQASIQYxEyJBUWEUcQcygZEVI0KhsdEIJDNSYnLhksHw8RYlQ//EABoBAAMB\nAQEBAAAAAAAAAAAAAAIDBAEFAAb/xAApEQACAgICAAUEAgMAAAAAAAABAgARAyESMQQTIkFRFCMy\nYULwcYGR/9oADAMBAAIRAxEAPwAv/HnmsH8PpenZ3mgBSLxTujZA39JByRuY8eudJPVjGa4B5LaY\nJaeCTDKpDSysoA4yTjIPro9TVFO3Sgolp8zGbf4u4oY0A4z8k/20Ou00Yhgpp2lZyjxq4G2TKgMM\nZ5B47+owM6+eViGqp1yP1FlqyjaxR0s0UkddTSeSZ4yoYHGUJXJ4xx27507WWlxboaqSfdTFfHUx\nhSGcdgxxyQCMnnPwdIF4mt9LaZKWnhqlnHhsk2MqcM2SfY4IH3B0/dPQ09HY6Jn/AJkDDYVUZ2eQ\ns7c8457D9dMf8OX7iMIJajK9ypadjJJUocUyoPHeXdvU5/5s+bsc9uf2pfQ2uSumWg8GSJlUzRsd\nojwM557j324+dGbhRWyrp2pI6pFkXacYz4YbGSOeOBnGfTXXwqSCljXx4fEmBWDw4ck4ABzkYznA\nGf30CZFGobYtaMD1PT1PPSQrAkQkR1bcBxJx3DAAHv2zqg1pFRC8CW+lQMQ/1KIyyK2ecnP24+dF\naylp5w863ZI44wiSM9RjGD+cqOOTj7DUs3gRJI1SzLHyZJIoyWOAOVAPtjn40wZVvRgDAGlGgtax\n07xxxnZGWy7K380842jPcDPP21VahKKS+6eF5F2gtyFA7Y+MDV1Lt/uCzYlaMkhCQNw5IAwPgHUd\ntraxwalqSYQE/wAtfDJXjjI476MdXGrjUUJbtjR7WViAisieJuwEBJ4+eQNcwUhpquqR6iZfFUIF\nPfOc8ftoUr1clNKqUskshw4VoezKcjP+mo7pdjVQQeI+yQjLNwMP6jk/P9tCFLdQy4B3OOpI/Duo\nZyJR5jk8kew0DWOYXCKRpS8CSlmXsDjlef8AOp75WK8BWZtwVg2Q+DtPGToatY1PLI7SKXh4kjxn\nBJwOPgaoCHjEsRyhmZXmK78SR5Lh93ZvnVK4gwSxNltzjJ5wM6oTVErqkaSMFY5bnC64qjlQrTne\nF/N8/OjRdxbNYkN0UTQNH4oMj8EEcsdDLlSvS0zRDxFjC5ZuwY40St0SO6zNIzsmcBRxwM5OuL9J\nHFaZKiPLvMhVge45xj/309TJ2FxLMbeAW9HHJPxpp6As63TqW1U00ZaNnRZVBx5c5P8AbOlqpnWS\n3eGUaNlPOdO34RT461s6r5jLMirkcfm5/tos/wCOpuD8pvbvFWVIpq+lUUok8KSpgCli6nO7aBxg\n/IJGkPqy32TwJissUtTHKBHUyRyAyLvxgZfAGQe6440h13Wd4heGxwzvPAuCTvJAb1PHcDPr6aI1\nlwmmipKC4MKmJ9oEwYKT3IHpn8x7/wDtrnvhIaxHHMOveHaKjjuUD0cstO8hkSZJDH5SFbAUYIB4\nyc49vnJu7We5VMNTBa0gkowqtHG6jKTH+s+vBwB6e+lEXgSSR0MMjx58rrPjg4wGx6qD84OityvN\nwp6QVLsadxG8CyodqjGOQQSCTg8dhkambGysKP8Aqbjb3MYbXXVVTRlKqbZWQSCNAhA8yjLKzHna\nck4GdD7hcJvElrbpDSxQjP06wEhkGeeAcEd8/OgVkvlbBW1FLcqylnp4nRmnidUd29FODuAO7Hvn\nVPqWpoquetmkZmjjIWEyOFd19Rx39fknRJiYGmEMsK5XLNFdqW6XqhtMTQW6ilqo1aaNMhyWH5j6\nliQO2OfbVl457DR0VJVCrhkiYGqpphukgUs6kqR+ZDt3cYwD66RKKWsW4Ca30TNI00bRwRKXyQwY\nYxkk+XTh+LfVNNeKy2fQwTQ1EdO31MMmY5Fy3l3DGf8An4+fnTnwgOOMiGT7gaCrrfrhFE0EdUTG\n7r4ZiY7WVd2VHr7ao2e63W9STUvjVBqypKHxiMZOMYPpyAP21HIqVMEdQ8YgZSqqFB3AgcH9f9dT\n2qmttHc4ploXWvkmXcpY7QMjLAjscgHVAIVZWTZFyamrI4Z6WkrvFH1EJkLOTkbTjPbPvx8a7x/V\nJcRIJV+mG7xy2SWA4QY9eCOdFuqbMt0ukd2ppJFCyFXdUJSHZw271IPfOOD7g6jaWjhttTDLUqy+\nJl1XPdDjePUZPb0P7aV5gPU1lI76gKFKq4Vs6ttgenhfY8jEI21sct2HPbPtoRQtcvq2p54pS6Sk\nDDDAGcEbux1Zoq3a0yJIyRuxG04Ynkckfv8Avq3JXQwztCpCujMrHIwQOOD+mqEXdGJGSxcnmpmh\npVFQY3U5O5TllPsR+2hDSzCUbsiXO0BRknPrq3Pc/qomEMOY9wy+eQQP7d9c0NDU1CPcoSHMaMAo\nOGU4BBH6HP6HRkBZllpYt6ywGWZlKiRSgA4xlhqG8Qwr0/UyBmeQyBORwuO+NX6eGrmtaTPUCFpW\nI2MM7io5/bGdCLhMaijq9tcjxRIWCbcZyRhs9icnS1azCI1FG4xFbQkxcBpHbK45wMYP2yT+2mro\nSTw620TNGZBFUIxUHGQCDjOprR0fJ1BaKbbWQ0kaGSGd5DyZc5UIuecqR+oPxkx07ZJLPdqGORld\nDIAxJGEKt2PseNb4jMn43uD4ceuoQsvTT092lkm8KilZi0Mm45Cn8pb459B6aOV3T1NR3Z7fWb5l\njiXwphEAry4BIB98559vTVW63+vhpCl2oaMUodGp5EkImcr2D5H5cHsO2NDbjfbhVRRzysTFHtyU\nJXk+vvqUpkc2ZuPJi7WdKpam21qT1gjSSNWRXK7vIQwDZB5bn7Dtq5PTq9sWSvkSpT6XwwJIiwUt\nyAfU4AB9yNL1xgmq7l4yVE4WMIBhd+TnPYcY9865udVNUTbom2vGwdWkbIVh6gDt2A0QQnXvPc0s\nyx03LS2WhmNVa6CtkmAZTUANhjnzYHJwSACe3+KdytddcGiqaVvGHieSnQNlSedq/wCmqtVXQzTt\nJIki1JQeJKqgZ48y4Axg/uNTNcpYaKllpq5qZ4ZTKHBIYED+xHvpgUg8h3Fkg99S34FRQNA1Kf8A\neJV3FIxllYE4z7f/AHoxfrDHUTpdr5c6L6+pLhvDDNMkinnj8rjAHwP00u0Vuv3UNSsdhpqqtq5H\nJPhgg7T3PwM51pNq/CK/vJG3VV+hSMKv8qKQzzoDyRnsrfqdacbkijDHD3EVYWs9ZGVw9K//APVn\njG3j8pBHuM8Z10uNDU/RR1dsgkjaFgwOw/zBtIJ57kHHpjW4dPW3oS0VAmtlhgJgwoasYysWAxnn\njOnSh6upHbw3oaXb6KIVwNaMDgaheatUZ5yt97r46G4R+L4DTK0EpDHawxnAOMZIPbHroBPRfWUy\n0tNIkbTKJJIkkB3jnHf17jGfXXrWooOgb7TOl8tVES4wWjj2EfqPXWDfiA34J9KVk1P0z1XejWsp\njMNJFHVRxH/vfGD/ANpOh+kceoCCcytQuZDNQx0N1qqcMQKcqsjlgxjOM984PfH6auWyl6XnnkNR\nc6szI20FV2jJxg7hn/xzrYei/wAELx1Z019fSVcNN9U6TLU14/nSgDALIpKjOT/pqjW/7N/4h2us\neaCjs9yp2YsTBVHxR3IO11UA5x202no2ICkCIF16L+hmDwtKsbsGaabJU4Hdcfm/NjVxqmntNKVi\ntv1E8GzDbTHHJMAQAFHOAGP3yPTR+7R3yWhFnu9tWlrKRPDWmJKuG4yzA8A9z3AIx7aSrndVmcLP\nVQzCGVd7KSvYEYHp65Ld+PjUYyNkcqRoR5KDqXpaqsqYHpXp4gyylo41AyRuIXkcZCkr9gM6W+pp\nPAtcdvRI4yZTvIUAtjBwPjg6PTT0c8LwWze07pvgjUHBZjnCnse3b1IPuNA+o6eYVVNU1SCVagHf\nkbfDk4BX4wTn5B+NOxgAiY49Op0keotddbYaeVjUQPJWTzKu9Y2Ixg4PIAXIxycnRDpauli6qieq\nBnaedY2iL4DbwM5yewznGh9Hdqqy2lnpaZT4rNC2fzFQMD7D11BSNG12SsiZttM7StIicEkDAA/T\n+2iyjlMwsA0aPBpZ6A0dwiNZvQRlg5JA9MZ7D41ak6ft1tnDQVEdPGacRKkhdvEHocnsc/41SpKG\nZUanjZWkdhuVSGx/f/586IXNJIY6eKWOOdIIgm5s53DJJH6nH6aPIygcYoJ/KAz9DHWSW53qQ28Y\nUZO9j7H8uABq5RLBWRiijpnR8lR4hG/ce3I/xrvDR0dYoLVAoJIMsh8MtknvnH30Ot00lo308c5F\nR4u7fjO49xydJRh8wQBcJT26koK96esgcSCM7Sh8uCMcg9zx3B10ttrihqYYacb97BcOQwHue3Gh\n9ZVV1ZVO0kxnXA2tj8vwf30QolqbdQSXCdNqSeWIyOEDY79+dMJsbMIJc0K33lLHSihsdKGbnxpo\n15c/J74+NcW+/wBZWVDSO0qgNjBJA/zrKoL5V3C4DewhjQ5CQnAz7/J+dPFllTaAO+OTnk6qxgiA\n1e0dbVLvR855cnvorFUeF8fPvpctE6LCWZhgHPfXN0uyUtFUVrHiGNmA+wzqgECJIuIH4/fiPUQh\n+lbNUmN2X/fpUbkA9owfT3P6DWV/htQveOtbXRFd4knXcvwOTpdutdNcLpU107b5Z5WkY+5Jzpv/\nAAPmEX4kW52baFDYPzjRA2ZlcRP0O/D+qUUUMEbBfDULtHbA0+QvuXONYB0jfngq1IZguf31tXT9\ncKqkR94OR76nb0tGrtZj/wDtU9GJUUlL1tRrGk9ERFXMSeYedrAD+oE47cg/GvKPUFkudRW/WU9L\nNMlVgRDcvnI25wc4P5gf/rX6A/iZStX9BXynSnhqH+ikeOOZdyM6ruAI9eRrxx+F1VWVVvrYahqs\nxw1IqIZKdf5Ue9RuAyM9j2HzjnXP8ZaHmOo1fuAIYm2y23K3VlN9NVSRTU8oYSIMGGRs5UevAweO\nMnTzVWm6XLpqf+LVSNOk2XuM8Y3sjEh8gY44G3IzgA5GeDge100zimgpmpHcHNKSRu9gSN2T+320\nN61o5J+mKmoo5qqCSQsm1186xjcfT7EeuCRqE+JTkNgToDCmMRIraZYulI6RVQtTv4NNI0uXnVFz\nxg8nHYf9QGTo5aOh6ux1VDU1ciVdtuIibbA+xlVyOWDAltoLHHrjuO+l+w2eS4dVwU8Eppo7bMoN\nOzbmC7ckg9sZH6Z09dQQ/wAHUk2xq8uNyM7l0Q5ABJ7JjH64x6aod61fcmUcrY9CQ0lDSwVRBWbm\nNshFxiRcAE+xJ7Z1DHY6usiNyk8dIg+dvhncV57A9znjOiFyrblDb2rqeokFZTyEoE4aUq2cAeuc\nHg/pnSh1r1fJW9U19ZAtVTU8jndGkzEJxg4GRjn00OItlJMEZFYQiIZZZY6Wjt0yxysd0wGdh4A3\nE550Dp+mq9Lq1HdqinSUkkEyDc3HDAnhV9yefbV3pnq/w6iWlX6mqWR1TxvDy3yT8fc8euouuY6u\n4LHeKGnlhozHt8WokCAMGxnaDk9u4zoxiKN8XEnIA2xCd6qenoZkt6QL49LEqrJyqyOBnuc5wSQG\n9fbSv1lWPV0KqVXyRZIB4BPoNVZJ4sK1TUq6qvZfOScfPIGdULjVLVosAYbpUCbXxnj1zp3h0Udd\nzebHuBOnqzw3ZXbawbBB1pVpqldEKyd/bWK17vQ14j8wIzkkY5z20y9K3+TxDFI/I7a6I2Ig6M2e\nnqcU+wNznQnrW4eB0tXndgimf9yNUaa5Hwd24dx/fQLrOsaqstXDu/NEdCWhgTHl5UMdNn4Rvjr6\n3D3Y/wCDpScHGPRfXRv8OqpaTrW2zyOFUS4LE+/GmgwCJvP43dTXrpro6FrNK0D1U3hS1CHDRDGQ\nF9s88/GrH+xP+InVtR1zX2a6XqqrbV/D5KhlqpTJ4bqRtKljkZycjtod+JNJHfLDFFIWfw2DRgHg\ntqT8Bemx0tcZqyuqBAapgH2qcmMHIT4ydKyGwQIzGKome5aCvhqAVLDJGcHsRrwp+OV7i/Db8Q7l\naLDcBNGkhkp46eQgxJIS+xm9GUnjGeDr1905do6kGqPkiP5B7Aa8r/7R/wCGP8R6+qOrbWr1FDOq\nmSnU4YSBgCAcHy451KoDin6jXXjtZwOtpp+kLVU3Kgimq/qPq3rtpEkw7GOTYcEEHgkcEaB9S3Ga\nuv8AKIKGuFkqZI5SqtmRsRqdpfgdhnHznQ64QXSokqIpKCWmdYUgigjR0ESrg524/fPvn10Pmgro\n7jHb62okqqKIJI8TT8SqpAKf+ny/bXP+mQm6FxeRjqNNjmHTF6a7NTwyxVsW+ONs++WAPwQPnsff\nQ289ZVqSStDG700x2GJVwQMk5Yj551Q6iBrTNPYIKhqGnqzL5XyIA58qgd/TH6a4tMVZb62ptl0t\n8lJWsu5fqFKhtw7bf6lIwcgjvkHVCIF2e4BcFSo6jhUXKRupZhMkksTSna3JO1QOcEeuBzj19dUL\nr0nVR9K1PUBtYoURs7aqoV3qyzeUoqgbR7bu+qcd8aKSpkXYlTM44Rdngk+gBzj9Mau9N3rbbqij\nkFRdROwVIquXwgpGdoVuRnvjjQoGUdUYvkOjEye6UTVRlRFpJnO1vp5cHJGD5ccc+2isrLUQC0mY\n1FRIi+FFJJkhiCQAPTtgj7as9Ux9NtUvOtO8QO3/AIr7nifngsMblHp20q2CtqoLqk0yBnnJjwpH\nJPYqT8+unn1ixGggye7dO1NrepWadlenyPDXz7z8juAM/wCmlGsrizxxwNyDuZzj8339tNNwvl4r\nLx/CaONXeaNA6xwlpCAASCe54HP20HvNrkhphWR+I2yYDBUeZT2wB/SCDz8gafjJA9U9xMB9cVEk\nt5m8SIQkFTt9iVBY/qSToVZqqOK5QPVSSxwbwJWiXLBfXAPfRu72mqrK2urqZf5W9nbkFuSeT/po\nJVIkMeePFUlDjsfnTkIqhMbuaZa7lZroj0tHcpoKjGKeKpCqJiF4w2eCT6HHfQTqqsq6aKelqI3h\nmA8NlbuP/g/zpFhMksyLuJ8wH21o9xhju1qobfdbrTUVUjhY3lBy8fuzHgn5JH+NAw4kGFdiosNv\np+g/Ep5JdtZXeHUbThfIhKo3qe+7vjjtxpcWVkcMrEEHII00fw2oS0XC2TrBG9PL4oLSMhwpZWkK\ngYYdwCeRnj10pnvpinuCZ6g6CSS59KWaaqO+QQK7k+p0QrbotLUjGBhuPnWafh31hu6PFvWcx1dG\nAh5wShPBHxoDeKjqnqXqNbba46jYrAqwyq/9zN2xrdXUzc9PdLda1dRRJRtwM8vn01J+Kt5q7X0X\nT1ltcCf6+MCTBOzysc/uB341l9LS9Q2rpi7VsQaSWhoZJAyrkFwo5H751PXdW1qfhPaoruGlrauR\nGBBIwq8F2I55Bx+vxpbIOBUiEWNg3KPU3X/VV5kihuHh1dUFJWqMAEgBP/OPbGghhYzRK0paN1JO\n5icHPuf0++njo6opp61qa/3Kmlt8aloZooip2EHjB7+mMj76ltFLba67RUMUUNL4qNmSopSvmJwF\nJ9Nx4/01yEyOpNpX+N3G8C3vEq0yxW2++LJC7U/ixmojV2VCi8bjtxnnn07acbteabqS00UU1kar\nrrJzR1kMxjPgsSfDcMSPtj7eurPU1LXWmtFN4aVdQOJmpysmSB5MYyGGO4x6aHRw5ukNR9VHSE7i\n0daWGST6egBJ+w450vmmQ8wKb9/254ow0DqUeo7HDTV1VUUkbNb9zLG+0qAD2BHtn29hoVSQS2Sg\nrqevWBldPGhlRvEGVONqHsoIzk+vHHOtPoGSahmtlFLJBLTxlA0rrUEsOSc5wCfNweftoWxNupxH\nUQislqYX8WA0++JomUMNozzzk5HbaedDi8U11f8A2AArmiNTKKq80cyfSPRB6IVAeaMOsTSEDg78\nEheTj5z99Are9vWqgZ5qhoFnHkJHiKM/0+nb99ONbB039Y6UltlpqiIENlQrOox2VSeD76WqeWx3\nWWq//VmndnjY4fI54Iz3x27ep101Y31BRK1LEc4o73PWxTPFUQQbBLHwGSQDk47gqxBH+uglVUzy\nVxhmeSWNQzRsXyFG0kAD2BA405Wq3Gw2+lmrohc6CqoKyGONgAVZRgHJ9VyuB9tCqpLXVfw6W20r\nUlQLYkVWARjx1BVmHyVIb0OdMXRjGixvrIY2twm8Ohz4kTOgDMMc8f5zoV1VRLT3doKZ/qI8Bg4G\nCcjJzpthaljip7cYzU0cwIldMeKnP/EXPqPX0I1Xv9oMNwlA+nMRRWWWSTYrKwyGA9c/HOiVwGqC\nVJFxGgTYwl8PDKeR76arnDBebVK9DIgjp8uqyYViABkYznvnjVmk6R/i8sXhXilo4VUh6iRG8BDz\n3YDP7/vq/feg7NT1dPP011DDdk8FfHjicOyzdmCuMZU9xnkduda2RWIF0ZhUgSjdquluVkoIfoK4\nQiiQGsdN5LjhlDe2QfLntjjPOka60i01ZJHHIJYwfJIvZh6HT1TUnUFikr4KO11k9NVwvSxoHLKs\npI86qCfYjt699QdSQzTWuLp+oscdFdKCqmac4O9EYKdhA54Ibv76NGo6mEe8R6CqnoqlZ6dirjg/\nI9tbR+G3U1B/C3mnIieMDxB7D31j89qrY7PBdpIfDo55WiidmAMjL+bA7kDPJxjRTomsSmu0K1IP\n07Ha43YGPc/A0zV3BPU9Mz9U22s/D64ra7pTzVNTEYYUXli7cAYHr8ay9a6pcLHVPLFVQSBcsm0Z\nHpg9jnjjjRC6UkdlejTxqeCObfKjU0PO3HGfQliQAe4HOhVFX0twnEMcU0aRwuJGll3sTjJP9j++\nk5H5bie+oftkbJRGsqqldySn83O19p2qPfnnU1rv18pr3G0VTG8RIDrszxjlj/T6f+NUb5HSUMNP\nQzyGelnRZSwiCsrFRnBz6Zx/fX1JDS0FPXTGoCnasKSFi+Q54GcnsAeMajfkELVuVFgaX4jF+H1X\nBP1bTrNUPHRmY73lUuBxyTg8DOiXW1Zb7l1LLTU+6KzfU7ad5sDYBwwJ7hTjgn3GlmqaWx2xXj2V\nCPl96EKRxnnnnSlX3qP6OC4Q1qzVM25ZElcAoRyOPbHbU58F5xVnGxFL4gp7Xc9CWnpuZ7PPb73b\nYJqoARzTwjDygHyuknqcAAj1zzqo9LcKaKncR3C1sQ6qgcsQobAXcQBkqMkDHB0V6c6wpV6iqbhe\nqSWkeuZ44Itw8BCDnvkBSfY98HtorfqLq+/WVbxVS+DbxVNB9NKmxwvCrIuG5U+Yd/QHsdcpUyE8\n1l+IBSPgzK+rem79d7n9askE7PTNEoLDChP6R282MHkewzpMl6Ar3MUsLvRyMgkYSqFb5JH9K/8A\nUfjW10tTBDc0hDR1FHCsQXwfzBsbWU55J75z/jX3U1H0ZWR1cd/uM1oqkK+HVS8ROnJ2q/uccBsY\nxxnV2PxTWADBZUJsTNOm7DRyVxsdzvH8QieciEeIdkckiAHhcMARj/09jop1T0Jct92razp16QUt\nMrMYqkBZkRApkRSDvG0AnHPGn2u6LsFNHabh0tVxVNNTUQeKbwldw7HxFlbBB/rzg9xgar9Q9UXq\nsn/g1dHTyRgnw6iMOq8AjAG0FT98+um/UG+9xvkrXqmA09EtLbZ62nheopcbiy8iNScc+2W/xqxb\nJKW7WSdqqhq51olLo7oNihiBhWBzjI7HHfR38U6C4w9OQx2W2oaCAL9S1OGMcr4DbyTgtjdjJzg8\nY1Q/D2z3CipaW5RySUsck+0oxwIwpJZsdgP+73Ppql8iPj5kycfbej1L3RdBFDDd6OtZYaWmZZ5m\nZWaOIop3lB2J7Y98Z476p9K2WlWWPqaO2iqpZZHSIOMKGOCGcLx2BOPUgg6LXG4wXKK4yUMcx6bU\nGWp2SukfmOGfYPJkHkY984GmDoCitcPTVNS11PPPRku8VPFGVbbIvEu30bbxtycE7s6HkQC3zHcA\n9D4irtnoxFe2eWnmnaR6BIx5OM4K87iAMjQa2dP1P0dyuE1jWpkqaJzBPGGy0mAScEglvOM+2dav\n11YazqKzwLR1kIggIeKOIbHcNgsScA89v09tALHR9TQ09eKmnpoaWiQxbKlhIDA4ACpj0wOeQRnn\nnjWrmoak2TEQTy6mV3m03y0W6zxRrG1PZ5pK00tVGjlZM5x2yyHaPKeO+lPp2w3i5XSNIaUu0pLD\ngLHjuST2AA16Ou3TVvvFqpL1bKiWgqKaFoXpanzLtyfOjHknv37jWctar5TVMqvSSxlJf5kMgBUs\nRypGePTtp/h84ZdncAqwP6iJX3Ova6RyVz1lRFDkSQsSiCMdxkDtwDnHpp7oOmrlR3a310XgGaeN\nZGgE3BV1/IynBGR9++iNLbaSop5pZILbHMkbTKrt/wAQD8yKTgFgPT11ap6Wa5XW3VwhljMUUcvi\nAbVKhgoHz3AA0x8y0QO4ePFsGUoLfNceqayO4V/0lNSSyJBCrZ3gLtUEn7Zz76ZaLpux/wD45J/E\nKSuTfIsoHj4JIBw2T6d/vqvNSwVtO8luhyY12TRGIhnI9d4/MWGQPnQDqKoqI0WGDqGKnEMcaNBU\npkqwAydvc5XB7+upnZjQU7jHWgWM63cySSxxFUMaDEcRl5c59fUf+ND57DQ3qJo6qop7fNSp4k6p\nEoDsScHIx749dfXK/ULUkEMkkVZNFGUaSniMZ59e3J5+dEYa7pw2SK4U6lqqLAZKpcuG+M+h0as4\nFyO1A6m7XmCghpDLNJOKWXyM5iUhXHJ3Kc/mzzuz21FepKNqKGH+KrS7yIzuQuqN3U/9J5z7aTb5\n1B9U0K04d2aZGeVnHhRgN5uBwy8Abvg6jnaVrdUW+63KhpJJXaY06VTErnG0bVBHPB78c576427q\ndAsQTu400sVJabW0CmnrzJISJQ20qEA49Qc+x7ao9ddC1vVdqkprXMrSQSFlmYkqkZGXXjJI9sDO\nrtpqqaSE0NHIXxTIoSZPJuAzJjJ82O3oR6asW2qrqGn+hp6c0yRSKxVU88isvfcx/pB5GhxLxbl/\nRDxYiVNxb6VtM1usMVtrb9LPVBijvCCHWLaQgGQcgHGMgHjHpo90/so77C/8QqZpWpV8R3AGEfsQ\nBxww5z9vTQ+oenp614qSRlWodZJncouWBOW+2WOcfprrUpDS0yQ1Fc60CVJjWVosxFcBmUEZJY5H\nB0eRATyM0rl48YVul8t9Qn0ayRMzSSBoETCrFypYk9h3JI7a7WFWh6bnjWFJ3h3NBIZ1H1KE7iyk\n9+Vw3uDxq50nUUVrrKjxLVkzRTfVpLCWA3DaV+AQvI0Dul3CuKOjoDDTedU2o4Cj02HGWwAB2Gg8\nmtXFFfmK1NLboamakNgelSfczRSxlUVtwOBznbgep7D407pQRokdVFuz9P8A7ukcn8tUVu2fT29v\nTSPbK67V31MErolJHBLktgMzFfKp9e+OM5010bLQWCa4TU7y0kkC+EDy4bg7eDkLle3+dPDEP8iH\njB5cjJLhDJWRCSkuD0+CFXyZkyucqMnAz8+/rqJYqy6tR0VyoPpKXwuI4XEbscHLjHGMHt86gmvt\nM0q0m1kSdhJAY23GNSOWyVwME8jnAOec6+iueLbV18lLLTSUziKsjnBfY+fKcjAwRjGO457DROxA\n1HuB3IZ666U96Nt6gWlqoXA+mkVQo8MglQ2AAWOD30rdV/WJ1JT0bXWKspxHEmZIlKbTwPKP6vQ9\nsDGNMFfW0NfJQRzU8FXTyTrGrA5ZCeB68c5x8am6ptNlS41FNTxurqgRSGDMvlA3Bie2RjnR4T/I\nxC4ibgKegp6msRZHhgkgiZYFUeXOQOOMjzZ+2dT1MP0MkEcVVHIZMlkPmAbHJ5HOCP7euvkug/js\n0cQcmMFQ0fmCKGweRnOWJ/XReGaJLrDS0kUsgKNG+5QMPxjzDt3/ALaY2T1dbnuFE1APgtGGrZ1r\nssm8PGyhBzwwK+4G3A0udVSR1dxkrWt6EsATs8pAxz5ff5GdGbzd7gakU9bEklOg2ySRuS6qe7MM\nADOQOR7aFWK2dN3quekrY6unmcr4NTT1B2kH3TPbOeAR205Az76MVmBHpEX6+8wUFNE0lHVyxOCh\n8AKCMjjg9x29s6DWeohrqs08PiNTiILHG9KuXAOfMRx6nHOdaRe+maKx0ErTV0FasiMYPDJLSYI7\n5/bIzj50Pi6foqi3+NaauKFiAZcKokXA/Lkdu/cjVSKePEmzJSv6n//Z/+0ALFBob3Rvc2hvcCAz\nLjAAOEJJTQQEAAAAAAAQHAJQAAtQaWNhc2EgMi43AP/bAEMACQYGCAYFCQgHCAoJCQoNFg4NDAwN\nGhMUEBYfHCEgHxweHiMnMiojJS8lHh4rOywvMzU4ODghKj1BPDZBMjc4Nf/bAEMBCQoKDQsNGQ4O\nGTUkHiQ1NTU1NTU1NTU1NTU1NTU1NTU1NTU1NTU1NTU1NTU1NTU1NTU1NTU1NTU1NTU1NTU1Nf/A\nABEIAmUCuAMBIQACEQEDEQH/xAAcAAACAwEBAQEAAAAAAAAAAAAEBQIDBgEABwj/xABFEAACAQMD\nAgQEBAUDAwQBAAsBAgMABBEFEiExQRMiUWEGFHGBIzKRoRVCscHRB1LwJDPhFmJy8UMlNIKikiZT\nVGOywv/EABoBAAMBAQEBAAAAAAAAAAAAAAECAwAEBQb/xAAnEQACAwADAQACAwEBAQEBAQAAAQIR\nIQMSMUEiUQQyYRNxQoEFFP/aAAwDAQACEQMRAD8Aohe9FxDc6cUlSKQP4pOGGP5WXtX0DWtRtte0\nSC6hmMc6tzHzuX1GK83taaR2dGnYssbe/vIwwc+DgjzLikzpqF3BJGiuio+Gk6Lx3xUusktGaTeF\ncV3c29kyWbm5Lkh2VSvOemKXvZzXV4Pl5HDbNzB8Aqw5I96rFCOIq1/TZo9Mtb7cRHK5RoT1Dc5P\n0pfps3/UwxmIyiQhdq9ft71bxCpaFfEUm7V7m6W3McU7HYjDkDhc0Uyx2+nwNZJuuRCiumMqoHOS\nfeggSGF38PWN38LJq2nQ3Zui48SI/lXjnA7ikELL86GyF3HnHFJfwCRJbnwNYd0kDRHAYPlgw9MU\n2D/xdbuWRFSS2XEZRcDA7c+o/pTNUC9CtL02XauLlENxJ4arLjC8ZzTSGe9h/idlGiwyRxLJHIvA\nJbqxHfoakm/p0XYn0a3NxHcWNw/iSt5wDhllwcgg9qLvNXGjWkJtreCOSWRhIBwAAAM8e9P8JT8o\n697FdTmM2/nVAHZuhJ9PbNIHlmcXNkITDIrkCQDKkDrz1FITiiUsTyskdxdymaTaABypGMA/pV1x\npc0cW13LAnakh6H71otjqCBX0d4LUsirJMhJ3Bfy/egrS/1LUZ1sJ7ovbI3KsM/Tn6V1J5YvWmaG\nCwUbS0m/wjhXXAwOOuf0+1J57bUIWmCTRFQ4Vwfyspz1H6VJcj+lJcarDngsNThnbyIh80fXP09q\ntudDSfU90MqRxSeZ+2yiuT8iP/NluqafBDZ79NbxJYRjY3Acd/vSOBZkvNzqUDDpvzj7VRteoEoD\nY7Xi2yJn3A5oGTR5p9pswrAHB56UilRJwoOPwvfW9r4phEqKuW8M520DPYXto6uUMK7d24nHH17U\nbsZIimqfMWq/OoWVZCpcDpkcVYPk5YTJBllB8xHQY9aEnRnjDLdo/nYooycyqJNyjt3o3UNQmuIp\nRv3gkKML/KBXPFSbLrw9bwpbac15I4imKbA2cEEj/ANC/D0gGqPNL5I4H3ZK5/8AvtV0ZO8PG+tr\ni9AkV/DzncOCaIiaK1DmBCW2kgN6UGg0gO4t5ZUEhLeEec+/pVsQZbWVnkKHaASO/Ocf/u0fgY3Y\nwsvEjia8EflIVVPpjqfeoXSN/Fl279zEcL/NkA80jLhz3Ud1aXMuSu5wsaleBt4xVVncvFoc0Msx\neV5sbh1HlPT70V4B+gMd0CSZmYhSE3L161Tc3kdu09vOrOrSCQSZzgY6fvWuhJDnQbmF7YytFkli\niIfLknndx2FUy3dy7R3aY25O9Q/JHoD3pRkEQskNtkxyDlix3cgY70rS8SW2ntokaaG6Hhyv0xzk\n/emTBQrn08fNNG+4qvPI6jFdtY5IrXw422wZ3bexNU+Eq0tDucEAbmBzVfixw2rS5PjKRiMp+YdO\ntMmaj1u5jnaXaF9N3+Kou5UZy0jsw6hR0oSYUg3QmuLgbw34RPhqpHBP/P61bc3kUmpTlECg+VVH\nQEVOPpRrCVo0gX8Rwf5V469T0oK+KzrGYUCSbdr59R3/AEosFYDlPCJCk9VbFHM5lYgAbVY9PSmQ\ngOYwWLZPA4JqDyqzAEE4PBFOhSZnKhwGAJ6A1dHJiDqAMjPFFIVgsh8VgD5sDIquRNrZHXPAxRMR\nlZktsnBbLE0pmj3OWDZyeT6UUIzuw4IyM+tXWqsv5mJwaIAiXUZYT4cbFdw5xx+9BXEzMQ7ZJPUt\n1NFABC+7IqpgSP6U5i2BscHtXpOoUevNYIMwJkwOnSj7dMwD260kh4lM+V6dc1bAm4ZNKEP01DLM\nqqCWJOBXqlJpMvGLas+r201s0Bjey/6V2zLJbxFTn/3Ee9N7e3sLW3Bt/EVic7n8xP1rlgsKSZK6\nnk3eFbCN2UAsC+CPoKV3GpsjT4miMcQBYY8x+n9Ko236JQmT4vmWVJBaOsr+TAXyD3o651nSbm9K\nSQKxwfxx5GjIx7c1kwdfpnfiRXu547WQW7TXL+LBPv2hlA6H0J4qj4WsYrS+F5fLK0lsjziJDhk2\nDq3tn0pn5ZhYl3JNsV1+Z35ysnUd8g9vpQ2l/PNdzRWayHKneipkY6UVSJTlbPoPw58R2EOkwWeo\nt4c8W+HjjjsRxzwcfas/efDj20F3fLIMRyYh6fiqepx24qb9w3gosrm3a4ht/B3SNJtkLDgrx0o3\nUIbrRrdUidHtpJd6ug6kD8p+lUav0Ve2MxYCfTIZZFVS2GDo+5uRxxVUNzNHdxJ4jGUEqXYdPbnt\n7VGbo6Yqy/TNGtEWSSK6azuHbbjaQAPWrtRT5orDIqSLbS+VgMiQ+pPXHFTcpVhOf6QSJlurdBFC\nUK8tg9B1xVc2lNq1ops7iPxVJYhWyWP+0/YVaLtWIsF4igk0+GJlKHO5GjbB78nvjnpUtPsvCWKO\nW5aaGL+WQ9DW0dMNuEsxGcQuWYZYoc7j6fWkM1rbQxSRQb4ujdcOKetwDk16ER2yTgG6mMURwokJ\nINHC1s7MSSWkP8SJVdrySbV3DOOO9GXgsZi63t7+5vJPmreFHb8QHbgD2FNI9LUrlkQll2kBsVNo\n0OTdALi2S3jcxKojA2FQ+X+vNLIo1jvMOqqFAbxWxx7UyeDyCV1Pwrltq7iTyPaikvI4i0pDje2c\nAYAoXRN0EwfFZRzDbWwmkHmILY8tV63eNe6Wsyolv5mEqMAwZe33qjkkgLj+2I4BD8k8cbeIMgsV\nTpjpUg62jrtWMLKQHXZ1FB6zNGkMVmXDBBGyx4DA4yKVyXW6dba3HiO+QAmDjPr9/wCtOkkgK2Xz\n3Eyta262+fDVss/IZs88fahb+0l+Q+aTPiO/Matghe5qS9OisFsMAedjKRGq87t1NrezkumwjDw9\nvJqsiSWlPjRi4Eal325UL1Bou/s1fTmRMxhypz1xgN/mpvCsSsTrDbFVmkKxZVU2evoaM0d2kkgu\nmwqqhG5u5HI/xStFC+aSNLCJdq95SQfXtj171QUZLT5oRna7ZG5OT2z9M0VdCt6ByQYO5VBRyd64\nxS6W0/DZm3nJ48nY/wD1QYHo80OUwaZHhH8RmKsSMckY/pz96Ga1nW8ihlI2oGcgt0DHj9qUZLAm\nSKRYpmkVxEy7cjqKXQJHaFY03kLlvenQAKeZ3MkrlljL4UNwc45qyO4VLcqhPhjB4HrVCb9Jz75I\nGFqyh1G4b+9KjulujFIeV7ZooBXcLLDAX2ttU4oaIvcyKgJyxA49+KSfgV6a62RdMaGFmOYgwlAO\nAGIxx9DS/wAHMzSnON2STQ4/B5E4Lt4btWBUBTnJANRuYlM5IbIY5JH2p6EtnfCDylVAyzbVPuDU\nJrjfIwCqBn+TjNYB5bRnUyNwOnJ5odbVvEJaQBScD1zRTBRWibC288g9+9dvGkZVSM8Hk4qiFOCM\nhYxg5ztyTUblW3kBvynqKIGVZ3RlSdxOW5pbMHeQhSUHXA6UUKW2kMk3UAqOpNEhQinA9zWAwaY+\nI2QxJ9MVCVB4ePSigAUgCynb0zXSPLmnAcTGeamW647VjA+7Egz60dat+A31pGUickiDkfQ1OxG5\n1jI6ilQzNr8E6VDJLG7rucMe3TmvVw8km5M7YJKKDf8A1O8MUq2FzIu7AZSBkexGP61Zp3xVLuke\n9mknAYKscIKke5xjin/59Y4cblcqQ1vviSN9PeeKCVC42ReIc7h0b6Y9ay15DBPcwfLSTWQhH4gm\nlwOeR169felUWWX6DdNuow5sxrFrczEeVTG6njnG7GKr1fXZZ4p9NkhjVR5HdCNzfTFNV4M1WoRX\nMUEkFtAqnfCDufJyxJzzWg0eW3Oj6hHeTJbvJCLeOcjJKseR9sfvTyVKhYv1mYmMMKhDgyI/mKng\nL0/WmtjeLpAt7m2Phi4kZWkJ5CDA/uP0pZaiH0K0X4g0nTbsLqdvHOniljOIxv5/rRmrfK3jSSad\ndW8Vpco22Igg5HoMcUngabM/Nokcdubqe4Alzwu3zbsZH2plp9vLq9zFJOI/CZ2SNW4jVgPT+9Vb\nYYpDe70TUtL0oXjyQyqgDMi8Ag9MY7V3Rrq0N2Fvo8xygYk2qQG/TNRn6MmxtfX9rHbt8rCs0znY\nDEuSKWQQNYzPBeXSSXMw8TaDkc8D+lBf4F21oBPaXMyttGWXLsRJ5cccD3qjQNVWBGVWkV94YrgE\nYHr6UUI/Au60sR3LXFk00UXMjbOFIPUcV1L22W7lDWcY2kMrSKQcEcZ/SqI3g0tV2l5fkwgjAcuW\nIBB6Y9TSue0t5Wd/mZHnYhsIozx7cZrXQX+XoovL2QSlbq3nZW4DSQtgen0plZWs/wDCmltsmRHA\n2DjcKpWWczTTorgkvEmYSqQpkC4HUZHT60SLXWLyLZBG0BH88gNRv4GMHYTeaUk0QEq7LhVxuUYD\nH1PtSUWXgXTfNqGXGNg/K3v60yVFpMudISA0caKRzkcmqjbtcRN4cgbIyB61iTti20svDupfEQJL\nKoRZP9tGwxOqfLzlGSTK5z37EU/+lF4E6VpzJumt3HnBQbuuQe49KtawsjN4szvIQdxGcKPpSth+\nAyGGTUEAUMrNjBftRWmaLPHJcXkcCwu6FId7AHPdwO4AxS9ykUdFpb2bRW8szy3Od2VYgDI5yTXW\njikuRAuRIQTkEEFf80yX0exbIkYy0TLnlcHuavg3G1M0xaNFG0gHqew/52prsTqA28rG7fwwNo8u\n8NjJo95J1hgGzrJuBXDZoNjJUXfLLIGAkRdx6Zy2RniqtLnJsJYEAeJ1PhlRgKRwRj70tDljXU91\nGDsCFG2gr/MOnPtimkW0qS8rFgAoD+b3wPagLSuwObZIGKqSQTkA96Fm85XykBgPzHpisYLs23wB\ngWChz5D1PPWrNT8aNtyqpMmBI4IwncfWlHRK+PiSYeUlkGMp0NL3j8h3u5Oc4I4FOhWIdRjkt7tY\nXUqudw+9GxGNbAOR59vIXniq/Cb9IRSjzNGMnBU/ShpocyeIgUH1C81kBk5Zla2EbbTnOR60Hp1u\n2n30dxEyybTnaRQkrAsGccedrOxZ2fzE88nmpqkskjIqHb0B7UIpIf0FuIGVz2GOuKhbo2QdxyOM\nEdack7C8sWRkYAq+4Dtmq4o8OBJtwp7UBkFyvstU2hTlzx6UslYtyQeua0TMrR8tlsY968AHPP61\nQRlnQHG44rxA3HOP81kKUXFqY3Vm6j0Pag/BzcBQSMnP0omGAKpEdqkKvGe5oF3bxUxxkkn2oisp\nc7ZhtcEMM81yVW29QciigC98iTkVYqgp0pwEXQA4Bya6gOCD2NYxS6/jD0zxRlkQYm+tJIpEsfjk\nVbp0X4oY0i8Hl6fQPgTyoc9Q55x2r1cEn+TO1eIr1Wz0bULS3EDyw6jFGEaRTtEmOMN/mhrP4Ukj\nQzS3QURjJRcHdx0znvTrk6qmT6Xp2y0+TXGM8oMEcTeFECcAnGduM+lMtd+H4JbexMS7rlXSKVC+\nGkQkDIHt/eg+S/BWqZ2T4PstKuvFsmk3lsskrBjxg9R0pdJprgrJPEyvIxO9vM23tg0VO3Yz8oXR\n2aRW7thg27GX6nmpSyx3WnixnlCBSGXwkG5j707dipC9NGmdpfDkDog/KWGXqt9LlgVV3NxnapOe\ncCtZzyR1dKe5lXxGiyRnlsdK86CyURXkYlk3qVyx4TuB9ayeiM91vQ0C7kkO5VPOB6fvT34bvbUX\n0hneGDaGxlcc00tQ8HTNFprXE+mX9pdyB4rsbYWZ87ePT0qh9MgtZAloz+LFthZc4y2Dkj9qg9LJ\nEneOAoHjBlX85ZSCKE06SOLVrze6MfDBV8Y299ufpWTSNtENXurizuZVDePbTAtuxgofY98V7S7F\nJElRUHniw4ZOf3p/oqLppRommxwvMkcfI2hcgZ9aTDVbeG4laRxL4wC+c5AxyMUfAUFQaneW9nKh\nZJFdAYQ4JIXPUfp+1JrLUL2wvBcQQvJgnKSMDn9OQKokvQM04v8A+L6f+PvtTuAaErznHHXtXZfF\nsr6GcuqJCoxGg4PFBMFadX4iiubS6ElvhYcSeIvlCsTxz7UA890dSkaK9EjpHGC7EEcnk+lOqA4u\ny4XDQ6mWlWe4s5otxkiGSjZ/p7UX4tlNCVchstmPK8mpv0ZRF99aJHIroEAJPGKot9qZi4wD5Qvb\n049KwrRHVLVvDjKymJmbnAGCKObSLFBFFBMXuIwHCk4yw75pkY5FHdvcjZasiE5L7l4wf3oG+tr2\n4kkt3jZFZSAexOeOc0rHSI2eiXa3EKCN0jSMs8xPAHUH605lKy7GEjMUJRC3UL2rUPqBbu1FxbCO\nV8kDB4oSOxUyBlkZCoB4H5sVmwBSytK7LCimRfMwC4JHrVc0pktVChcGTOQO9BDoEUeDA3lAJZmy\nF69sfpV1rE0Vt4rYk3IPBEvAwTyawQaO/miuZluIwACAuDkMDx96L0TTBZ3RiMY2vhtuenuD2rGs\nJ1GD5SJnBSPdJhIScAVbbXBfax8MEL/K1A3089mv8OOTtaSYnK9qCYSjCoFbccbWGf1oGLZrtLSL\nwmA8TIy+cbVArkCpekIrFxu3YGelYKDGjLFgqkHJON3qc0DeuyRsOVZR5ge4pkKxRcszqu8xsW8y\nnOTioR+VQAw8wIIFVXhNkrQfiBIc5cHINROJE3JuLegrUYhbxMZF8SMnJOMjrUceZto+3tRsIQ80\ncEeWLFydxC44zU1lmGx13BWHO6l+msuuXa7h2oQPXIxQ624GVMm0gZ5opoRkkdEj8wBX96skgiws\nqhst05rMKZ0pbvp/hiSUXAlyoAG0jHQn1zS2fdHJ4eQT0JrIzKEXzndjAFWnGcDpiqE2QMu1ckZy\nMVEzMV8uDjsawpW9yxjCy9jkYrlvCJ2GWCEnk5zimMFlCA2TkZoeS1V18TdxnBWgBlPyaCVBHIMk\nYAYd6FnlRcjqysQQG7UUwADtu5PWr4E398CqIxySPB681wDCnOaICqPDXKDnrRsS7Y2AH81TkUiT\nTo+aYWkaiRPL5d2CRSLwaXp9C+H7RbASxpkqkzAZ64616vPkvyZ2p4jPRSyXGPCYLK3mIIBA9ad2\nN/CXMUknnSLCoV/Mc9sU8lYfUXXEcNybdGhkk8PMpROpft/allxqRttUUTwPbhANo5JU+p9qn1SJ\nzkl6EyazA9xGba6ZmkUePKfKhPU8fWrJVnlhMeXAkUbHJ4H0NKp09EUk/BbqdvJbIhQrcTZ3MqnG\nAPWhEVbc20kpk+YmY4iUYKAnjk1bjn2V0Flet3MFpfTW9rEoUsH3H8w9RSnxZGVAWO09M8fXmq1a\nOWT0IKNG9vGRIcguoHfJweau1q2UfjFvF3IMNggqR2qV08HUbQDp0STXkYkkMMcuR4g/lOKqOfHO\n1lfYcBsfm5x3rqi8J1TN1YXlvbXKuq/NsbcJ8so/mxyRnvxUPFmlsd0qLBNJdmVHIyQfQVyyZ1x3\n07E9/eXU0twyTIHKbZMgD+9LMRHUWBQgq48w4HHB/rQoekFnVrdtWkEsdzNG8wEWD5VGec13Wjd2\nsiQWKyBp3YIw6D/5H07inQtCu4vJrO7htp7pEKsC4lOWIIJ3Yz/ahDDpsV1IYs3DyEsHdjx7gdMf\nWmFlH9B7ac+o2szIk8ckMeQxHDAcbR+lS0tbexgCWolj1CXLLLJHnYMchewbPela/QuFtta+LZy4\n+YmuRh2JOQxJA4PrV09reS6YrjYm1gjeI20L168U/iNFKzlv/C7NZoJLie/doizxonhxtjtnvUbj\nW7DTHZLTSrRSVCszM0uR9+On9KyGsnB8VwqWtdREdvEoP/aQID+nvQc+oJaxxv4gfupCgE08hF6E\nxyyX06GFRIjoGDse/pQ2pQlLmKS0t5TcIfOEPX/NJ4ZoJGoNasj3cAlbwzuiHb3qTbQySrsy6eVM\n5x9aFhUSr515riGGOYxyf7RwOTxzRNjC11rEts4DSqSRIT5dg7k/U0G2bqXyX7wXklr5pI4kCk9p\nO3FU3NvI4EtpjYEAK4JKt60yQzJxzwBfDnkIkVfMcYyaXCzDXHji8ljQNyi4OB60RWglfCVHJLtt\nJG9uKtlK/LiOPABUNhRnB71mMmDWyJNLtlYrEAzNuXnGScCrLmzjWFplRlQY5PdCOgFJY4DE9ub2\nJEdz4iYDEZ2kD9u1SWzlmuIokZ/FiwpCv1IOefWiBl/xHLmwuk4a4eRGCOvQHt7UJ8MwzrcTvKp8\nIrgLjoa1YTbdj1pNylSCpAzgjHNDBZFSaYkJGighierE4xilKC25naCTdOAzlhxnIriXciMSjFQW\n528YHpTJYBvQmCZnlKr9u1VTSNPLsJwinaST1rJGKL5Ybc7goIbAOfpStJW+ZZ0QMADhRVIiSIxz\nb1HmIORnbV8yQrYeMu9pC2MngL9aLMiVtqZsZ4rm3GSAQ2R5c9D1qILMxYbSWycDtmtVGsJmgMtp\nyFDqACFwM80LzHGxYE4OBk5oI0sLrG4PikMMRleuM0Q1xbosjPkuOE4/rSP0UXSXSSHhTV8c6BW3\nNwBwCe9OgWdM6MpMQJ74zxQTlTLx37U6RrIv+fnGDxXZjsXnk+1MIytZQXGBnjpVLy7ZTyysvXy1\nkKyKp40nGGGcnjBo+1gVd7AAMBnBHWmMi9wBbsCDk87iaBmuFRVVAfU45JoGZ4wNeZJVraP8uG/M\na5rMcLWkSxWyReHHsLoOW+tFIUSIvlGcUTCu36VRGZYV8RvtwPWg5225wfaiAotfNdqeTTKEhi68\njzY/apyHieJCo2OxFNLIb7iEY58RePvSIeR9D+HZHngeRgAWuH4P1r1cEv7M7V4jO24tharMvzDE\noFeQR8Z7j61oNEa2vrg+BBiJQBwPMT/8jVWtJ3hob2yTQlhurVg8qBtwcYxmsRqNtJfXDSyTMznB\n9qjNnLN9mANYzKTmUYp5Za4I9K+Q8QSOFJDFMhR/mkejQWgL2UNtH40V28jSoA6ycce1e1J7bXLC\n2WZN0kQUNMi8gdiaun9R0vcM7eWKpOflyH8xUY9R/miNNspQqXDQeJh9u084JHFOpr6cko6Halpt\n1bfFdnazXAllniDELgBRnpUviDT7mwQ3EYmkiLFSSAcZGBxUo7TKRxGTjZo7lDnwyrk/8FObWCfN\ntcQsk5kfhMc8HHPtXZJ9UTStmhW2s7PVFluJ3RlfcGzjacZPTtnAq1NThXSI9Tz4tq8zKQvLJ6Gu\nK29Oug6K4jlR5YH8eMtkvjPOB1qp3gkRk2qHZshjjIHpT3gFZQ1i23LAAnjy/wBa4Lu6hjCSkAq2\nFPByPpS2F6U3wjmsRceJDLJ+Ur0Ye9JVtomV2Tw2lVTw4IBHv/imbwDHZurv5CNoJhGQoONwOft2\nq46pHe2wt7i1FwSOcvjJ7nHahF0hFG9J28C2tyJofwsoI0t1B25HOc5yD0pfquuP/wDpCONHDRxR\nuydyT+bBx70VJsyWgVhcwAxKtgsRdNzsz7ywPHX1o7WIkjstEsxD4m23M7kjpvO4A59B/WryapUZ\nJ1os/gLXtq8lzdQW+WwA4ySK5fQ5twMhmgwyjttwF/r/AFqUpNgDNNuWVBCG6ANwecD6UVG93NqA\ndWIijcnxEBJz0C+1BP8AYxK0eU7fEUzSAkMqDBYE/pXLh4GPgJGYnjG4Z7kHBo2FADQyNcoLfcy7\nt3PB+1Sm1RxZmxRvljnLu5yX54UDt3rVZm6GEaeCVO0BR0xnpg1HxJIYBNBKzReLjYKZNoT0hKY2\nl/F8wbqO/wCtcjIKNkjaOMdsetaxhZfzznbFCVEKSZ2jv9f2ouCTwtRMKMzv4pUAdCP+CswL0PDP\nHcmMKJFiBlIZuTtPIzQmpXMhvQkTOyOp3buiUo4FHdXM2pwom0rMCXZE4HOB+9Wpcix1d0kKeKH2\nFV6rRAFauHlghlkPiROwVmbnnPFUQ39jZSvbhASr7vKdpopWhJOnYWt7HcTmRG8ndhzXZ7rCKRIF\n8uVXw8gn+9LXwdO1YBf3LTohkWNjjG4LjP2FL3fbgiMAjnIY806QrCFuBJiTYQBwdp71Yi7418yg\nycYNagojPA3yDwHbI6nPvgdSKVKjjb4LZJHOPSnQGTt7aRJGklUbQCQEPPHc1UJDdIyKoIUA5J4+\n9EQ7cTeCzITuHBqy0nBckZzkYwMms/AoMuJSVO1B/LlQckcUH4viL+UYPJxSo0mejOGyOfc0LLIW\nlyeAD9qDROzrDGChye4FQWVt+GUjBzz3p4owY7jwRHGOT3qraVkB2kEL0Ipwg7SjxPbOag84Y7cm\nsKzsbeGOAQTVdyGfcwXzY9KwGTSFlVVG4hhycdKYQyMsSKwDknqeuP8AFGwI6tq88jOG3nYx256A\nCqYY445GKrhsdfSsg0SaZlXJ5OepoTUZH+XQHGx89KYUWbQCewFEwDI/rToDO/lFAXGNzUQFVkP+\npBNGwEiV854bNJIeJNj+Zf8AcRg03sgxYEHGwb8//HmkSGZ9C+FZ459LaSM5Vrhzn6816uCa/Jnb\nF/ihC7PDcGC0mkCHzFgg2+4x60z+FLJrn4hfdKIxGu4dhnA6rVHInQw+JNcmGqzQTSJ4SbRnBINZ\n+a/SUhLRTNM/5R0H3rnldkFAnHpLzFpL6+RWj6QxDGfvU3a3MsMYRYYkfc7L1PHfPaio5ZaMaO68\n8d6q+EmxUHXHLDHQDtVWi6hGixxxTTPpykeJDtAZfXJAzjJqi8oZorlntv4bO0QSMyNtj8c8gnnO\nfTgj70LcsbSeK4tpvCSQAbM7huGMn3zRjFWQkv0S129vdb1C31a2dLa4to+h6MB3+vsaZ/FGqO3w\nfbzxnLyNHmRCODgnp9sVTrVJCKVoxHhCWH5gt+JuJYH0p38N28piYpEGlbzRs8hAVR3/AFq03gY+\njq9hn1S2SeTERjGIlHIb1P6ims8VjplhPHPbh4JiCyltg3bRnH3FcaR2J5SBtO1K28BrKC0jt4pQ\nAXt5/E2DPHBom7sSNRbdFGhAHmPAPvWoDb+npUkupHhtrqEucKjgflPofbtXIVT8GPU4Y2ZDw6rw\n31PqKFE+1AmtadBbP4Vi0bMWUqnAZiT2/wA1S3wzPPIshuIm2csY+QMdR9fWg3+jXZyb5eG0lG9A\n0h2Iyr5fcZ/vXtJga+uhDbrCCg58TIUDvzTLEFYH3UVq3xDb2UUyCZBmViN3BHQftVN+oFvdSyFG\ne2VWiecdu4x3+n0oxf7JWdhuNOls7WVltvEkBZlRAcAe1AandXGqXEjbHeGFPDjjVeg6cCquUWsD\nGT+lL2yQW6STjwUUFQ7dD74qrS7Y37b1YyxbhGSFHOe+cdKmxqCU0mzhmbwJHluADgg7Rx/9V21t\nrq2v2nJCxzDe+x8EetKPRC7gWC5cpJcbSuMluftV9vZxyxlIpMIqgmSVsYz15pro1DS2TTFtI5JS\n7KqnZIGCc/3rPzSadezum2eJg2N+RJz9ulZOw4SeRLOF13OyIu1QeO9ehuCEdvD28bRluPUmmQtH\nGJJ3xDdwML6k0DfX7QNJDkeInXaODTpAfhUlzGyeOwyq+Zlx1wRx+9FAPHewzqrLF5WPODg8j+1C\nWAjpb4civcB8jOVYHgnJ9K5e3ipPCVXy7dj4FL/4OV20cbRPHDcFFcln3Hb9ga5DYW4d59+JAMhm\nbO6oz5JRfgC8zSXGlSQyNEkUKlxtOckHOPvmktzEW08zC4icq+Ni/nyf7VfjnatEuQa6GqC1DFgp\nxzjJ81EXdyVhVo413wkjzjsaPrGjkQJJI7mJZeI8nbt/xXLmBJFj8JkMjAHaD15xTmbJNlI/l4tu\nC/mI7mgzu3MJBjBIIzyMVlpi6GdEk5L7HAAYds9eaXzXYEzeAmyNepbqaZI1kp5fEhVkyMjp3Paq\nbONYWYYJB/MD3pqEsndWyzfiKMZ7DsKkLkpD4UQVc8lx1+maBrIxBopAY2AOM89K6TnC7lJ7hRig\nBsvaRYQPKuCOhGaBmPiEEDHsKFAPIxPlzgelW+L5fDIAGc52/wB6ZBL/AA8Q7zzzkc1V4zRybh19\n+aIS0xwzJjbtY8Bh0FLXTLMgBwO+OaIjJiLJCs3OOKvgjjDGTxGOOADWAXvcHy7FXj96tuGl+XWZ\no0VgduBRMihXwygDAbqQfajJrU2qGWRwWkUFAo496F0EBkMZBDBuT1FKdTlSO5EcTMUHIz9OaaOi\nMBkny2Mcd6JtLuN1dRwccVRALHkBBx2oWUZ+tEBVFxPgcEA0dH/35VPTApGPE5JjxQO1N7GbEbZG\nQUYZ+oxSoZm4+AEA0EpnO24IOPov+a9XDL+zOyPiM/bAkkOshXpuU8/WncU4ht/+ndUfb75P/Paj\nIHoOY7rcHuoEVXwTxndzVvykUSsYnSN5M7FC4C4+vSpfRkqKHeS3g3XUUgG4gNjIY+gNcgu7eSLw\nTCRHK2Q7sDtP1qqSoVnb63jgIkkukmRAUIzg8+nrilvw7cGXWJIbaRXXYxkWUcEcAUVG0Le0V3cE\ns80kjKuEyGC98dKl48F/p4ka2kkmhXhQO/p+hzSNoXwUXMt1HetYv0C+aJjgZA5qnxHX4cuBlhGZ\n0wp9MHp+gq6+EChHfbgE4yTk1rdBvLaCyhUi4DBW8WRfMu3rz6U3J4HjaT0eWtwh1GztHYpGtr42\n7HYsMf2q74vtZLy1jjtxHLN4oYKzgMRjtnH965n/AFOhMVaPBc20RMlmYsAv5vLk9MH/AJir9PuZ\n9X0/5OWQreQEsjMR50z0P0OOfepLUPdht0x0+xW5uAy+UEmFchWHTJHBoSPVdQvRE88atA2SZFHb\nucd6deE5ay25WDVHiWBVijWIqbk8AkV2UXtrpnyMLxpbySDfOJFIUDuT/altIK/0hDrNzGziOCCa\n2jwqksuP0Hr6UzuZLUaMxubZHuGbJEfAA9K33TTS+Gag1u3GtSlYY4mSPaHjzyQOOtX3cJ1aEo4I\nYbXJ6ng8/wBf2orwRRLLxEJeWO2TwwNqwo2No9TirIJ7e3kRZF8PKYHrk9PtQiopgaoNjFpdNEpt\ngADuZgd2D6UJqsr6RcsXkWOzkIyoxwPtQlJqeeBTzSTr8xNHLbKJbWOHIZBggk9D60pvQ0gRYAVe\nJ2bAPU9cU6djRlYbJazX0zXb5it5Ig43469wPvmqILyKfNsqpbwiN9o58x75NM02PYjudZN40kZj\nEYHCgDAXHpVcPjeNAzsyxhxvI5GKaEaFse3VvDeXKRxSr5iGGM8j6Ghbma3tGEMituLFQG4XA/8A\nuigsEuBPbXEgUjEAyrAnDc44oSWXeCzoc9atERsJjsLu408TWkTyRtnLKvfuP70VCGOnRC73q8Si\nNQ/BZc5z/Skm7DHDstyZ9QZk879Ax6stUTXCNLsPld1OFPtSpBbK4NsURJ252lgSDmrCxkk2iN2J\nGFGKSUWwFsNoY7LeYZmLSrlE6kDgjFC3mjTyXUklrbzhGYuBsOMdhRhFrBZK0d06S9jzC8TRquCQ\neMEdsUUxuILW4jeFVDoVVs+v1qyQieAFraBUaPxhuMm3e5wF4q+GC2W7MbXUZx1YA4+tNQScJg2M\nYJjJ5jsfGB7/ANK8fBuJ5Vkk2nDO7N0xSr0a8KNPaO6ZLeA7BjjeRjPqf1oGdY4HfxSFC8EYJqiE\nZdZzRXUZhhYyANuyEPFVSyR27sMuST3XgUfoqIx3rNIqkAAnn2orVPl4yk0AyCW3bBgDHTilfo3w\nWC5Bfr16Zo1AbVgZFDZGRg01C+lck/iNnGMdjUBt5YkZz0NBmCI41KjGA2eOauZsqF44PBPNAY4b\n3xE8N41JAIBAxiqCoC+JK4UY4HrWRmdZwu0o24N2PFVzTovO0k96cmDu/iNjpkce1MLOZVQq0Il8\nuAeRt9+KBgfI8UjcNgPLGiLyRlD7HDrvwMj2rBBgxKBScZHUU2eeOTT5FGGYeHsO7BHHmoMwtMcj\npkkIByOc8UiuVM90x9DTRFYN4e6TAHSvCFo5QeKqKE5J49etVzNxgdu9YxRDJtn+1FwSlpCW6txS\ny8Gj6XSgmUVZC7NKinIQNzz2qaY7R9U+D4jHaTptAHjbuPdENerkkvyZ1ReAMvw9dW/iLZQxO79R\nG+cLXo9LumQNuMM8JAETJgkeuaZxEsItor+8txK4DJG3IjBJODyTSiW+8dFjaYlDISoI5JNS9eFE\n1Qxja202FopZGlLrncTkD1AFDao+kpYzppzIq+CjbJySWbPO2tGLJSmZ2/15pbeGGW2jmEYIjPQj\n7iqbeeOysfGZSsk3AKN5lA9frjFdLjSoTsmw6312K73rEhicgMVbuelSiWSNC/jfLqXxznzZGOBX\nFKLhL0otVlU6rc3JmljW5e1IRplyV+vH1Aq34hhgsYZljIaG7RHQY6MGB8vtXUn4RyhHa7rmVUOB\nHCOmME5rQWL6bb6bc4ubiK52EEAeQn0P2NU5P/RYfsa6Rd+NrEkki+M/gCBFToACD9+lEfEcNvdG\nMTRtuXBJiPmPB+wHI/SoTjhdPBLefGcdnapa21vOJdwRRIxYKOhOOh6dKZWNrAYg0jLE/wDK2Tu2\nnnbjsOaWPHgO1BQ+H7bUtMuI7u++VmQjCqd25e/39KV21l8nHJbWrPLGD5X3lW6YOB9akpbQydjO\nz0+Ce1t4JvFgy5LOp8wzwRjHNeudLhht5I7cXDW+8ZBYHJHc0zh9GUqKLaEW6eGyRrBHM5Dny5JJ\nxnjk0RereafaLLPCWhYbQ5IO04o1YWxHEnh58RNzE7+VweaP04PYXD3CSxvJIwI83t+Wm+UZCjUb\nm8h1CWWa1lRJmLKYx78gYoaCa4vZdh3Pt5HQFR9T9elSULdkZ3Y/kRbWBWE2FwE/D6n6+/vS+9g+\nf1C1tLi58NHyc9dp7Z+5Appz6tISTrABLq/0nUntg8kRJwQc4ODxxT631Cw1uUWmqbLScnyugJVz\nzx7Gg2u1EocnVk7t0TTLd7eR4vDJRY1U8YOcHFCy3F/cB3tgCWYhRMuVA6/WuiKw7Lsjq+nR+FBe\nLbAeKPxQjAAN9KqXRllRGkZlVcHw1kA/XNFWDBhJBbfNiZZWgYbQoYZXgY5bt2qV9p730Xi3TITy\nqyJj8wHP17VlELYFPYQRJIsl0xDH24qy20bS7wsTdGJIlO8o24+3HvVFaEsMt72KK3SNZDHsG0CM\nlR9cVXc6xbzYiuW3sg8vVv61uqYOzItc2GFMcMaOMgsQDQ76hYJgpErOoxkqO9HqbsDnUfBTdEM7\nBnGMgj0r0WrMJQ5/OW6+9CjXZe/xEbZmRmCnPIIPBzUYviFpZlhhIUk4BxgDHqaxgOSKSa9nuBde\nC3hlstht7D+Ue9BWmrFVkiu0Mpddu49VNYm1QS2nl7so/l8u5lkPtkZP0xQ0Gm7LJGVnKy+bGfy8\nkfpxRsZBFgn4R5KqueO2aiyqFKYPhMBncOo7/wB6WxhZ8wsWqboJGWIHy4PbtRF6yzxMRMxeU7id\nucmqIVndLMulC4eORXeaPZh0PT1FUG3MyhxleQvmGQTitZkir5QwuRvy5yCpXoPai4y8kKrJCdoX\nBKvnNawlUtpCI42QHJXJOeakoG7ynoMZPWtdgpF8YQhcZPrkdKHOAW3L0rGZEyHIC+veiYnGSHx5\nTzjuK1AsollAY7Scbv5vSqvEUk55HQZrIDZapVhjI46ZPSvPaybDJ5ZF9UOcUwAZQcZYc5xREErw\nMkg5API96Bg7R7eOXWIN8W6G5bDktwpzk/tXdQjQxAKDxI5z64PFLbsasBIhh8v0zzU0l2yHkY6Y\npmIQlDMzZPkPQUnlXbIw6MKMfQMqUYbng46VXK2XB7VUQsiz4Zc9uKqmwi+Y9e1YING344OMCmFo\ngdSx7mll4NH0LaE71PqK7GmLhF9SBUEy7R9A+AtbVdRutOlKjdl1LdeFUcfpXqhPHhSKw0UGrw+A\nk09tNEZW278cfc1fqKrd2SsCxEbBjgkZXvTsmC6TqlzY6KxtLdFDEgmZ8Y469MUiXTmY2skhgYTN\nktGMkD7cCpKNaa2VanaQpE/hzRyRlyA6jDD2P3oO10DT7yWPxpZYkY+Yg5/rUOX+RLikq8A1owu/\n9OdLUZW+uIz1BYDpSC++GY9F8W7n1S3CqCkUZXez9O33zXRHmbdsZ8aSI2t4sU3yg/h7JHjZemDB\nP1ol9emubc2s00Tt4hPiqoXjsBWlx9naF7JRLLOC0OuQRw+IsMqBZJAchn6kH2/xRPxPoVhYLBeP\ndLfWJBURxYSSMn2oJtSwRV1FOl2dkuoW76fdGVXkAaCaPY3HbJ4zTrVdOuFfJtfBS6l8PyDARtwx\nvHIPBpeRyU+zGilR688T4V1ZLq4aMoMKxC+UZ4wO+cZ59q7efFttc6lDDpFgZVlXc8tydox3I9ut\nG+yspCIPrel/LyXerRTbbaWZfCiZOJON3/DVOlXzarq5S4kSIz8Kqgkg8YyTTt+GeMeNcrbXJglV\npEidlYoACx65+lG2N9prLNerazNmQRBI0AO7GcjHQda511cqDbozE96nzni28cy3QkMm6U52+gp9\no1xG923zs0khaIykKgAd/bjmrJWyXZheqzW9lpcj3EskS3YB8ORMjI54HY+9KS/8dsWNrcoI3/LE\nPLsb3BpeWahpv+q8YI2i3YaKQSF3UbZFJ/MB6GuRQQ6dIpmZnIbmIIVLdc81oSjNYP8A9cPaYl1q\nl4sFpJHEiZYHqR9+9NTYzW9o7XfgkKSzOVGG+pxVOtYZPshYZbSeOVrlimwGSMR9DgZway1zNPdJ\na3xfY8juQTxnpgf89KlOP5Jshy/Av4vmfUP4fe2Ykmd7YJMEBJBXGf6/tQFvMb6ATDdG0ZG8Z831\nxSNVFSOZ/i7NZFqAt9C8SSQeJDIEfZyG3A4Y0vbULm7ULbOeeRsBJb9K6eN4d0dSYU1vO+glb94I\nkjk8UNIPMR34HPp1Heh4tiW0gfUI3cAFEOQGH9qdMYGmuS6P4bBlOckHJBojUL5oNGtFhyG3OWPu\nQv8Az70xhHJfyTMBI5wTz14ovVbmPTbW30+Bt8rr407g/wAzflX7DB+9U/wnvoPby3DgMQI4WHDt\nnGKsaObIbcCp4BAOKLaQFpTJLJFAWIbaWwTQguCXwnTPpisaqDre4uLeXekcm0jg7d2autXcXCSq\nhYo6tsI64OcUjoZWF3K/MahLcyjZ4xJZCMheeooaaykhf8TKB2yg/wBy56/elTGopl2wRyOgbe77\nlAHAwK88KqY2Z2YOod8jGcHkZpvSbRTNfbdVdpQ08TANsD547c04jMRWJ4Zl+XlIPhKPOhHGOe/F\nBo0GgC6kIvgYyywKAcE8k+9ETESyW+WLZO3afX3pEirEj2zpflQFWNZNmR2FF2iSrA8TtGrRuVBZ\nccVVeCA8lzM8rBZGUYOAp9AaDS6eOTzyOfvWSNYetyJ7WZmJbYAFDN5vfFUPEEy0MrLheEzzQeDL\nQi0vU8IKYQ5VMYYnP7VxZEfaCjKfZhRoWw2AN4oQfXHtUp7di8gIVMDd5u4oWEEKKBkEE1DxAilg\nQT06UyEZNJED7vLvHADjgihpnWGVt5CkkYAooBcG8OAtvOSeBXba4zFP+YMMY9D9axiBIB/KR3qK\nuSWz9hQMNdBYC/3MR4ccEjDP8rbTg/rVUuWxGWJ2EjPrS/Q3gMX3EqCc57VUc+Jjvu/xTCjOC0kn\n8ZDgbASPoOv/AD2rP33/AOuNjgVovTNYDPIMA1FFDn1qxMIkfw7XyY5Pel0mWbJ5zWCVI2H5ppaE\nIo96WXg0fRlkeGGOfahmcGVGwR5sVBF2bb4Q0/8A6u6lkUblCMpHuD3r1cvI9LQ8HulRlPEiaZpW\nUGQRt+X3GParX17T49EmvYZ1mAGzwicHd06dhVmSQFD8SWisnjAiNRnAXoa0Vq1nqdgJLQoiscEx\ngD9RRr9marRTrPwnqNtD40VnDeRsM7wcMD7ilb2FpaIyS3USXKKvlbKhc9sZ/eufk47fgUlIvj0f\nVJbeMw3UMpl4XbISPtms58QfDV7bvcXfgNLBAqkyFx+bI3CnjHr8C1aoptvldP1tlntfCtZwAY5P\nNtyB0P1oWSxe41ONMCOOWUIu3sCcZxTtqL7EG249R3BatpC6npoeSWIBWR2X8rBsYz7/AN6vt9Lt\nL7SyLm+Y2ysXRQArbscD1P2rklOUnaCl+NCPRtJmmuVk2hVWTILZA69/ettaW8xmu0W4cNIcY6rn\nAINb+VJtpLRYqvTK/FUeulo4bwfMxvyjooGD+lVabaXEF7ak25nxbkSKWGAATwc0/G4uNIvxujt9\ncXOqaOi3zsEViYxGANoGeAOnrSjSZLcXwlt72QmI42Ou3k9OfWuhLDSabNDd3T2yBsNKeS7Jhst7\n+tNtJS5YxKkhhe6topjIFwE/EKnA+mD9jXE+Kp9jfBne6BaR32IGUo64jLNktIOTk9h7UlivtX0v\nVnhgkQR48RvDAJI9jjiuy1TRByqVCnW9Vdb67hubhZhJJtUtyVAPAFLSWhw2TkncAGxmmUFWk5L8\nrNHouqtqelXUM3h/MxAtEjeXxFxyAfWrUuVto1nvbyS3XJU23/d+3rUJfxkpXErGP7KW1q22Eaar\neORjeyBOvoo5P60olvtVnJjeVpFPBBHFdEY16M3Soqitrq9uBAZEhYjHnOAar0TSJ7z4itIJVJt4\n5zG2TlQducj9P2otohJ6O/ja3TQraK309mgluJPGDI5BAwRj9aYaZa2/xT8FrfxRKmp2aGOcqBmQ\ngDIOPUYrj5IXChLViqytYCJ7K5DobpdnhdCxGGXA9TgY+ppVZXpkvIoUxHEpKAL2B7mn/j2oVI64\nf1H+n29xY2lz8tPHc24hliA2jyN1HHcnJrL6eTcXa2rbSJmC7n7HoP7fpV09HYNLDJp980e4GSKQ\nqwA4JHBBplrlzDPo1gts53eJI0i/7ThRj9qdaD/0W20ghbx5RuC9Af8Ad2/eg5JWmuTLMSzOcsfe\nqfRH4EtqUzrtMjY7DOB+lMtJ1F7q5SyGPxAQpY582Dgc/StIVHroMlqj4bEjMpUc8ilyJukZc9KV\nDsaRakylVty6ScIgHc969d3jw+K0hClTghhgg0tBsnaajIkQ2lWHBywzn0o2QJcW9tdr5RG3y88L\n5yinkEffP7UjVMdMUX+15VhU/iDcWA7Y6VOWP5bS4jMjRyuvlyMdelUTRKViITM0w3HIAwSa0nw7\nazWsBvUHzC4ZgjDPmHAyPqRWkyfGrYM8scl3cmdWiCDeVJwWPXA9uaDN8y7kRzu69c80VErKXwrm\nnk2hjyxGeB3phMss+mxXbFT4jHdjqG7/AOa2ICFgmSNVG4NvBO/0HpS4sTKOnPenQrYfpdsJZpJH\nwyRISATjJ/4auckj5ghVbdtEeemehPt0oN6FMMsr2KWAm4hERCnJUdfvQwIacCAliRwAO9Kg2EeJ\nJjxCDuA4PrzU0uCQySMDnkbuaDCy4Wo8UAHylS+enalrkiXy4cbuxrReiMpeWQueh5q+QfMKPw8s\nMcmqAIFSibTwepOc4q6JlS3YvwX4PHWsLZHBdwyA88AV3ZiQKwbJOMelAYZwvbw2d0J4ZCSgQNG4\nVhk+h4NVIsVxEssUsignBEi9PuKBikpiTGQR6iizZI0BnVXIUbSfU0fhgqZ5LBI/FjKmQb1YfzKc\ncf1rK6gVN++zIHbNCHoZeAMwYMeOKuhGAc8cVYkW3kLx2UO5GVZQXQsMbh/90vdSvBrIzwHwQ4+t\nM4cmJSPStLwaPo1TzQLx2qkjhCezHFc5c2PwhfmPUBCSW8RUGMdAM16uecdKxeGp1h7P52E3rPB4\noZRcRcbD/wC7FJF+CWDG6hkW7hcbjtGC/PXPSryVaiUWESWelKdy2kv4anILbhxVlr8QaXp9rAkt\npPFHM3iRSRLjkdV4/vUIzd6Vabjhprv4sSfSCwUJD4TblkkCupHfA7VipNZ02ezgjmszqV0RtU4z\nk57elPOROKcTyXUsJZIWS32LhVEp/Dyc45+td129W20RZbq3DgDaSeC+T1Ld/pSrRn5Yu0jTodW0\nrdeJIblmPg7Tg8dOprtrHYq63M7bZ1lXIkbDDacHHbtSyi/DnU7A7u4WXXLhZ3lMU8jc7uxOVIPt\nT+wt2t9Ga1k8Ax7ssw53H1/TFQclFUxlIsaSKO0lnl8kcYBJUdunSr7O/PjmKGPc4wXZztIBIwce\n9K+K1aY8VbA7kXE2qxDULme2ijfds2nHB/8AFWS6bpsrz3Ms0g3u5QBWIO7oDj3z1o8GJookkDPY\nW76d8vcW0rMFUYB28g9Rjt1pf/6LgskeYpNboWDHfIDz7GulcmAaVkLKC6tLwzxlVTko7EYYdD16\n+lPQJJZomgjiklt4f+87MuQQTtxjB696SSthRCSCbUSzXFw9pbMAsmGx5wMZA6g+9c1HR5bO28LT\nLotLgCUSlmDYHXJ6E5zjpWTaxk3FXYrXTECCS/WJ5SDtWNCefqcZq+zs90Iig0x4W6rNOrSDOOx6\nD71RzoPSyzULh9Jmtpo44hcOmDtAOxsfTqaWyajtu8XWJjINxXHJJ7Zp1pliKJri2a3BiBjYghge\ngpUBLuwWZkHfJqiIyB5JnjYbstg9K1/wzqljp0RiuZtk0tzGY1PVeQM/oanNEmhj/qLoF1qFrDeW\ncZla2yrr1O09/ekv+mV+1jqd8JCyweEplUc7Tn8xH7ff2qLeCVth1reWWva9Nb3HM9nIz2NwuV3I\nrZ2nnnjoetZ7XbJtJ1+UQKRFL+LDjurc4H0zj7U0Gjq45pqhh8Paq0NwwmUMpXBIbBDYwre/NUXN\nlLOyy28aLcR4WXaepB/N6Drj7U/jLCrV5J4dWnW7/wD1gNmQkbQTjr6Vw+XT4mlDZmZ154wMDnFO\nv8AV3MeEAjOUA8v0pc5IYnB49arEnIlDNCJR48bMuP5Dzn1ogMNN1K2nSRJoldXDA4Bxjg/fj7UW\nhUPpLWQ2F5BBFva3uTIhC8+4H2IOKB1GxuNMuYriW3kjt7iJXQkdW2jI9jnPX0qa/RRrAnTYlvPA\nkIaFjJguWGVHc4pZ8SvMmrPFK29V24cEEPwOaH/1Qv8A82W6VemWLjb4iuAoPA645p7/ABh7GJlk\nbdtdRMgGTggjIPtRktDFgF+iHUYYT/8ArDuS0g5DA/l+nrUr7xrxjC7sVtkzhvMSAeo++aUzBtO0\nUy6+vg5eGAGU7lzkDtR95qLQtD4iFRccuqqFMZ6Dp26H70snbBBdQPXIBHbxR2pL3zrtnXHAI58v\n60mtoJQ0wMZeRUycc47E1WPgsvS621UWM5Ih3uh8u8ZGP6VuHWzv7ZYbWNGae33GONhlGBBJHpxk\n1PkTWopBr6Zi9+C7i28OeG68e2A/FkK8qx/9vcds+tZ+WEbscqqnzHbzVIS7CTjTPNd3MBWO3iKH\nqd6/moy2+ZvLma3kZbWO5KbwwyGPoPSnaXoi10FwWcsF9LbOWfwlKbieDz2pvpltAHNwUZHiHUH/\nADQWoZ4y6eys9rmzeQKSoxKQcetKpbOZJA+5CFbpgg4rKLXpnO/CYuNq/ivl8cEntQIQwnDKPqta\njWVlRvOeBjt61fHIkUGNjFiCdxbj70QAk0jGfbg4zV+1nQAYx70GKWeEYlGCxIBPDcCjprJvBa4O\ndqsoOGBOSPSlY6Oz4/hczDdu8ZFznHGGND2EY3BABuAz06+tEBc5Gc5VcdSattJXnvPBjmxGRuZ2\n4Cgd/wCtM/AVoTf6gLgxSPG0kKACM/lyg4FZjUNr38joMBjkDPShAMgO4yTwe1WwIZ1EfJL+XgVX\n4J9G3xVfm5vltE8tvYJ8tGoHXHU/c5rPzDKjP1rIEgY5yT6UytvyAe1CQ0RnCQYPpVHJKqeAGqP0\nt8HumNLFOJEfay4Ax1r1Sk9KRWGx1i5i1fTnmt5VZIiSy5G73BFS+Hv4hDbG2WUwpJ+Qk/8AbPtV\n1ipnPWhl/Zm0vBJ4wIaLdIM8FvX+9DRXumg7bxl2IB+Gqn8x9Mf2NcslbOhSpC281W01O6n/AOjW\n3uYTtVZVx0GM1zR5LPT7CeV3EFyT4aljjwx/uHqTmhQOyesUx3dzm6nHhyJHj/upyF45JHXGevtQ\nervPIxtxd+PDk8D8p464prrGRnyP4UR6iiJDbKVYjggtjLHvkdKLtNPT52ZbvKeHEXRJEJDtjjn3\nOaevpNagk6bNquhwrFHi4hy0LKMCRcZKn3zmrbZJRoyrdKbeRuVj3ebgdT/io8kU9HihidQit4be\nznhN0JQsX/d2g7xnHTirQfGv/lra2CoYgpLv+U57E9cHFL2bwojjPeWphvpLWR0RggNxJkBlJ6en\nNMLbWbmWQfNYR5XO+NlBA44OewpYrGNZPVoZJ72MwtudT5mUhePTiqtXSNmgmEwVxksrLuwAOR9T\nmkTcU2zMxl9b3SyNNdS7kydjYzgelaQgz6K09z47GO23bFOPE7AfXGKMZKWig2lWggtJrz5Nl2Iz\npC67yRj3ouDT72/0i3uFnDXBAfwGTHXkDB69qpLdGQBb6lNJdwQuTDcRKQzBCOc9+Kuv9edIdisi\npnDjPJI749KZRsdiRviNt7h445967WYjJUk8EY79qG1YWsEkYiuSW2Zl3HkN6VVRaJtic3YVXTna\nwBqcVyvikDd04yatRF2GNBDcWe9Xy+/aV6fel+oo7Mk8bE7T5R7jH+RSsVn2u1u/H0+3uB/+aFSP\nuAcUFLpNte2V6tvbxQXNzE0bsqgEsfXHvXO1Yp880bTo9V1W5trhmtrtVZlK8qrL+YEf4om1fT5d\nPuLfUbgySWsrLCybiVX7Dpn1qcU7NHBd8n4MqtbTC7jXDEquGUe69fvQRnkNyV8UqsjneQTkD39T\n0NdMVZ0Juhld3lmbx7mWWea5RVVInGVTbxlvXp0pJqcl1PJ48swcytxg46+g7CnUdC3gXFYXzacs\nc0BWZMmIkjzL1oW4+WS2imDMRITGy46OBzT3TpAq0CfMQzSqkUaYbu/cVZZ3sVq5+YtY5EZ9oDZw\nCOck/wCKZoRM1eo6iumyabM0EkZmkWWRopcggIF6e/I9eKa/EOlyX+hWcdtcM6eKCnjDAk/NgE9m\n5IrlljOhaBaboradpEssqf8AUeII3JfOwccY/wAetZTV7hLu4IbGWbJZF4NU43bJci6kLJRFBOEc\nOGx+Zff/ADTS5juG0wTRbcZ2SED8x/4aaQkWGf8ATzJYuzFGClJgg7Doc+pzioRLsjujZtsJjCK7\njcRlgMf1qTspGrC47ZwkICud+IZZVJVg3XJ9sYFK71kvNRKbnQFyp4yAfb2rLdGkEai8U1rZyBT4\n8ZaLdu5IXrkde+c0NaXLW9rOLfaZJU8NiBywzzTxIye4S0vRUvpZReTQ2dtHjxJZGORnpgdSabX0\n+mWFo0Wm2cjGKLYly0uDJlgd20cDPNCWuh4Nei221iSHTryG4hMhu48Z3BduTnpQbQgHdLncy5Hb\nNPCNGnLsRGN2W2tjoSc0T5XjPHvkdquvCQZaDw4yXy7Med1eeQiIqOM+lKaz1hKhYx3DHa/G4/yn\nsf1/vVU0zRvsfhlJDcd6BqwDkAmZZcDIHpUxKdilfzE81jWVyxgAv5fckdKEdgrEZGCOeKBrJxf9\nRIqIGZ2YKAo5zTR9MNujLK6iYAEoP5cgEZP0oSYyR6VDZXEbKgYuoddy8H/x0qd/C38UjmAENvIV\nd09R7UjY6RZq9m1qrGNGa3lbekudy49yKEs3aGcSAgAjB3elFO0BqmQvgbefBGQ3mGTxt65oi0hi\neydm4mOCqEdRWk6QUtCJkeTT7eK4bbFEhKAnoM/0yTWfu03XGePqO9GDBIDm/NzyDxRukjZewseQ\nrjirfCX0o1HdJe3DH+aRj+9BSny81kaQLuGG96Z24xED7VmZB1qwOV9qv8AgZAzzmud+nRHwc6IC\n820LkqVbP3avVCT0tHwGa4kjuJX2hQ8fhsegPGP7da1lt8SK1hbN+FLJ4YUqp6EDv7966pq0csWU\nXHxGvgyJeoJiAcQrld2fXNILS/uEMeyVoY5XZow3O1fY1BRGky2XV4rXU4p7XxLho+XMx4YnvTvS\ndZg1jRL5biS3F0Cd0EnkWVeTgN1B9MVpxaFUkgPTLmzs9GvZzGwE34Xhy+bHrz3rPvE6xFYQk64P\nlRsMq/epJPtbFekNLtreCUTXMLSBUO1Gby59T605XUJ5LCdGd3eBAY1MQVef5RVmzBNhOp03+IWk\ngEtqS89s+d2OhxXNQ1gzXRa3Ci3eMbWUeYEjrzn3FT9DFsojige5kS9jkGcMk+7Gw46n+tOraS5t\n5i0N5abXA2tMCeRzx2HUmpyg6uJpTadE7rV5TavDPcW0qSKzCONPE3tn17UjuZZjqitHIqsiASMo\nyVbsAe/HakipespEb3erNDHd3irHFOFyMj8x/wA0rtNb/i8W26nVbhc43vhcdelZxcotDNF9vJDL\ne+Ebu2mSSLeCz+U56jPqKaaRpeqy2/zdrdW89nEzAx+JknHHH7VJcbghU9F15qN1sCB8kPhhEcv7\nj1FV/P6lHDbfLyiDcu9/FO7HmwOMcVVeIZBd7qF3L48nO22IYyAnHpk0jvBY6latHKfx4iZSYjhp\nPr7e1dMGgOwBj8q6FEAZlADbBj78UA1g99cBY0LM2SSaomIwiDTpdQRUgtSzWykHHU85JP70vVts\nzYXzL1xRUrYkngTpzBtVgVwSJH5Ud6t1YxLZx/LBghkZxn32/wBwaEn+VAXh9L0CQz/CmmNk8RKu\nR/7Tj+1Mrd9t9IhGC43qemR0xUX6IYv4tsF0PWptUhcRtdkIqrgAEjDEj3rI2zhZiOG3xmP1Ge37\nmtFbY0T0EnKPMD4qflZDjbim2kwQPcTX91KhMOJBG55lf+UY9MjJq6RbtgsuAHu5ZZCQ7nBOOM56\n8VC4s5ns9xeN1En83WmQpRby3NtcCWOUkq3l3c/1ou9tUudHUFNkrTlpUX3HUftQa+jJ/BE2mvZj\nfchwoI5HA59PeiIrGR5oVUPLD5iB0wQMkfXGKdywTqP10e7d7FJgrwrGFdmb+UsSTjOcjPatHZS3\nl1FJY3UafJykSQu2NquDnOPp1NQk0WiDfEEN9pelR2sHgzWqSeNIyktIDk/t0rK63aQwXEtzbRxm\nC7wbfnzDjL8ex4ocbpgmLbZ5djeVgynJH2xT+0kuv4a6xAyJEVY5GOtXkSCpLedNNS5khLKH8wQ5\nK5qFhc28Ej27WsjuVOD6+n3qPw2pml1GddW0h47G22ySDaznIztGOcfSsiZvBTYjksRtclMYPp/5\noRdYWl4U3jtBMJInLSKAeDk1VYN8rI+xsGUHDAchuwyaqjlkFLeJFr6+Jh0mh2ymQbjyB09PrTC6\nSK0T5fKSRr5SwPela0pxvARZonVE2N4IwrBVwT964s9n4oMizNHjDKMEimVjui/+H6bGX88z4AI8\n35iRnAwPeq7aaGFmCWwz2aRyefTAxTqToWSR1pnmfe/DHkgcCrTllXAA7daZCFEsLAjLY43Z6iir\n8B7aJt6M2MHA5PvWChU7GNsDkAVKPC4PORyKDAim6cgKBwCSSPWhWIlgbaSH9aAQ/TzJaxGcNiQD\nAPof0qz5oSXgaRskKqkEdhx3pXrwdF1yUEUX5mCsyrubop5FEWb5Y+KzMY8hFx1x2/SkYyZRHqjv\nFLZszhAxHtVZlWNSq4f0yKKVCyZJ5BfRNLKMGNVG1RztHTFEs+6zkljAcqQSE/Mn19q0vAxAo7uS\n/ESI6nGU59z70vvYmguTGzBivcHIow9BIClbdj0o3ThtuIeerirEvpVeDbdzJnOHPI+tAz42nH70\nV4Z+gZ/rTiEDwR6YxQbMi+2GJCB2FNLUb3C55K9655enRHweaBiLUstjoFwPvXq55el4+CCFGvJ2\n8yooO0u3TPOPrVt1NFpN0MmObacAB88+tdTlbo5kqVgdxr4aYTXbCVgceGD2q6TXrPUdGe3kgEE8\nbAwGPnC9wT70Hxv4JKYLCpU8cA570xtbNRaKwP4jglc0J4iV6GnVZ0tDHsgYxuEw6jLcdc1Vctbv\ncwkINqY3cfmz2z1qVfSqkW3JjW+VkXamFzHjg8AUxjsdP8BnuISuMuSkhFM9QPpRLYWcFvHcWV1M\nksykKGOdoPv1pZHcT6bOQ8ygDyMiAjPPX963XAxLPiLULnUb3wYz+DCq7YzwucDk1f8ADN89lqgf\nULaaW3YMGRG/LxwaTyJnG5WFQ30N+ZB4HhODhCRjcB0zVDmKECCVYYoJWDtIzElCuT+valY6TR74\nu1K3n02CzgiX5l5F5CYyuOv681nNftZbLUt1qu2FsbQjZwce/wDzmn41RnZ63tRDpLm5d4ZJJQiq\nR0AGSfbqBRum6s9hZPEhSaJuREwO0nHXHrTSjaFbaHuy91a2Sd5YrWN18yrw2O5NaTSbiOTSGtWg\njigSPYqggbcZ7n9a5ejot+hDrGhX2hWs0iCWSyu8Fj1wOwNJbeWG2kjkmcx7m2I23occU8Ua0w8Q\nW1+slx8x4m1N7gDAQ+/Y0viRWhlmt87ID53D9j0p4NvCc1RGf4ga2VY9Mj8EiMh5c5aTPf0HWlKS\nLLHlAN5PJAxmrxRztnNJ3W2rQ3Ew8sLmQr9BVU92fl5lYjyurIvtTyirsyl8PovwJfiT4UhV2I2X\nTxdPXzf3rUxkBvNgFW4rnl6CzCf6nNC15b+M58sJKAfWsyr20VvExc7SB+UDj600dHRz52FJytv+\nIh6sVB/TNME1WcpGgZCmcBcDirLB0rKbic3J3P5TjoMD71WJg0e1VHmOc45oMKRSlszOT4ZbnODR\nVtMsspQyRxFRnz0AhVxse1QmWCbDACMjnHqKIe0NnZi4lhUwuB5w3Q/b7UjdDeldvqUD2a23iFX3\nE7sE8emad6fcyzXEKuoMDEvleBgdRz6ipsZMhr0sWoiR4Vfx7dfCkiGAHVvyt+vH6VmtPT5XSJf4\njbyPACXiKjbt7MQfv09qeLSRpRbegeo6Fc2li91E0lxCxxHOucOPceoPeu6XbyXkcquGgm/lz0JH\nY0/e0JKFMaaPdsEl0+d/Da4ZVDMPygHnHrVywZvbhEkUi2cfiD+YE4JH65NJdBqwmSWfSYIBPdRk\nl/w/DHBByQSO9Uyvb3HiBLLbKw3bg5wMnng1k09Q1OtF17ZTsGmIgVYgAW/KxH070suXaBlWErIC\n25WB7niqRZzzVFl7AzLE6oNwUKWU9SDRMkMkuniMofzbjjtRBAlaxBbORN6xbSHUf7j0/vRFvBDd\n3K+I/gDYd0jc5ajdFURtbJXmba3AB2546Ch7S5TcMqFG45FFGkSDjLBACCT2opG2RHhM4zgnpTEy\nl51DgId30qEk42dTRCDMyySEFsHGagZdi+bPtQZkRyJPOe3Q0JIuydfLhRyc9T9KUI5hv2fRDAso\n8BnyEYD9vSl8xaM5UZIPeska2G27LJYs0qoG6DPP3qxIZbaZ5mAO5VZTnjn1pGxkCyIDG9wqkIDk\nkcj6VFcyQIwygLY4OT+lG8Awi6gjtDB4MskhcFm4xgdAP70u8SdMtBM6qRscr3PvRRjkMLFtqON2\nCQoznNXX0LfKQOsZimwd6N3HqKKNQoMjHrx061rfg21ikjvdRuQPAs4z23ZY8L+9UliEjrM5cufm\n5c4DbyDj1zUY7Ca8V2gjaQoMkLzWUklbBLGVJpszyhCjISPLlDzyKZXNsYLhoI0YZ4VSeak+VNmX\ntF7WL2M4SV0LtGrkLztz2PvRFu5R8+wxU5SvUdMVhofhtoG1Vop2Kuxygx1ODXqjL0qhPaQR20Hz\nQjMat0dgMYx796R3krKzOp4Y5HHOK6ktOaTyhc87yZBzg8VfEijH5mY9cDiqeIgzUWWnJMgEc8cZ\njQMxkXAOewqVxAIpgMh9v+xuOag3boyFV1HJJcvnaMc7mOK5A8kTkl43b+VQ/ejWBG17qiTLaboW\njfw8seoODj+1duNcVt0SAbpBjjpSKNlCBiVdPupGeWN1QBFA3bj/AGHvQpWW5tFleNi7tsyG74/+\nqN/A1Q3sprWPXrr5qKVwWVExF4i5GAd1U61czWtxMlvZrFG8pG8qcgYGAM1F+jXSsv0hkMQkv2RS\nT+GidfvQOtTrdaarQwhpA27eW7+mKWCcmZTwK1BXefx5mX8WGPLoAGBx2PUc+le0GOIakLgzpK9s\nhzHdchh6r6kY71eqDdirWbe8khtwSXBG9sDBLMc/0wPtQUQkt2zKpyDxuUjmqx8Ek9NTpN1Fqdg8\nTkiRiqs+PyKM5Pv2q5keW7YWsU72qJtzNhSxx+bH1qMkk8Kx1aR1vV5P4FNpt1dTSIpHgLG/THXO\naRWA8e3EgtzJGjkFQ24/XFLFNKxU6Y4ntdS0jTUudpW3nfaqMBjJHJK/rzSWaOeO1K+aONvMQMgO\nR7960KJ8jF3LcnPpmoxMIUHUk88mumKwgy+z5mdipIVeV9uKClCzyFdp8y9uvWszId6DrZ0eB7ea\nRktp33q2PyMMD/n0rWwatK23ZIJc483XP6Vy8lozwSfHJh1OK2vRKPwT4UqemeQf+elZCZE5WJnY\nJ2PY96pwu0GLLYoQkUb7sHniiISWTcMKc1Rl0NNFsI5ZxNLh44GUMN/m57gHrioTeHJeSsvl3SHH\noffFIm7GaPGWC7dEG2KTaQWJwuRQNxA/iGREDbF5CjIx/iiCgyzMoi8ddqRxElgeduBn/NModesr\nx5Y76B47d4miXwz0JHB59D/Ws1fgPBBBatLceGiyAqWaSMgZCqMnB9hWu08TRxpC4CpEBHlgMKSc\nfrzSSxFeNfS6SaK3spNQtnjmWCQQTJjOUPQnuMHnNQ+ItQf5WNJYopI4MTSGJRiSNxg59eoqcXtF\nXpmrW8awn8JGmktI2yIWGAFIz0rRDRYbm1N3YTeYg7VYdMrx+h/tTyX0nF/GBa4La0+ELdbmPZdT\nlGYI3UevseelE2ekQzmNQrgpBlpkfKjOduR68YpWn1Nl0cXUvn7cROvgGAeEuUwyenXtihZbqGKZ\nnlmG0vksQfXrQiqB2Eet3JnndYcCInGMZ3e+e3rQMCMsoVxg9QRXUvDll6HL+HJbq5IAyTn+tTnl\neMgoxMR/MQe3ajRosOt0e/0m8mQiJraNXHPB7H+lLFkYbt5by9T60q9KvwviuMSBXYBX9aqLLJdG\nNRjHGQOtUFLZ3Fu2GBAHUe1VyXcbhggx2z3rABg7rO5DAELnnvUd5LjJY55INYyLY0dwzhfKp5Y1\n3xFLbWIb04xSjBMEKysUUEswxtH0om+tIYdNtLyRd8jqYpEby7WA6/cYoNhrCUlkZbK1mt7Rljmj\nLgZzyPrSSUTI/Iy2cECimgNBVn4klwECZOCdqjk4HP7U+t4pHto4oCzWzqHJC5xjgnH/ADrUuRhi\nTS4tX0iDT0/DZmKyFDhSueM57jsaBaxT5y4i3lY45SFOc8Ack0kWyiiVxtbm2DQc4bG5upHpQ9lc\nJBJIjMjKz5OR0xVUKwu4soXnintmPjFiSU6ZPSl/xOsn/qMxSODJFEiNjtxWg9pgfli2RFkYF1z6\nmm3wzq8mhanG0OHimbZPFIPKyHH71aWonHHYZq3w3Yr8ShomZ9OvAZInV8FPUZPBx+9PNI0Cx0f5\naeyvDcSPISk68J0HlcHp3rk5ZPpQOR2yGsRtdXp8HEaBy+044PcZ9KV2lp4moySzFEmD7o1AL7j7\nnPFQ7VESL+lGq+bVJN0gmlBG541AAHoRj1oaLqo5zkZq0P6nbHTTaHEH19edpUDB+oxXqnJ6UM1e\n3/jolv8ALmGNFVUUsTgjqaT3Thx5Aea70jibBkgJPAZs+1MLawwUlOcKOwoSeCsPjZxliSu3BA2k\njj+1NB5ooWVFbcHZwBgMT0xUXJegQJeQ3RsmaNQ0CNhgFyVI7nvilsVxCpVntopSeh3sP6GmhJSQ\nzVMfQTWctqqTW+yUIfD/ABCQeTxil66Q99bSTQwtG0b5Yt+UDvQumOwNbv5VZTLK28jbs9QO30q2\n1vPHVI2UwoQXQKSME8UX+xE3Y0R4Yrq7SW1kuGUlUeOTDR4747jtTG71OO7sQVWQgbN6vzjAwQB1\nJ+9c0k3qK/4TXT7bUnhFoY1WPzujMVfAPI/Tn7VHU49H0/5ayhBkfzeJIjdff7e1CCaGxCG8kKOj\n3Sv4ZiKJg4yA2f70TpOoWK3AbaySBDuZlDhQB19Ont2roStCt0wbVPiVHv4JoZpGc8kDAGc8D9Kt\nX4jF8xhlhMspbjgE579PaqeIFphmt6vDZWlnawxGC8KZLIuAM8j9sVTol9cNI7zu7I4KnccgH1FJ\n8HugcJGsrSXUEk8jkKis2Bju9SliW0cjTjJGCvnDjpzmhFGk1WEm1vUEsmt5ZZGQjlGPAFLbrUby\n9jj8SRpliGxFJztFOoROeTK433PteM4x2qRRTGSAoxwB1p0LRdp8BuJ3VMswhcsq9gB1qWiaK+qr\n40UiRKT4YyDn161OUqDQ3n/0zukgzBqEczSc7GBGSPf9ahEkXwjNJKwl8O+00Og27gkvcfrn9a55\nT7ugNGLtJ5lyjuXRz5lP1pr4YKAqDk10xVeC+Hvlt5AVtre/SiIbRVuEhuA6YPPHNCTLw1E7YwwX\nzDcGjGQWHWr7m1VIY5I1cNIxwW6GkLAUKO7qH8ozzn9qbGJ4Gk8OTZhNrsOQ2ecUzABFQunywK+d\n7AnB9aA+Xbx9pDElsYNNFUJLQqK1mt83EZGFJU55471qrG5tn021t0kDvJIsjYHOME9T6f2qXKW4\nngi02G+s2CRxlRNKQ5c5DjONp/etTDbQOLP8MNCqCJoxnzIW4H/7JH6Gkk/0NGIInw/JqOo3MMMw\njAUBbiVcRnj6fak5hutG18wTnxBHAxVYySkg2nOP3qSkCS0F16NZLe0t4Hdooo1YE98k5zRCajcW\nsUFsWYwsq72R8eblgM/SulJuJPt+Ry2uY9Rt7tpZrqC4D5hQjyuPrS+6muZrRonlXw4/Lh15xWil\nZKbaAlUSwHaThV8v1FVxz7HyCD610E/hebwTIN2OPTriulFdPIzc9Oaxlgy0pwulXNvdRum9wwCj\nl1zzg/alJkLMcMQo8vmHP3pKdlrVHpXV2Vl52CvLcEOZGxnrzTil13ei7lLyRKCQAR9qCeYQyZY8\nN0FYDCtLuIX1aE3CLJAzbX3HbjPfP1oueBopp4sJhHOxwwORQ2wqqKkUlCueTzxQkAEk7h2xgcbq\nBgqVWsrW3uVyHeRtpGegC/3NaVi178MW14FkBW6VX8QAjJBGf2FLIoj0MyalD4dpI4aIN4dtnIIP\nJKe56kfek0kP452si7jkqR0oL0S7I2QX+IeHbnzluZGfGz6fvWjt08G3txHG0lsQQtxE5DJgnkj0\nI7VLlBfwzvi4nKNHv3eROeQc0XqcMmk27xTqYzt2DAz16UUvCsfBIt6suItvbHAx0qNtG/iyZj2K\nAfMw/NVqoV6GfD6t4908gZnjXbCyHBU45yPQA/tQms3ck+omaeQSSuq7mxjOAKEUuxniojbLvhdw\nueOwrtn4fzcRmd40DZ3Iu4j7d+lWfhI+kWmofDMXwxbRTSPNaJL4RaWEcORuPuCMZ4pM/wAT6IdS\nmezhurgz4jlD4AbB4kCrjJ/tXHOLaA036NBDDdSzLGxxFzllwDSGa7W3vFjFvCCW8smCV7n146Vz\nJWqMo28CdRv4LrS7C+lZYI7pfCkZB5UYdMj0PP6UnlspbO8CTjbkgqQcqw9Qe4qvH4dkXlDqC4+S\nu5Z1BLBVKe5Br1Z+lEZedDtDHOWXqTSyYEsFGcAdADzXdZwBemiSCTxG3LEAS3AOf1rQaa7Gbxo7\naO4gaPGJlAI9cY74pJ6qMWay4ivzHAoih25AHUgjvXpNM1Ca2SC1jKySKNrKPyiueupktKpdNv7W\nyi2Sp46dEE3nOepK96AuNJnmxMYlTzBWVFxtH+7Hpxz9aeE1TpUU62H2sELNlJOUG3OcEnsP2q6C\n8v11BIfl5HskkDSgDOR3z7YpX6NJAer6a9trrXAiNz4jb4REu5VUk43e+KFSykutSs5btTZweJvc\nspwp5OCOvIp28JNaajUGMfjvEI0iuGIRxgbx60v0fVkW4eO4W1igB2sznaxb1HGPSp1mGUtGGpyI\ns8Rtni3ON+VO4sn1pFc2balqQaBovmAvlUnAx29qWLwozuq6Jqk+hwSzeGgtWaPGfMxNJI1/hiHx\nZt00yeZQmcr7/erwkK1elNvpkeo7zHcQwyou4Rudu76Z4p9omjHTka8u0CtIPIVkHlA6n7k4ppyN\nFBUfwjfazO800yJlsr4rnOa6/wAI3GmXEV05DLGQZNrkox9DxmoLnjdDsEmuYrS8liuR8wTyDG2F\nU56CvXt1DcOJbOMqGyCvU1WNMRsRTzuPzcdqsspAuc/YetWokwvAySBk56+1VeGSpQd24FKwobaM\nUju2h6tMyRlh5WCk4INcsQ+nwrDENu2U7g/Un0qb9odrDafDuovdy+FIq+QbkUHkY5NZX4luptVM\nazRMjQO/CjIIzU+tPCbRkb+0FpdCMFo2A4DJg16P5yF1ZQsqn0yR96umDq2N7aBp2A2spJxyMYNM\n2tPw1trlBNcyElJQxyRjpj2FJL9lIJrAGPToVn2Rzx/mAwQQR9aY3NjLAgXxw+3zLt6dKRM6KwAW\nOVJcybRkeUdRU4gZ2aPdj+bjuc1QU58rtyAeS3SrpVjt4N9yrbgeQvX6itYGsKNMuuGRyqK0uQ0i\n5wv2/wCda0ltHby3LrZywMjnghcP/wDL7VOaNxyRVFcGE/8A6SMkksU+0MqcccAkVLSL2a11eG2l\ndJUhkLoqZBAb1OPRv2qTWF7Nj/Eo0s1O5FUxgsFP5eOlYz401CW71Kx+V3y+DAys6JnlwR/YVyRv\n/p/grQgvBqGFimtsKgAUDykk881xLZVv18WWRdv4m0Dcu4Bu33r0uyqiDg7sbXlzc3TW3zUawxtj\nYVTauAO9Z67czzyoZUkcHhl6Ghx1YOQWxTyRFsEDByM115mdQdqKf/aKuSRZbJMYxOjqhzs5ohFm\nMn4hDEdSBWCESTsqKCVJYkEEc9KEmdEtX8RDuP5WGAB9aw/wFkBTHuO1eQkrg+n60TBi3aQwgCGN\n3b+ZlzxVU7iZCwhVQDnIFYIA7OxwGHrhhTC6uYmS3kiREcw5kCngnOM1mwI9DM2/zFvMOCBRg0S5\nv4XltSg2pllLcgetL2rTBtxp/wA1bWcTSM3gwE4TjcSx6euOKb6I5+Rv9Hd3VUt2mYtg7SpySPoD\nUJyvwqvRFp7yoRJGSpXDiQLjaR0PtWhutIj1S0tdVu5Ws4zuhnKxndIwJKsgxyG7/ShOXVAhmHtJ\nsNM1m1u9Lt42g1W3QyxAn/vY5/xx7/Wo/CFyJNcurO9l8NLq38MhuPxDjj69aT+ytiMTataPa65L\nDKCPDlZRgdOeP2xXL/8AFjlSTc5V+pOaqvg68Fq6evzIaNyBjpTa0024v4Ggh3Mx5xjOcc08pfsC\nwlFZNbRO6EMwz4ingkn0rLX0z3F3l8ZHFNxq9ElK8DbdjHAq8qp/MaoVsSHafLu71YQfavN4PwJb\nW7xSRNPeGZGYYDLtIOP1A+9Jfh428Gowz3YM6q2BbIxBc+/GMf4qL8Y088N9d3kIuZWu/EhUxiVk\ngPAVh5eD26D7Vj7u+32MCyp4ckqk7k9iRiuKCph4mmiqG6Y6TFbtzEkhOD2yKZaTqUKqLLUwxs2x\ntlHLQHPBX2z1FW69fDoiOprOZL0RBQ7Mhddp4dfUGvVNlkJNVjhjtrF4yAJISWPvvI/tQttMEmjO\nxXB9fQda67w4GG3VxC84aGEJETgIORzRUemNgfjwxptIUuxAzkc1Nv4BC63W5utRS3kKbg5Rj2OD\nkkGmV/fa1YQPqFoWhtZZRCcqDsA6AE+o70kmosoodkUCwiitPm7uaXM5zGzg+fuB9f2ptEjanfwy\nRM6CCM/oR5h+lK3elIw+AGq6QkMv/QoZEUiVcnJHfn9P3oa01BNP0qTx2muZ3P5C+2NPqep+1NH8\ngzSiwaS+kurd7tppYpPy7I3x0749MYrsEzR6ZDczzyyOZJAi7s7sgcnPbginccJr0ZarrFrqscMY\ntzBLCefN5MEjgfals6QSW6QzRvKi5BVWxz6/WjFUib9DreeNdMjt4mDSwrjew8wU/wAtUtATKCqq\nGCYZwecjmkS0oh5qEtxF8P8Az9sd3gMA4OPMDgdKx9/eaffWLrNMbe8Awp8MFH9BwMitHQeFWk6e\n6kXd5GslmW2Ap3bg4/etJ8MGXW9ee31GJfDiBa1Vh5MDPlJ/50pp+My8NcutRwzvbpaxyyxMVKs4\nVRjqcmkmo/GbfxVYrWOFEUjeq8gkfsa4Y8dvTNmb1dVuZDJDC8byOST1z9h0pVcSmIR7HIYLjbjB\n9K9DjjSJtg5kDYLqDnvV0aq+NvbniqgDxsDrtOQ3eiJgBCWCcjnC9ftSNjJASTMzvLg7gBy3WiBf\nz3Uzl2DuTvIVM49zih/oSdhqbW9yswmcrkjAJGf0qx7ozXLyglyx3E9DWqxWV3GqwyFjf6eJctks\nJCr/AGJzR82nCTQRfafJGu+LISQqpJBPQ+vbp2pJJorCmILG7vnu8SOJFQ7mR8DOOvTmtZYmK/gV\nzM9rKzZjEwY+G3oG9KLqhad6RutLiRZJZBsYjLS54oa1gZ3W3jm3iRgAfWpIskUS2rLeNbHopxu9\nTV0WmSWzbkVizkjgbsAY/wA1S8NQZfXtqkPhQWv4yNhnbq3bgUq1Cyub+18S1jO6HzsCeo6VosWS\ntCrTbgz3S+FIkTRjzMw64zRujxvOXmaUCTBYqzE5Ht707RBYN3mjubOLztG0QzkDG4e9XJcW8VxG\nzyRsojwxOPM3aoyR0Rkg1r4NYHcQm/IYlRzk8dKRvCvjh5nkYbjlZBhW+hBpIqmGUtJx7HnzGQIy\nSyo3O3P1qy50hJT4iP59uMAginFuxK66gLkW4aSSFTtGRkLnrj9aEjspUld2jdcOfzDH9TTxpEGX\nnT1D+IQNx7dQfvQksar5sqCXwAF4qqYpOydRHLHkYR8nNEMy7m2kn/4miYCeRzN5hJnPTaTVs9hc\nTzovy58MAEHr15rWVSbLW0bUGIEdq5xx0phZfD13AyzXMaAIreX68UHKgqP7Ov8AB10IEaGSPAHJ\ndsUOPh+6jUIwExPO2Nif7UvcbqDS6JFFbeLMkqKWPmY4XPpRSR6fBbK15GQqq0alBkg5JH1H+azk\n2KkApdRyh7eOTxUfAbK7SMdMVdZXc1mztEWhY5ywbGR/90GvjCl9NNGXcJLdtk4QI7Dcoz0JPaiU\njhttTNyGSeKRJYHMbZyNuC33zU3g1lmm2droi7tRYNA0XiRmJcow4GCPbI+9U2esSapPLbTEGKYG\nKMkDYrj8mB29PvU03Jmj6ZL+K3OmfFXztsPCmhkAKDtjhhz96e/FN7b3EunT2sbQyyO1ywIxwWOM\nevcU0o/om9bK9eum1GZpZY/ClVVZ4xzyAOc/pQhuoWgd4sklzwV4xVF4OsQMrb+V2jB5PNM9FvPl\nNRVfEVGmBQZGckjH260kmKhXdXUkbEb8MrEEZ/WkEvM5IHU9qtxeE5BM0jK4C5wAM1PT7Z7y7it1\nILyMEx75/wDurtoyVj746nOoaxHaWgJtdOiEEahs89z/AM9KzGnTG3v4m3BBvCksudvOM/Xk1NLA\nz3Bxe/Esk2rLIoBRIxbkheHQcCqdQYSwWLBvKI2xhdv8xqModXYOONMpjb8NcdzTG0QD8RhwrA4I\n6mlkdMTc6LNLEUSWMMp5UsvKcjp7c16ossjIXtl83ZWm0lBCGTPZhnd/eg9J0RtQ1cRRM5EYLO54\nRAO5NdClhxqLbHNpHYwXfgyRSyZYENvAA+lNTpcWsaobKNUCmNWRS30z9zipXtlHBfA3TvhmCNmS\n0tpPHeVgCwOcdyP0qXxev/6Fs99uGdXO+NgQJNp4z6c4qU32ejJUZe0mkTV0udWWKYBxmISkhe4x\n6AcVqoZ0MMsIjE28FtyHpzkfYd6dpofsJorjw76VZGjg8RHZBGMqSOcj24P61mJLUDUJl2t5ZMgH\nykqeR+xqnG0ifJo2i8MWzKAgSIFuTyCRjr+lCNHDdww3EgXyBgmw8e39KOsVUFxuDpk9pMS6yyeP\nG4YZ3en0qq3tY5WAMyxgvlWYdMdf7frVEqQlWx1rGn28U1pJax4neAJcAZwp45x9KGt9JS8lhjW4\nG7eA4YEYXGTzjrxUuxWqZK9WeOO5hVYzDMjsiqxOADxms5rfwwbRLB5JlU3KbyUO4AD1INNB0LKi\n+6u4haQ2qyKIbUEIsYB3Me5xirNJvJYoze2UxWa24lBTnaeAQOQR60z8F+UehuHkuWMrMfE4dn6m\nqHGydlCswTuaXpQiL4bSWR8SNtRl5yD0+tXSaJYwp5FcnHGWqidCsW3OjhkLowCjoM80CVe0JeIM\nWA6EcYprMg+xX5kDgZHTmndjbhpNsqM6EEYGc0k3Q6E6vNLcYkUSBRhdq44969JcGzimWOMeI4C7\n0bBA5yvvnit+glFlA9vFHPNE6wlvMSp2/qK0/wAIfDVpr+oSpdq8UJGYWQYz9M9cZFCUqClZbqnw\nFfaRO8UTideHIII47jniqLDSDsW3inRDMv8A3Np2AjnGO5wOtRfJaHUVHQ29ubGGH8Bba6mkUG4d\nQcMQO3pn2rOya6vz0NwdOjiSLh1jyviD39akm16xZy3DR297a30HiGJkt5VJaEENtI6deKASMxRr\ncR7bdQeCy45PQCqq6LR8DIdPM0ZD7YEPDXEjjbn24oB2ktrdxBcymIOq8jDMSM/XFFNtmaKrloYL\n+yjty06MczFhlgcc4PpRTz5j+Y021lGxWV9+Cpzx1FN9Qplzo8ljZtcTupZpATGOOO/P1NTsUaa7\nae1BjRcdDuwelU7XpBxp0TWSdbgq7liZMMpPVTxWhWxhW2CTQqyxAKoYYzznOaWW+DcfulcrQzWK\n27+RA5UA845obU7SPTmitpSXiMe5HZSOpIqY8lgIWm+VitoAGZyFUJycUetsLaFVWV/G3eZXGMD1\noTdYiL0FvLWc6cjKkvjtIDuU8Ff81x1tbiF31WN3YDiSNwGAxwDTpjKOHNMuUbRIlni/DBI3gnkd\niaW6veIIAiKAPEGCTgVSLFaI+AlvMQxASdMFs5Geo5qVsoiuly2RtyzLTi0OrW+sIjKbhZXMiBDj\njj/NVfP2VtIgtbxym78kiDy/vSbZ0J0h1b3cxjVklUluQdy4x+tXLLcTowa6CAcYTDZrNC9r9KJL\naaRkVHuJuDwxwB9quEM8ahUjlXA3EE7e3fB6UjGQDFLot9dNYrCjiSLIcg7WfuBn+tJJtHa+hyoE\nUcUjJvfhQR0Gaa2vQpWEWWiaZZWzpPdySzABc2yhsH0BJyaPg+F3vIUETtEjHhbmMKx9cHPP0qcp\nP02JYFa78Q22mJ8vp3is64tjbyufDAxjOwfm6fvSexuQZHuGkjjMDo3gqNu/nsKCToS9PatfXN9H\nJIiNEkSlG2ngEkH7Un0WX5OZzI0hKNlFUk+cdKpFfjRm2mcvbma51p7m5iVJHcO6Y2gn/wA00zLd\nJb/OwsfA8sUm/GB/t/eswL0vufHkvsInhO6c7umMY/pS6MAJIG5YyEEGsgtsJtbZYbY3V1xCrbY0\nU8ytjp9B61K2u4BP49xA0yDoFYpz25AqMkwL0V3EaGd9+9G3HAalyRmW7VFwSWH9a6uPwm/Qm2g8\na5CswBwevTjmtloz6PJcyapHbzi+tYHmmhAAhAVeCPc8fetNu8Kwr6YWS7ZZnkyd7MWY+prkd/si\nKvDHKDx5k5P3H2/WqfBPujzXNH0+yv7S0WaOK5jtlaRM5BY87c9mwaC1NWhtrFCrIDE2AR/7j/ap\nPUUSohb8hQMfX0oy9MsNoCoIDngg/v7VKXo6Hfwlrd3qV9DazK0i2sZPiKeQu5ck+uK9U5Rd4Uix\nnb6Q99aNPAAIdplwWwQMeYfb+lUrbw2+nFpJtovFBkC/yoDxz7k0W9onFWQhWxt5zJcyOvl8p9+1\nE3NwkUMUdqjfMpKsqy+gHQUqUrM6RrdPn/iNlFm4a2ukBAkVsHn61nviCDULmVbfUN01vbsCZ0OA\n3sT3ozj9QEI00B11B5WWVkdiVVVwMnoPuSBVlldPYXLhraRXTMcgBOfpmh2t0GkHw3VrcqyNbvby\ngsEMhBVMjt3rt3p8SwPJOPEeNQitnO7jik5JdVYX4I7yG3srHzPMHbhjjEf0B9aomCpZwxxPIihS\nd2M5qsNimTTBJL7xFijYKfCUJnHLck/1NNNDtpNR+IYLNfKHyxJ5GF5IqjeGgrYfqE11fao5tY9z\nO4RFTrx2z68VKW22WIjuQ9rdz5XbN3J6Djp0pMSK1oui0rU4YUjUI4DlXEcobIYGlVmVmuFt7gCB\nkO0nHB/WqwmpfCc40rGsOj2sSkR3C7S2cFc11LiytJg6FGwNjAKBuHUj70JKyKbF9zNZxSv4LNHD\nIS8ackj261BJTIokDYwMEEHOKMVgH6Ww3j7Su7OB1o5FNwGlIYrHtywI96Jqs9dlApAA69vSq4dE\n8VX3MxYgFV5wc9Bn1pJSpDccbenF09rC6WN7Oe1DHH4p6k/YcUxfVbeDaluDlYznzDqevI980kW5\nrSk0k8EThHOI5FjOOcck1VHYzTeZLaaSPdt8RUJGT6ce1V7KK0SrNT8LF7DfJeRg6eqZeRoy4DdN\nvPQ1K21p7/XGNjcPGygiJUIXw09z0NQeytFkqWjTXfiq4+VFrJeK8LYEkkYyw/frWTb4gNzdFTI0\ncOQgVQM4HfGetbjg36hZOKxFEpigcNbSNsDEguMEj3FDTmW4A3ruAP8AKOKaXD2abI2VZbKxBHGD\nkKB3+lGW4u4mRbiNyWbK7+AKskkh4tjGbUbyMxW8jF1wQu/zBQfT0oK4knJVnl2452qKFIpYSlnD\neQxLKJIXOR4mSQft6j2oyQrBdW0fhyIpVYC0bjbID/MR696STHQNqFvaw3clkrJKMlGYnp96F06C\n0sHnRS8uxwUdZBt6dxSJvq0K0m7BtecLrcVwyqFmUL+Hjr0zxR3yt1Gq7GkuQEyozkH/AM1VeaRf\nuBEUtxYRxlrcQNNyHljDEY6jHSqNdeW4spbkTzSrG6kkt+UHjGKRay3b8aZdotxaW1lE9wzCTJCt\n1JP9qF1l9jJcx5EZYAD1PelafeyWOOF9vdidSkbFJEHQn830otoIr6KN7uCHxQ+Ny9H46EUZWU46\na0KeMWVvCttbwu/Qoy+Ur6Vg/iWaCW82W8bxbCS6MchTnoKpxsSdfAqO3uLy0tUVdqnkLnrVssYt\niAy4bHIzT/SSB0u5ADGreU+b0xQ2q37XroCqqyDnaMbqpQ9ldtK0YODjPWmbXt0sCzJvjQnZuGdp\nP19aRgCtC1Ob5hprm5kaMOsQUserZ5x9v3raTY06G6kUlpC23eOQvHlH6EVOTplYaKrH4PmN9az7\npZISoaQg8gnOMD06c0O9i93bjTmUJ4kjOW3EZcEjcR06Y4pJSbY1JA8kSQXSxosTTMNqyxtuKt34\n6D/zUphBPcCKy+bkIYZaVtpDHqcishKti2XR7KHVlM7XDMWBbbja5/mBI+1EHTrZry7EsQtWWUBU\nAO0KQOh9c9absvA9K0Dv76eytmsofD8J1Ido0J8Xnqc1ZoWmRtZveS3USGGQl4pMjAHQ/qcUXiFX\noVbi3vbu5siI8zriGbbwJF5Xn0PI+4oKOWHcbd2l3IQXyMbT3H2pY+6M/wBhobxbWTcpLqwCHPVa\n9pWlG71kQyfhQA+ZzznJGAPfrT3QKsq1KFZ7mQopRBnbH12bex9KGtWltozGVGJeSp5DfWtVivGK\nr64V5JH27ck8DpmqLFsXnicDZGzAn1wcfuKrBYIzVfCUOiTaPe/xKcQ3u7ETufKqgdfuc/pQsVxJ\nbfCeov5UN46Q7hnoNxOPbj96FO9GTMsx68HuTjt9aa/DVrbvqguLzAtLP/qJBkZYLyF+5AH61Ri/\nQZbO+1jUpNRmjEMUztKHlbw1wSeBnr9qaXPy8WlabHcMWXwnbyqc/mIBFRnLxDxX0FthGpyhYhBk\n5FRN4pjZBPw/5lZc0iVsq3RrPgu0tVsp2YHxZPw2mVclUOcj9K9UZN2WilQ/+H2bwG8STZDOmEU8\nEDuR6f3oTVdHmICbSYgqhJgwyFJGRjrxgUva2SjhO6+GrGwsrq8vXdt3lgVm2mT04pFbXtzE0qC3\nQtKmwsW/IPanUmTk1Y80CFI7VXvnKQtuWNy2SGA/80VHLPDdz2zyl0VeWXo4rRlY1WBz3MgBaOVo\n0HRMdCOhpPJO0MrSzSIBkMy7Tvf71TqmLQbdfENtq8LRtp6iXnEkb4YfaqEu1toTJLKXDJglV4H1\n96SXHeBE+qa3DOqO+9o1yu09CexxVDX4MZRZAdo27CeneqRg0qJpnLOJb+9FukZc4yWVuK0Fhb3W\nktPq+VjFsvhecAkswx0z6UHipjxWgdpdPBctNHIkcqZKlvU9xUNX16e7ETBl8SM4DoDlsDqTWUEx\nnKhQZz89DK83hyoVlXc3f0rS6jpq6pc2yQgJLM26STaSVZug9Mcg5rSx4BLsKb7SZ9OuTA8xLoMM\nytwT7cUkmgnViGlJHcYANUjJMlKNDnR7e01TR5bE2zzX0GZo2VlBC++cd+wzQ9vKIkI2lmz0YGkT\ndtClsZZm4hGAD7U302a2ldLfeBvC7yRgEjPehyOo2Mhfq083z3mjWNAAoCHO49zWm+GdTsDZStKb\nX5xIlWOJ2wzkZOceuT69qi7nCx4umK/iWeP5pEjd5GQje0j7ixxzz9wKVyXkdzqHipD4MbMPw1PA\n45q/HGkaTti9dySOPN5TgH1ppZa9c2Nq1vDjacsSeoozipKgJ0G6T8Q2EkE1tq10/wAuX8Xwwchm\n9TRdjd/CtzNO7ymFF4CxuU38Z5/X9q59i8OiNNaKdQu7XUZLYq6W+7KSeG2VRV4DfUgZ+9J8NH4s\nkUrSLFJgAgjeD3FXhL4SnH6GG9tvDKGPew8pNVpcQvJtAAJPCg9TVSCRfHK0V45t2ZXQ4GU70wL6\nhe3axOjPJkgeXpStFIj7Tf8AT2+v0d778Nd3kA6mtTa/6f28UShgCyjGaFWMnQxT4MsVUbo1YgYy\nasPwdp5AHgoMdMLQ6h7MHHwLYckxKxznO2hbv/TuxlhKwRCJuu5azjYLFV3/AKaLcWD286RtnBEi\nDDg/X6UsGlXWkWSWfycjXKw+F4yp5SPXPrU5QY1J6KdY0i7i0FJZLkmVGASLYd2CeTmk1/ZyWtoJ\nOGEjbDxjb9aEXWCyTqxWqXCwu0eMIcnHb3oy4lm1OK3jUhViXIGcAknrVmiEZVgyttMguo9wkPjo\nw2qufvTl9OmNulurmZnfhgNuP19KjN0dPGDa7IAkbRnbMgMbLEcfl7/cVidVgCakRKsihhk5PJp+\nPGhZ0PdJ02S2vrYZZllR2ifOVYhTx+1AHxrtVmPBKkEv2Of8U8XciVEH0iQ+bxAQRnpS2aIi4IOc\nAYq1mLo4m7BsfSimuLgacbVnYQBt4Q/7vX1qcqGSL/huA308tuVHhko7uo8ygHsema2Gl+HYx3ln\ncSic3su2Nc5bO3jJ9elTkykfDS2xCWaGZpIUMWwoDtOAOm7t6VlJ0uLTSjLFJIryKyOrEZHP5v0P\nWot2x6ESN/1RkXEayDJHbdjr9+tONEu4ZLlrYqFYQnwSV6vnj+9NJ0hQeLRYY7+OSW8kLHLSADJB\n9qne3lnrVpcWVvPK15Yv40eePGXA3A/Tn9KjxSlyO6DJ0jPwXLKxjQb92cbxn/6o75tprGXTWjjj\nWRjIWUEsSBwv06V2PREL7aGVcuY/KvOc8ind9bi+ij1JISGcgXe3ACvwAfoRk/WhYQSJyUVUDFic\nYK4JFWmae4hilIZ47eYKy54GOc4pWZE9Uhjs9eu8Fysil8KeCrjOetK7q8/EQQLtVVwo65/rTx8F\nkKrkSS+YoABzUI42+XaVMBNwRs+/P9qqnRNkY1DEAgfQ0zed30+K0dyIYiTjHQ0WrAmF6HCNP1uG\nS4gaS3nQrIpj3Agjr+taZdK0VrubT4bSRIZE+ZllcELEApxx1POT171DklTwKszWvab4sh1Se68L\nxEBtrMxsp2AbQeTgA4z96X6u4kstKx//AGpBx672oXdBTsgqNFpBuMgb28PHrwT/AIpfDgv52xim\ngvSjNloME15atFbyEI7LvcD07Z+9erlk3bOqKVDeG8FxL4ckhWIjI8nQ4z+lMv4ppmj28V3dyxs8\nxwp53N749BW614QTQR8QB9QtYbxSpj2g7t35vTFZeO1Mtz4SFVMhxuOMU8XhPrbwK1K8FvAtvJmM\ngYBG059wM+1XzTwyfDNjc2d0ZSGaOQkYJGTjj2rRTrCrpCq4v4bsxLCxjfGGz0+tCZk8QiIeJs/N\nJng/4q3gidlxyEM7sWMYyxBzx0x/T9aqVw9rIQW8qhtuOufWkXphJKY7i4VABjHIFcdI8mMBG3Hz\nsBzirJk2e0qZ9F1UTIdzDgKen3p/Pf3WoEt4xxNy4i6Z+hoSSfplJopMXhAKzbmzgM455qqCNVtp\nBwTuyNwpVgbBpXs2bfJbo7L5e/HeiIdZY5ijEsjngDeTtAHUU/votkRd3cxX5qaSRUBCJnO360Tb\naf8AOuFLJCgx4jsQdoz1pWlFYZMWX6R2F9JDC3ieGTh9m3I9fuKksa+FlVHIyPahHRX6XRzqWVS3\nBXvxzTa3026a0+bhjEkK8ZJHJ9PrWk0kPFWIL+UxXKlpCzSEsMdh6VBTHOOWxng9cnJpksA8YXO8\nZJECsqrgfmz2Gf3qpOCcYOP2pjHbp0gmfk4HQCgVu2ExOOgyAayRip4/mJAy7QWGMAYohYIINuws\n0i9dxyBQaMj0fmAxt2buQB6U1ju/H08RLFGSGO3A5Ge1KNZXp2gXupAeArcvtLkZFfUNF/01tbKP\nddDxmbB2t0X6U1hUQmH4Rgj1KeadU8MEGNQO/c02t7W1ilLBUzu64oemGq3MYHGK6bxe/H0rUAib\n+LnkfeppdBuhomLhMvXNc+YXIwaxjolRhhmH3qTJEy8qrZPpQ9MDXGg2V6hWW3yCc9KQ61/ppaal\nCUillgBO7AGQTWcLN2+GSn/0l1a38SO2uIXRwRlsgmsvffBevaJGGubJwgbAZPPx9qGr0VwvUV6V\nqdzpFzLL4IaRhtUOMBeeuK9L8Q3VzKxmOJQGClSfKfXFTlFSlYY9ooytneXtrqSzDdM44AfOG5rT\n3egnWZhcT6haQyugPgRne4HHTFUm1ChEzQ6fa20M9pbfOSxIk25fGt8bjjbgEHjOcffNY++lKyvF\nuKFHKg498VPidzYzeHhcyDTvDLbwpxuxg/r3oKC1nvJWWKF3xzlQcD610tpAsJTTUjnIu7+C329V\nB8Rv0XP70db6tp0bRwRWvisSA0s2PXqFHXj3qT0a2cvpr6HUnddwtTkxoF8NSh6ECtJ8NvDq2nte\nXcQExlIUDy8AYz9aWSVWUiO9X8DUvhq4hjYSThhG0eeQQc5x78Vm4JHGmruYngKwJztHoa4ufk6e\nDPQa/gjSJgcMxTdnptoPT9UezvYJIAr7Ruyw6EVPik+VWxEiy81TfDOyMIrltrYKnBBJz9O360Rp\nmlxxaRPeSkpfbTcogYAlMYGfqSeK7OOLj4ZoQaVKfnwqq0plBjAAxndwD7c4NPNNt47aGaWZmWdV\nwEU8jscmrPDIouGN3NHCNiqy4LnyhRnv61028VldmOWZZrS9jZMj+Tk4b7EA/TNCwlnhtp6TQTTF\nLqIsqIUz5R3J96TieSz2OsnmBOMDg/UfaitN8sM1LVZ73SLLUrdwk8B+UuBgY4GUOMehI+1LraWX\nVHjjRR4wOQQccdcn2po+UCWlFxdlpXt28xzsLLjk/WjX0yay0ZjJJBslx5PEVmVu3T2U9+9M2RYu\niiaKfEg27exon8S5lCpGGLcAA4prwBo0eLSLGM3DBpFA/IcnGegz+lQ07U7i81efwWkLOrbEnkZA\nFxjHB+tQ62ymJaLn1DUFuZ4WunW2eJmWNn8RVI6L5s8ZI/WluqSNJHalkVT4BbKjgjce3aikkLEv\nuNqaRZQHbuZGmPfknA/ZaTrEzOAF68AevNNHxlH6aSxnSO6SxknNtasRGZF7Huf3r1cso6XjJJBl\n/dzLYRGIbQi5B2DPNZe5mnupQZC7sPKvHaq0cc5fDQfDfxFPboNMu5nW3biJnBIjP+KZT6nBbW8n\nysXiyscB3PAx1IrKN4GE6F00st9dJcTKoKrtyOu2px+EIokBkC8gDPYjH9qqlSHu9B0QeC0Y4I87\nnGWx2FeS+mt7XbHM4GMlQcBu2TRasCdBOm3KXNtEZ1MgDlJHHBK4JA+tE7YJZGVUkjjztO4+Y9et\nI1Twe8EtyUbyxYwvlBUYP/mpWmmRGFXkZw55OOO9ZuibCv4dbSY/EcnOd3rRcdzDaFY4ouoxmt2s\nm2wK81FhOUjQlfUrnBqmebuTlm5o0OgSCznn8XYoIKnrVVtDc21ws6MoeMblyOh+v601gaNBf6rH\nqT+MllFbsP8AubJOGPrigDdL0RYz7HtWAmSJFwjPIwYqAucD7Cq1KqR4sgj7crwKNGOmC3uD5JFl\nKt/tx+lFw/MJA+LrwljO/aznH2HSkkUTFl3ZtdNHIjHcOO/SuxWsqrskdlyMgrRi8FbLo7WCBGEb\nu2RyCe9VZEcpGAR6+1MZM5rKKuJo2BjmUMvtgYP75pazEnYAMY60yAy+2XEbdPX6UQumX05JWNwG\nGRheKVuhkhhpPw3quqRsIIG2g7C54ArW2f8ApyFEXjXLkofNsGKX0e6NtpGn2Wi2gghXCKTjPU89\n6Mn1qMDydaNAsXT6g8x44z6VBJJApx9qdISyYu2H5q584zjG/itRuxzxNpHOfqaLiuQijnmg0FSL\nVvRnDE/QVZ8xGemc+9Cg2WQbWkBJOBTaKWMAYwaZRA5BSTg+1WCQGnRNskMGvFQc5APqMUTC3ULT\nRwu6/itFxzmQAVjtX0//AE6nlZrs2aydzFIVP6ClcExuzWGZk0z/AE9tL9Lqw1KSCRPyhVLjP6Zp\nPNpdlLcyyWmsQXAkO4AwtGwOe3HNSnxtmtDObSLu+aC4itTuWRC4iQ7fLjnB74qOpf6eSXOpu/8A\n1atNO0mTF5Np5HP3rm6zjK0NgPJ8K3unWPiWVjK0xcq4aLc3HcZ45rK6np2tpOVu4L3Y35VZGAH9\nqvDt/wDSNVil4JI2/FUqf/cCP7VZDEGlQHhSQCRTS8Gpo0WuXJ+UWKWSeRLVUSB5EwXQjLD7HvQ9\njqWoGN0tJY48jDZAGSP71JVWm70V6bNqWlyytbxeNLIpOeTg+v1p+kmoQ28cyC3UzRDfG6AcjjP1\n4rm5+KPJ9N3sA1G4uLhDHdeGzN1wP6UKbOS20k3DoQHbw4lHU4/N+gxU/wCPxrjXVDJ9jqWNpJp0\ndxdzSpKMsYgMlxnAUe5xQL6jeX9zcNMhhyoTbjyqBjC/XivQiv2CWHre2NhCJohiZW8QSD+XnNNr\nTUw7maeH5prjO8Hghs5yMCtJGTDbV/A1KWWexaS3Yt4SLg7eCBwwOetC3dgX4fEIIygIAwP1/akH\nWhGoCF7cTyFZJ7eKNHO7JZDwGOP0P1FZm8uY5wDBD4JAORuLZ5poL6aT+DL4Y23k8+lzMVXUYiin\n/bIOVP7H9aq1GEfD9pNAuGvEI8RzkE55wPatf50Bf1sz9pb77Vrln2ktgKO5phb3EItLi1dSPEKy\nBy3QgEf3P606ekZL6e/CW2mkm3EjhSKhBcBEJTgjoe9O3gqC7iSQ2cSSSF17Ar+XJ9aX/MC3imjV\nmWReY3U/rSQdhbs7a6hcakZGn2My4w2MdfX9qMm0+ZDb+N+QqwGTxjJP96E8DH09qN3/ANT4cewJ\nFGEBAyenP7k0Pp0JeaSVy2IlLe5Y9P3NZOolH6OI7K0TQJru9lUTA5iiB5A4ya9XO7Z0JIZrBLIX\nYkFAu0D1q+KIKgG0DaOmK6Inj8jdg15tUFtihj370kmlCyAHgn/bTP0bjv6WElT6YGelElxc28EW\nMGJSWOPzZOc1jpiSsbSOWcu0zRoDuJUZJoXUtkFx4kaGY7AVUgjj7UqDZ2wuXFgWKpEszcqOSMVe\n1vKu3fLywBzxzRobtgHb2SPdlJJtkcSlweOT6URcXcksIE6ggeVSmAfvSTRNyBopJM+WN27Y61e8\nUxXmMpj+Y8UsVoCmVJkTb4iCQHna2R0qyJAypv2kqMZ9aqhjscngMCAuDQ043SySFVQuc4Ws0Zsq\nuHYR4GemcZoMMYpWL4/LnAoL0AWrr4QJG0jmqpZ2nkEb8EDjPQiqIwRZboZA55HTFGu4dSSBj0oN\nBKYb4QTAEEr+Uj60MLtmZgWwRxzSpAKmvMTbgAcjFXxxtJDI/YjA9qIUXy28lzo9oFiLskjRuAOf\nUH+tFad8I31642RtvL7dhHQetNYyVmx0H/TD5e+jlvHyqtll7Hmt8dN0+0U4RFA7YpasbwD8e2tV\nZbaMbTzgDvQk940g4zzTpCNgreIwJ3falz3UkbHcaNAsst7pmk3MfLijvnYwByc0aFK3vAeAtVl9\n3TjFajWeEwHBqazemaNGsuS4fNFR3LHG4j9KyiawpL4IMcVYmpYPFP1EsJXVMDrUl1RieCvpya3U\n1lF98X22kRb7uZE9AW61gPiH/WK6lnaPSyEQf/kPelaHRg9W+JL7VZzJeXUkhPYtwPtS7xstxWow\n20TSJNXn2LNFGeuZO/6V9Y+Gvg21skWUlXcgZBQcfeszG6tLWKMDaqj6CjgoHYUtBslgVxo0b8yg\n/UUQAd9omnajHsvLG3nH/vjBrO6n/pb8NagnFn8o46NC2P2Oc0jgmMptCbWv9LXn0u5it7gXcrxq\nkRnG1kAORg1gLn4cutCuVtr22aBiAAT0Y+oNc0oOHngzakHRiBJ0j8NF7FsnrivJfxpaSmSFZEjd\nQVJIPJ5/SoOSZkgW1+HtQ1nWJbWyACp5/HlOEAPI5+lVyKdHv/l9VvciBS0cKNlZGHOSew6e9bjT\nbHX+mffWJXvPEnZpGZsEHoPp6Ci21YC2MIji8IvvII5z9q7PERk9KNS8GXTIp7a5SNlbEkTdT6Y9\nsV4z23/p5NuRqKSEqRnDKRxzjsQf1pOyHi7GI1i6l0OGe3xbNANk0RJL47SL6gnr6UmXV3lY+Jl2\n/NknkH3rLSjxDSw1eC31pZ9S3fLyxCOQgfynjkdx7+1D6to0um3gCyIySrmCSPpInY/2rJdXYbuJ\nOyX+HxKyAPct+aUZxCM8jHqfWm93BHrfwPfBr3d4MizRFhliOhXPccmlm36PCqox0JEemmBRkRj0\n6nNVwqHk5wPU1WGkJIN1S3WKygEbbnlJ3AN0x04qvTYhLKPGPlTBOMU0vBEhvqKW7wy+CjNKcYyv\nT0IxWWMTtdFCGEgByD696lw39M/S+zRLeCTeQWDKu0n2J/tRl1dRz2VkkRkJhLhy3Tls/wBKeSsy\nBId0jNnLHOeOOKYwJ4dikO4iSabJ9VUDj9Sf2oSeFIrSN/EsUjoCThuT/avUlsdo1Wm39vcx8S7G\n7o3Bqy8ukgtpXQrIyDhA3LHrj9KdKjgnBtiK6vG1GCNkPyysPMsinINJtTuRYIvhypJKePynim9Y\n0Y0Q0/XVcYvWYuDgFe4pst7gEhGCEYBJotUWR7TtfVdWhtguYycFgOSaP1UMtztUtheen3xU26AA\n+G3jIjAAbg2cf89a7c3IeVnjAGxgACc8dKZML8JwLC8sxkdAM5GKLefTo1G/OcenWtLSYOdWgj5g\njJP0wBVTzyTqd7eU8kc0qQUiATxCQqZ5yML+9dVXSMA4B3fzcU45xiSTnBGPWoE4UAY57k5xWMUs\nqqniMxOcgYFRNsi5IQsSeSTQoyL4oFkjAIDEnJPaqHD27uPDO49zzinQGURTPI2TJ0PcUfEx24z1\n6Zos1lMkDtLu2kk8DFdvNIlu38Qq8YI5A9aQxJNLjhjAcMeOpWi47cGNYoznaNxwDQY8aNX8M/Dr\nSRTfMhlhcKwIOCSDW6tjBZeZVw3qaKQW9OT6yADtIFLptSaZuW4qiQjZH5uOMedgaj87Ex4oikXu\nI2X8wpNcP+KcdKJi+GVWA7VeSMZDCigMiHOetTD8Y70QHCue5qcXBxyaxglDjrVqy8UyQGzwfJ5N\nTM+BTCnvGyCT+/asp8TfH0WlMbaw2yzd5CfKPb61jLT5rqOsXN/cGWeQsSc8mgjMc9c96RlER8Rm\nairSGSaXbGAT6E0DH0v4J0aVrcOHhyG8w28j719Q0/8ABjVWPagwocwyKMUTu9KQJMdK7TAPV7FY\nxzFA6to9prFm9veRKynox6qfUUGrwydHxb4l0z+Aav4N2JTGHBJXqy5yMfbFKLnUYvkJ5EVykzYU\nbencf2rznFqTR0Kvgw0vXUn023sZLiW0kthlpiT35w2OR35pXrGlPeJezSXNvcSWe0h4ujZxk5+1\nVh+LC42hHd2L2RRZEAOA/J5Oeaj4vO5MD1GKq3aOeaoqlTPnjPA6iqiDsBJ4FSQiDtL1BrC5imkh\nE8UZ2suAdwI5XH0o3UNPj0y+SW2VJbaVRLbuwyGX0PuP7ULplrtAM0819+D4YeaSRRGwXH/7Ppiv\npPwt8L31vp9vZ6rbQPAgLpvb8VCRztPQD2NGUsofjpq2D6v8E3EXiLpkxnYJ5o2bDhT+zDtxzntS\nezsrqDTJ9PiglXZ+KBIpDIR+dffjt7UnfsqHiktTFM2nrcQM3EYIHTqaBOlGG4hJL+GW87FelWjI\nnNWBteD+IOsTFVd9u70AOK0Vz8Pm2jF5CDPat1ePjBz0YdvqabkdAjGyg2W+9RLe4UeUMrNzg4HB\n9eTSu5too3nFywNzG2FMR4alhL9GlGtK57GOG1L+IhkZl/Czk4xyc1RaDy4Y8ZznHSqCMt0wtHcq\n6jo+AWGRitJfad4MsksLJLFIoljKDGR0I+xzU23ZWHgtvtMu1m+YYKYC6lnHG0H2716k/wC0QtlN\nud1xtZTyPzZ5FFylZbm3ES4UuCyIpYudoFPVi4O30K5e3DWUcgkGMQXbKrN/8eefpSS8tl3TW99E\nI5cbSrDDClUZJkpL6hLD8M+NepHb3CbWxtEmeT9hTy4+HLmC3YrJCfC5Khj27DPeq9gJiWKwUzeL\n5w+fL2rTWcj/ACu24uBx0GMnNSlJDJActvNId6uJJWOFzjgdzS17WWK4yEG3OQOoIpewWG2ttZmO\nSa5MqOR+GIxu59/aoPaTz7mmkREQdMct9BVUxKKPKgEefD48zk5Nde4idwsZkbgAKAck+tNaCh5p\n6tAC8mUO3aFPWvTpA0bK0ZdjyDnoan3t4YCfTSkLbZYyAAcnNDQWyXExjeURljhTiqowVc6W8EX5\n45FQZBB5zS+6tLpNzGORFbnLLkUWZMK0aJvDdeGIOTxXtXiAmSSNgxbr2xQTMLgpKkzHaucAdz9K\nk0ph2sFLoBgAHGKf0yGdrOWhDbdpI74NGQCOUEXEki+hSoTlTozJnSoppR4VyzAj8r1q9A0u3tbc\nm4jXeRtzjtTQfY0bGTXCQx7I+AOlCyXp3fm/eqoLKnukP5qGlucdGpkKCu8kpO1wuP8AdVkbN/8A\n1N1EwSFBXzZoWQBZM7jg1jF8Iz1yauXAGP2NFAZLjPoa6Bz1oiloHvUwwWiCyYf3rwYmmMSJOK4C\nSaIDIfHHxYLK3+TtHBlfhmU9K+aSzs5JJySckmlbGiV7sjmubqUY6p81PdBtWnuV2qTjqRWRj7N8\nMw/K26q20cZworUQShui0JGTDY3IIzRsT5xmp2OEqc1KmTFPV6iY9XCAaxjF/wCqPwvJ8QfD3i2Y\nAurU+IMDlh3FfH4tGZtOtoZHdFd2MhbgJx+tc/Iqdl+PS64mQSQvY2oR4go8wH4hXksee9d1U3E+\npSPHAZzfxbpk28AeoIPJGDUk9KtJIRa9ujv1VgSPBXbn/bgYzj2xQMZp7OOb0sbI5FRRwSQxAB4N\nIIjgUhmT24Ipvo1wb+2/g8rKsnMlqzdCedye2eSPcCtKNqysfRa0kcsjCMkMp78EGnSfFGtwQoIb\n1hsXABAO768f1oVYPGTvP9QtSRGSTlWgMTHqwzjpjGOla34V165utHiutUkaSF28O2kfBlmb0U+n\nXk0Hx0rOnjd4gXUfhuON3mttRgjikBdEuMqUGeme+OlZyC/X52RJHSeKJGXcpyrHBwR+1PDTSWmN\nRC0gYSIS3I7c19C05ru4nkuredGMlohkSXiOQflYHtxjNPy+IXjVthF7pdjp5tbizuEmjuCFG0YA\nI/lU1m9RtY4dTfCBA2W2sPfkZqUBpxSQF8q8peVhtTcAq7T0qvw1g25Yc+tX9RzlthNHDceIUDAE\nnAHfGB+9MLaeWOESLgxqdgyeBnqKw6v4NrKSWfS3guYZeJR4ZdT5lznGT9CK9XickZRmxkhFbLJb\n3izzWvzKj8yM+0H7imsOoyLMr2VqtquDgZyQTx1r3okmekaaWQPLNIzRMCrFjwfWmVzdWOovG93C\nPmmG1pRzn0JFPKNipldrbWpmMbwoJEAPPBI9RXLi7t4pGhO7J7c8ij1QtCWayn+ad13uh5UBTkUV\nZ2ySuFuFnVMcnbiuWfHpaPh6GH8cyqpC5KKCecV7+Dy3pJMWIx1bIXpzUlHQMH1O60+6ulNiot0A\nCbEBwCO+elWWcT4PzEDBSvlcg8mq9RfgStlbMc+EuD5sHipeJY2DRToyxhwVYMM4560Ghds6X+bu\n5I4irOg3gHo6nuDQviKUZVYMwbBII4oQVDpE7mRvARdgx1L5qqC0GPEz3JFVTozQK9vdqGkhhZ1z\nk7un602sdQlQgXS+RxyN2cVm7FoLl8CWQtGFUEcmNcZ+9BXtnHIVmUbyDgKW4I98UPBkUrMGzvt0\nGwYUkV2MwO5MlojBexOKVtjtFMgRpD4SJCD0TPFShBeQAMAT2Nc7tsQf2OlnAZiQOpGacSXYjAUc\nACurij1RkLp77ng0L/ECTzziqmOtemRemKpadpP5gv1ooBOMGQ7WIbFEx4jXg8+lMAuQlhySKqlO\nH471jBlvwvOc/WruCaKAyQGDx+tSB/SmFZ7fUt/HvRQDqtUjLtHHJogPB/XPNZ/4s+J10WzMcI3T\nyjC5P5fetZvT5RPO80jPKxZ2OSTVDHNIUqj249qkCaxicYy1bL4SDpOCM4PbbkH+9GPoH4fU9PY7\nFz+gGK0Nq4CjPFCYIjFPNjFFRLgc5qTKovU7TVqtWTNROvU6FPV6sYiwyCK+Hf6mfD1xo+qiWDeb\nZ3LK27oD2qXL/WynH6ZjTdVmhtJItviEnERJ5TPUD1zTOe3s9MsY49TeWW6uiWIQ7TEAM49ea43K\n/Cq9FGsaRY2cMNxDJMWuovERWIIHPSlCQSSvsi8zkHHuR2p0/wBnPyJXhJ42gt0a7aOGViB4RfzY\n9cdhUUt2ll2xjJPemr6idUwmxsHu0fg/hruOP6V6XZaGCeJts8EhILcYwQRT/C8VWl2oWSJqyXoI\nS1vU8dCTwCeqn6HI+1XQxxX9yIoZ0LspwM4yQOBn3pEaXpzTvhsalm6ut8em25BnfHX2XOMse3an\nNzqQvdUguLWJYLSKBIraBudgHHX1yCc+9C+xWFxRoL60j1fTo7dMGaaPxFycqh7gfXOawNpFImtJ\nYBGEm4RuXXheRmhBejyu0Hv8JaXofhzahqAupCCUtoBncw9WPAH2qU2oHU9Nt23R2KJmN4UHlC5B\nXPr3p9krYrag6G8wkOkPbT+CYonLqy9I8DcpX0yQRj3FZ7Ubu2e0jZ7fxXk800jPghjkYHpU0tBN\n4BWolaMIpZo0KkpnryP813WdIuLS8mMUUj2qyNscAkAD1qt0SirOHT5otOivDtEZbawDcqTyMj0P\n9qf/AA/e2ei7bjV9rr+aKDbksfUg9KZu1gyw1uj61L8fM9jaWqWkULKWkc5P0A9a9XJyQuTB2McZ\nozZsWYRqp5BBNcWKSPJEbMmzduwcYzXfEVlct08jFlG3jBHXih/+7etvkfaq4AIxVLEofaTcvNaC\nIQIWjOY5fTHO01O8HzMUWotCg8NwVVXy3vkemf60jdYMlelj6xcRIVjWIA8HC8n3rq3bNMElTarc\nZ3549aEsRkwG9tbeWTdZTN+Gcsv7UqvRcW8ZKPyp71xvk0ZieL4iv47GSxRkEL5ziJS3XPUint/8\nVX9xpFvFeLbqyKCrRqVYj37V0p4LQLZSyXqTRz7oiUzDIDkE+lAyrJJEySuGbI4pL0yCbDKwLDK2\nwp/223EY9qLRITcBVGHxyVGB9zRQ4YrwkeHt3t2560Xb2bfLB5V2P1CUzAU3hIhK7TgDJA6fpSlp\ng0bHO0gcYxQiKWWUk0gciVSdhIyOuKuS6dm4IwT1A4rNDIu/7keTy6nI96gw87cYz3oUBsquIkVS\nVkDMOgxRujRLJKCy5A6kjpSddAaGS4WNMZxS2e63nlutdJkL3nZuh5zgVBZWZ8MCD9KWxqPSXaRc\nbst6HiqxJLI2RGwH1p0Iw63Mh4wB755opThgMN9aIAoMduKqcfiDFEwTEo+pohB9qZAZYuRXScDq\nPpRFZz+X09KkgYfmogJO4x15rin1rWYHv71LGzkmkcKqDOT0+lfI9b1eTVb+SZ+nRR6CgwpCzOf8\n1xvSgMeUVIUDF0IG4Zz9q+h/BFsJRuERBH8zDp9KeIJeH0O2RUGPSmkEq4A60JAiM7WUkjtTBGz0\naoMqi4dasXrWQWWiu06EPV6iY9is98baJHrOhOrKGaA+IM+3X9qSauLQ0XTPkl9Y2enTpdwQBpWc\nLBEW8pfqffgUnuLGP4jurq4u551l37wduSnGcfTsK87ibxsryVWFM2nxXNnAkcjSSxuFVXbZkHsR\n2wf60FcWk+lXyx3irGynI5yPtTylZCdZQPrnw7cSau7WYBgmxMjM3RWAOD9M4+1TsdGNnckpcmZl\nQ4G04zjn9KtHkXUaKs01pLp+h/Dc8V1MUvpI98SgfnB6Ems5qubjdJCxkEiZJHI3H/hpYN3Z0Sjl\nBmp2McFtp0GrzOxSDmGHG4EknkkYHBFM4LHT9MiinstPgMw2gM+6UqWUEEgnA7jOO1GcnWGUaek/\nibUXGj6fCzfMRTO8kjZONwwAF9MHPFFJoLNFZubhIw7MfxOF28HGOx5Nc/fr6UUfo5jsVBSQQuik\nbA8bkAKR3OaGd1chESGW5/KWIww980OyCvQDX9NGqyxFEhedUKk+IFbPHU9Ox/Ws1Fol2168Pyql\n0cZ3OMFTxgeuavxyE5Ir00djpsUOnXMslmy2rFlWEvuUMoJLFupA479TQEenWuq6R4NmhW7aPxI8\nt5W2nkD6jNartonJpUmIdOuUtNRgebd4BYLLgZJXvx619hshHNpatEiz29woCgYA2+/9KlN4Dift\niTVfg+zS3luzthlgYLBEWGxyecnv5az978OLc2rzG1llLZHz5nULI47Kvp6VSE2qTHasI/02vJNN\nv5rW4t5o1dgykA+Vx3zXqlOnJsk1TEL3TbcIx9SKi95dAhk8SQcDG44x9670AIhiWSMyEOrFsFBi\noB98oXrzjdIwGPrRT0DQxjnWx8zvHtHPlHDUyNxIdJN34ahFYZ2chkYkFcf37YpOR07H41aoT3Za\nCQqjBwwG044IpZcXjFyqy7VB6CnbtCVQTa3rw7ipzvXknviqrrVI7zybMMTyc8GuCUaZmwB9JAmM\n2FCnnwz3+9AStvmdSxwwK8np6V0RlaFTNHakjTLaNCAUGc1XL4MEbFSHlP8AMaVrQfQBpvEjY7VJ\nU4OKZ2tv49uoRMyKvlOePvTrB2X6SjPdnxFy0R/Mw6f5rSNLEkZL87QWz2PtWbsILMyfwe7cRAye\nFnB689/tWTjj8Jc7c7hgZ6ZowFLreUm5C/7wVG3HfiuxSJkMN2Rxg06G+FrXG1htKjHWumZZAA/G\nOpFZoUikBlmHDYJzmnURW3jGMfasjFE93uB9e1VWgaa7XMbugB3bevT/ADWbHigm3sJbeK03gE7z\nLvAyCO4+oq17u1vLW9M6xGVG/BZcK3Xoex4qSbKNIrubW0hjjeKSNxImTkZKt6UvD5YbT3q8WyEl\nQwixsznn6Vcv1Jp0KWhuOuKjnLCiYLhXiiFb0FMhWS9MVL9M0QHc+tRZxisYrXlqszjOegrGPnHx\nzrzXd6LWIkRRdcHhjWQY5P3oBR7P0rvWsE8KkDQMEWSGWYL0yRjnFfXfhZRHZKgjCceu7P3qkRJM\n0yvjgUXBJyOKWQYjO1k54PNNIJOxFQZZBcfJq4DmgZkxUqohD1eomPVB1DAhuQeCPXNYx8N+OLYw\navLpqN4XgybgepwfTNZe036TqccgaTZLxuZRzx35NcEY9LTKT1Gks4bG4SCO6jDvNiUEA5JJ4wQe\nOas+NtAvr62BW2jMtoimVWb8TB44/QVycblGTszjcVQot7fx7GFbzxI5YU8PBwMrn+tPJ9Ds7XwX\ns9k8sjARrJLjJ5z/AI+tW0eEK9M5q2n3epak1xJAts6KqvDkAoM+v+KlFqI0RHtFtDIZdhikc7gT\n0bHHrV1KlRdwt2EXGlyXaSy3ReHxHCkytxj1HpV1loDxmRQ0fhABEbxOWHOcZ79aj2fg7ivRrDDH\nDDBFJHEIYkOxZ2DDPXI7Zq651SGRVKxG4lICxEQs6gnr0HNKwrwYrHqd5bP4VhNDkKU3xYGR04zn\nFZrVrjVBeRvcWs4OWU4jG1/oRzRSt6ButM3d6slrOTCrSTO+PCY9D2GO9HlLicW7STXC3Vw4jCHy\nBcY5xyDirOKRyt9mGfGurXFppsOmWtyhgA8Jwp82FHP6ms1Z3kiRRhWIKnqv/OKaCqLojyP8hrda\nbZCexMZkcXUm2RwdoQnHb6V9K0xVsbKC3s3BsY4ydrt53IJ6H0qLjZ0RiloDqF1YfE2qxW1xK0Vs\ny71BBBJHUg4wc8g+1Z7XNf0yeaSyuUe0ttNbyxwKQD7/AKkfrWcbeFVKgjTPiLQ9ReOz01Znf87M\nUwCPevUJKnpCbtmH/EWQtyADjr2q1XYBlUtxzk9K70ibY1smSGyaafdlvylPTuaFaNLrLRN4+PMB\njBFZehfgL4skAZrmNpkJ/wC2+RitXprRT6eIrWRII5IHUIzg4Y/X/nNS5h+MUxB7mKM3Ug3wMRtQ\nYBX/AIKHfSkLtsCk9fMG5/etFiyw6FSCPE0a5IxnnFVRacXkPhLEwHTGc/QClkr0T1E51kijVZos\nMw568UoukgiuiSm4uMbV9a0RRiJhHaKikKQoHNJ7t8ykGYKe3B/xRoZIJ0eBZbxDvDIwIY//AHT+\n3eKEsqkAA8ZPamoLLDqkCk5ky2ecDoKvn1JXhVgoKk5Ug8UlBQPda5cSR+FEYwvTcv8ASl8N2/gt\nFJKzKDwpHQ/WniqAUQ5S8iZZFGxwxzXBxkK4IY5AHUUbGOq/nIPHvXBMqvkspB4z6URRrbZjTJYE\nHpirS5c8ttTIBY9s0Gxki2GIzFo7eIPIj43OMZrhtNStLt1a5tLOQMB5pByMZ4FTUrHaoSyapdSn\ncbh/Ty8D9qjEwY52k5OefWrRVEm2Go+DjNFxJuGcE04pciY65Gfer09unrRAWqc8E13PP0omCYWy\nPQVaCxpkKy9FI681In0xRMRZvLVBJJ61gFicCg9VvhZWLyZXIB4Y9aAT5FfT+PcyOSDubNCHrWCd\nHJqQ54HagY4OtSJrGCNP/wC+ucgZ6ivr/wAPTAWEZLliVHLdaohJD5JMjNERS8jNKwxDIbnYwxzm\nm1rcZxjvUpIqhik+0DPBomKXIpBi9W4qYNOmIzteogPVyiY+dfH3wnFqfxNDeNM0DNBtDCMspIPc\n59MV8++NNMtdGOnwxeNLMyeM7yAbftXFyv8AOh/Yi3SddOkXlpehI5lRzuiYdV71vtU11L3WEurY\nYjurYxjcCBk8j+1R5Its3E/gs0a4RLpnvbXbKY2LvKuRtz1GeM/pUZbS2+JLaSSyUWi2zsiziTgZ\n55HH149anCDTtnamvQF/g9j/ANvVEmmJJYmNsZ+uc/tTbSHmhHh39vF+BlV3crn2J6euKs2mZ/4W\nahD89B54hID5mCdTj7YPpQEcVw1vJ80ptLdAXBOPKn3xk9eKSjE5bq3gtUjggNxBnJEy5Lr2wMcV\ny91C/uYNlvfGxCJuWBIfCz9x0o+DA/yeq2S6lNHNcRLcWolhd5Msq7hnn9aSXev3mmiGWGcSloiO\nWJweeee9UjrJzxWjOaXZz6pqgVfEZm8zMAT+tNIb17a6jkt5V3wk7XYAEnuT7VWVXRCK/GwLWJD/\nABOZywZmbcSORnrxVVk+2UdftWWLDlm7ZtdPnjvxHatgyRIZYiZfDIOOTnoenT60NFa3et2kxkzE\nEj2W67zuI/Xv71M7o7FFWkTxyhtHu7g2wG50d1IZHwBgn/aRn9KdQfDuzTXaWPexbw50Zt6v3UgZ\n6H+1B18CsYNo+hx6b8Qx/LREwYYu6nCjsAfTnmvUst9JTSbMyzqw4ToeTXlXxJljjRmL9Mdq7rwk\nG3EbINoBCIoUcfr+9COWwVXdnqCvY0FpiKy3TuNzF1x+VlyP1rSwxWFv8N+NIkQnYlRg9QTzjPep\n8nwrD/RHZXsUzssEyjauTuQ1ausRmc5KnIAwAfpms0I0WtIkoLZHHY1yJl3ZI2j+1K0IsOnUYyoh\njCk8jDL2oOfTxJMZEVuDxwc5rJBqwSVCoLLliBnGKCuLNXO64SZS5IyCMcUzQ1B9otta2ce0Mzud\nxUnG0Zo5oG8NUAVMjK7OTj3zWZgYWrWx3zMA3pjrXYrjB7ADjFYIBPF4chYA4Y5JqUNx5igHXtmm\nFLZIjDG8h5O0bRj1quF9gCebjgYFAYshIZf+oDRNyFXHU+9dtwJ7jZt289B0rMUepbN4O2NGYgZY\ngdB6/SqZLyG307ww2ZZXy5AzhR0H170j3wqsBJviA7GKx5dhgueDSp5/E5fzcY9apCFCykzysXb0\nAFG2w8vvVESYbFDlgaPiIC4ogLdwB681dHjqTWMTxn6VwcN3IomC4yDiiEIJ6UUAtJFRZ8DAogKn\neuA56mhZiedq1jfjPUUEPhA7nPYdq1hMCzZPb7VHODWMdz6V6sY5j0ruaxgvThuuVXJGTxX1nQk/\n6WMbRwOuadCSHyNjirBJ+lBmRfC+CMU0tZSSOtTZVDOKU8Dk0xhbnnNTY4VG3FWA80UxWTrtOKQJ\n5qQ5rGMR/qvd3Ol/DsV/aMFeKXYxIB8rfX3Ar4lqOtz6lGq3TtIUGxWYknHpXNyx/Kw9qQDAdtwg\nb14B+9fS/hnR7XV9Itprq6nAiUcJgbWTpg/Sp8rqNm4pfkd1L4dtL+8L2l7PFljuy27yHtmiILYa\nVZT2Omwhiz7xuckSHHJ/btXOuXsjtjH9kJ7zbI8e0IVUNgDYVbqex4oSO1b5SSa8mK24y5LKQrE+\ng7n35oooW23yk0X/AEUEy4AJkdiQO/Tn/ho61EN7drDNbyy5BcIMgcED6YyRWsxTfac0mmm/tZfA\nhaQI0cw2k89QemOfSh001rrXp/HuJhKij8kRkwvUcgUbpWI5UBT2d2LW9drmRbS3Tapk43DIATPv\n1rDandx3lzEIgTGAOCACTVuJNi8kvxxDrRI5LS2upLK5EN3I4h8ARly4I7HP1z34oXWTZ20UDWKx\nyKcq4ySSR1PPvTP+xJf0sR3CtKS+0jcc4q3Srdp76NFxkk9TgfemfhytW0fQ7axaO1bwI4ivhjO9\nc5J6ge/auNaIG5kS2gZgSwJYqc9MCoO2ehFJIJuZbGaUyG1Etyn5bh+MDGP+CqNOuF0y4Mdra5Mm\nHZImO87ec47VNN3QrB9V+Iba8kZbmaSFiAWVPU8/rXqqov6TTRnILGS7fAhmTPCjb5T9T2ohWuLT\nbDFaPgnzTMnb2rq7WSo46BpCZtpBPPXpVMd80crLHC0cZbGQT09axqGUVnfSCOWJGKM+C23I9T+1\nXazvljjitvDUhdyEjgHPX64qEnuFUqQue2aJdrkFwoBZT1NVQo0URR2chiQemcVXsIVufAiDJl9x\nxhjyK4mzGZZpAWOQFPakcqEYbaywg/8AbG1upbkmn+lXNnaN4qBgXGNshDcjkUVNAK1ksJDc7Yo4\no7li7Hw9zc9cZ6fas1qFlppkLRNOuP5T0+3eq9ovwFsHDoreXK7eTuIzV0k29jIijLDGDU2OiovL\nIhEnDE55OasttqjL5PP2rBI3KlmIx5TyDQKwNHud8DH71rMERSOUVckD1auoVxhQuc5yRzTox2Vv\nOrMGwOqnvV9tEAyyREkvyQe1K2arCbiW6h2lWdPEygxwG6cUBc2F2t89q8TLMgJZSegxnNImrKU6\nF8quE3EHPfNUxEsecV0IiwiMDdjkn2phE6qvPB7e9FADIZTiiUc9vvRAWocsMdPeiUzWMWbjXs5a\nijBUI470Qo29eaIDrMAMelVmbJ4omI7uakAOtAxVcT7VPfjNfOvie5W4uCcAY4z60DGc79vtXe3N\nExEHHSvCsYl2rhNYwbpgBu48kAZ5zX1fRpVWBVyvA6A08RGNhNkccV0SkdST9qDCgmCUgc0wgn5A\nXIpGOmMreY5AJNN7eQ8ZOamx0MEbkVchoIzLBXqcQiw5ry1gmd/1DsE1H4Jv4ZN2NgbyjJ4Oa+A6\nNocmoaja2qsXE8m07TyB369wKnyMD8ALm3ktr1onBWSF9jA9iDWr0HWpodMkt1wmGyhU+vXI71y8\n67cYeLJDm/t7+SH5u2jeS2aIOGAztPcH75ppp1rb61FYzrMLe8hBVwv5ZB2zioQhSo6HNqRy5s5o\nrs2st60DgGQiNfMR32nORSP+HvDcCYGW9CycQyjPHY9aZYjpi+x20vdYvbiWMyNapFjCflGD9KIu\ntSvtIuRFbhXbb067h6g/5rdbM8BbnUBrdzD8/bzi0iHhmKIlAz5OMZPPUU5kaeTU7iCB2laCX8OO\nKQFsD2parw55NXpT8VGdngjvJUVJl8SWF9xwOMcAHB/xWY1GTS5opYntjAq+a3ulG45HQMoxgGqc\nbkUtdRXbNI90t1ZHaYgCX2kgMfoPril1zN48u5lkB5DB2ySe5+9dMmjibaVGk0SwtNY+GZd0Je+t\n5sRlDyUPJz+9c0zTIrO4lBi8SbIRlxkJzyc1NuysIp6aYWUskkfjygQb9qK7BcHHUcVK+tbma5Sx\ninwrKCq4I3c5PmxjvU7OgU6pZT2sztvi32cfjPDuOcA4yc9+4HegpPiOcWrS2kkZeaLZNKYxvBPY\nHsMY6VRaTkIoVPzGx2QHcCzueB9TXqLkTodT6jd/IMxupwrkAlnPI5oNdWugqhruZlJwoZzVUkjN\nl4uppJMTouBggsOT96MjS2kANwDgN+VWwT/4rS8MgoRXFnp67biXdL5kRDwB70EsLu535yx43Hj9\nKgnY8mcu1jCk4ZjkDygDFBqSeOretayZTtdJSdoORjnvXJbctIJNjIu3BGOBWfgjOorbvwwdpHQG\niLQN83ET2bvXJNu8AQbxA3U5qxWlCPtVWYr5cimhNxBYmubW4aQGYKHxyVwOK9cOYU2gNkgck8V1\nxl2GTJ2sokgwevWpzM6KyJjJXcM/WqJWE9JLMbmBXUFHhVnwOh6V64jZs5BC4xnp96HgwS1qtjaW\nztNl7nJETdQAcAj6/wBqgIlnViFwQSAfWinhkgrT9LlvJUjMcqow3bmXI9OtaGHTljHg29t1GMfb\n1pHKikYiO81TwF8COPLxEkFiCqnvSm9129nMoknc+Jnd2zRjFPTSk1gALuTwSA5wR0qnxfKp7mul\nEGEQ53hjR6gFPeiKFw42eaiFl48o+9YxfG5Hbr3oqN8qaxie7jmvK/PNEwTG3SiFmxRAVSXAORyT\n7CuK/HmrGPbwCMmpO+1fWgFAV1Ltic98V831h8ztg9+poGFY6100TEe9SFEx6vYx1rAC7Bl8dd3T\nNfTNBZRartGKdCsdK3HWrY+1BmQVHjFExPtYUrGGFvc4YZJ4pzZ3W5c5yfpU5DxGcNwTjIotHB6U\nozLc8V3dTCM8TXqNmBtRtVv9PuLV+BNEyZB9QR/evzLOlxo2tSW0heOW3lKnHDAg4zntSciA/Cu6\n0++e+DsPEWdiyuTyx9PrW10b4WvpdEhdYLCQeYPHI2JA3sw747Gubm/rRThX1jeSyvIpUCK8ws2X\nfbAho9jcHGMc9aTatBeaFqc6RJcFUO6OSNS2E6jntiuJzaOjrFg/8eurtg1xJ4pA2nxFGQPfHIow\nJbXGnNstt87uBsUkcZ/MOaguSUJ1LwpFuqRVrUdvBczRW9vBF4aASEIMyNjjB/5zUdOs4WsYnmhl\nMgX8pbzZyeee1dPLyx4422FyL7zTXa1HhKjuzguWZgFA/wBoz1phNpen22mxX0cUUJjG15lJRgfU\nkc8/1rzP/wDRLli/+b0i2k9I6vexappOnOqEXKK252/MV9/60pvLGxlia41BHOVI8j7f/FXf8iUe\nRRQF4wL4Tj0210e+nvpJV/EHlQ8qgyQx+/FZKYK0rshypYkHGO9enFt6R5ZKkMNEvJrKdzbsFkkX\naCWwPvWv0m2e5uUuZn8O0zm4AGN+OcfUnpTG4JDW1aO4uBeMXkiZ/JbJz4jZ459AP6Va1yb1idhW\nSEkLE44z36UDquwD4ndbj4nhR5NsN3a/LyIuPOzghR6de/ald58GS6PEBcyQyKrksseeD07e1CUq\nRKboBVbe33K1vHsxg5GMj616o9ORkf8AocntU/hEySEt+IhBXkKMMP8AFUWlv4MYUqrFsHLkf07V\n6CY7Qc94bSJm8MMuMbhEr4/XNeke6TT0lV1dnkDAIgjwOmDx70JPAoNvb3TUEYN5IH8qYRThG7jP\nvQUzq0jFCSAeCetc8TNgiTeIGBAIU55rkfmfMYLHsBTUKWs4gO+QDHpQb3CSynb4uSOdxwP0rN4B\noO0/Srm4ZHEMixnguw2DHrk1dcqsbbVaMiPIABySfXNc6N1KmhhtrZVkO+fAJAPlQHnn14oCa+RE\nIU4Y9Aafr28EaFlzOzyZbPTvV7wGezSXykYKkA45HP8ASrRj1CiMVqvhPNbyLKFGGi6Ff816Ccyy\nDMZyq8Z/51qqoJpk0hbz4Te5giMk8OCcDnHf9qV20EkhUPF+EPMiydxXJDk72mVUcsFubSSZ0kkn\nUkHyAfy+wphFCluSWKuNucY6Gqxl8AloxgUQpE0tycg8x4IwDyP1qi9vJ5SrxM0ICbSoPeqxSYJN\niC4UL0GSRyfWllwcZJqgLwBVuSPWrQBvX0qiJsOi5x7UUJMkjpimAGW6s4yQdooqM4NYxaD7/arV\nlxxWMW7813dlx7UDBcbelT3/AKUTEC+AQOBUBJj+asYmjfQ1xmOTQZgO9bELfSvnOsOGvmUdAayM\nADrXSaJiBPPapUTHc17OeDWAX2wBlHXg9q+k6C3/AEiDtgZ55pkBoex8YJPFXq3PFAwRHUbzVLbT\nLUz3T7Ix371jGTvP9UvCYraQYGfKzmuad/rFdQXO6eBGjPULSNWUTPoPwv8A6laZrlyIS4hk4A3d\n8/3reRNzzU2qG9CN2a9mtYtHgamOlFMx7OK+J/60aEbT4httUjQCG7TZJjpvGOv1GP0oy1AFWiyb\n4GD+EyIu78U4K/T0quz1KYz3yRXohV8HyDI/KBke/Fck1pbidId2Ot3UHw8L1CbpIYWy8qYEmGz1\n65HNNdM+Mmu/gnU7+6x8yW8NdvQZyFHvUHELnRVcx6Nr+paRZSo8F9fW6yyTQeTqvTHQkn27UWfg\nbUrCEpC8N7EgLR5BSUHsM1CXHeFITVGI1jWpY53sWhEF1FLtk3r0wevvzTwsTksrZIxmuT+RxOMV\n/wDol1I691J4DFSqMq7gZASAP81mNMlj1OZ21Y3UtvMOSrFVG0+mea38PjUe0ngk3eD7UdujoZNp\nmtY4wcltvB/L5vWsbqOq3WogLOdqjpGDgDPU10fxf46m3N/sE50qQTDMbjRZfCjEKlvDZyd3iEDO\nPalQXgV3RjVkJqqCbCJ576CGEkPJIFXHqcCvol3p189uun2MwVVceK48xbsSfQAcVm6H4HVl8wdI\nxmBhbQgLbpCcHI5yfYnmlKW90wmubyXZHI26OJHw7E9R7ChF2dKbK7mGwuNQsr2/BQQvtZN5KbAe\nvHOc+/NM7vWLR5N9hcfMRHA2FT5f1rRTlNJkueSozt1M0twZbqNFgyVVFGfua9XYcieaXaVzNKYg\nu10O4YOMgZz+x/WoXniTRNIs/l6GJVxg+1TbO4haCS2ETbCfMN2QDuHpT3UEh2D5gMgUAomwZHOe\nvfrUpt/DUKpPDs3ZVCup52tznOD+vvQUk0MjFoozAeBh23UUKyAgk35VWIzjhc0OGntrpY7Yiadi\nRwMBfY03wyGltaX80iKUQSEZYsnlX3JomYwQvFKsUc8keVkkZMKT22juPc1FuxqOTahNNCys0m5j\n0yCAPag7crFdiZlLCLMjK3Ty84+9TbAC3enXlzG98kJYuDJyRjGeeKzrFjqCOWyjIeCOh7VfioSR\ncG2yf7u3PNEzXdpDY7LgNycgIRkH6VVeixLJrKN9KtrnTWknmeQo4C4+gHv/AOKdWFja2cQPxA0c\ncgAAjh80h/8AkAMCpylSLRVM0VlrS2Vu00MUYtUIXbu25B/KMEfqaE1Oyje7ZtPTyy4cBmyTu/sM\nH9a44um2OtAZLBVUBCxBYgZ7VQyrB5rtpBEV4EajOfqapB2CSojHerfXHlDhUUDBOelV3r44713Q\nWEW7E87dckUsu/ymnMLTjxM1YjZIC9R0qiEYdHxt9+pomJt3PTacCiKG20xZSBkBetFxg4z61jFq\nmpBsnnrQCXDP2q5Ov0rIxajljxirD+Xg8miYrY84rg5fFYxdkKMVW7celBhAb1isD7sc8183vm33\nUh9WNZGBxXe3NEB4Ad69RMcrx4rACbMjxlz0zX0jQ/D+VVY+cd6ZAY+U8YJq2IfU+9AB68vYdPtX\nnuGCog6k9/SvlXxN8RT6vdkltsSt5UB4IrMZCAylieTXA9KELsrx7edJEdlZGDAj1Ffoz/TX4tl+\nJvh0zXBUTQSGN/0yD+lTkUgbJJs96IVwRU0wtEl61aDToRniMis58d/D/wD6j+Fbm1UDxkHixE9m\nH+Rmm9AfDLS7Nq0kasjovDxyfzYobUFnki+dgSNrSM8lF5jPo2Occ9ahJUNB2qL7XWFk0ySxjfyg\nkAfynPcfaqmljh01UW5dmIO+IEbQQeO/PFTpgkg34Rv7q3+JLW8Te0kTCNA4J4xjv7V9hsPja2nu\nRZypIHbq+0YT61PtTDxq0YT/AFSudOvru2SxsZPFWXdJemIgNn+UNjnnFXfCl9a37LbrbukyodxY\n7gex6/2p+qpAk7Y0bRDZiXwpvEty28pLyUXHIyfvXzVLwaTDqECSiKcznaoTJZc9Qe1LLjUlXwW6\nZSmr3k9i9nNdMLdmDFT6jpRGiadZ3fzZv5RFtQeCxcKN2ffrTuPSH4mjcpaPL2e3tPh+5t/DtVeV\nFZXi6/mHXtn+1ZW4tjbybCQeAQR7jNT4k6tlOdbgV8P3TWWv2k8Sxl0fOJDx0PP2raaVcR2Wpg3l\n2jfO7nVEzjk8Entk0zRPiWDDWriGWLa3lud+H2nKgVmLy6ihkCqQFbIUt7VGKldHWmlFtiUapLvl\njcIyN0BXp+lW2V1IjK4Yg5y4PpXao07RxTfb0JudQhI4IA7cV6nIMdRfK6fO6bzIzqSNvQZrsOxr\nG6uXMSRRtuLlSeT0GPf2rn7HpgMepxyh5Eh8TYudwQJu9PpT0TtKtr4lufxRtz/MD2wPrgVCPI3j\nBDWIZNUuL2KS2n00btxIkmTw9p9iagNGluG3+LDGAAxbJbA//ZzmmXJ+VDThQdbWdsYJIy04w2J7\nhcbQD+UAdRn1qq20+2idpII5T4efzOOnr71XRG0XXupNbbdqkRsSipjeW9zS1Z7ie7jWSUBXPnfG\nMAdMCl6gshcXsUBkwxZQSA6LwaW/OrdWlw0RJVl27vQ5/wDFTnFpWZPRtpTXB0SCOSVY4/EZBzli\ntJdS0NoZDJCZXgVyu4oR9/pVONpIWSAFeR51hS3kkY90U/0pva/BF1qDNdX0hsLKP80txGQOPQVV\nyo0IjVb/AE/SdFurLRoXlmyCJ5CCJOoJUZ47dPSrNC0b+JTRS3oWdyDJIgbY3Toc9eKjNfiUej34\nm17T7aKLSbaCNUePbMGG1oiOgP8AWs5bXywXEcpnkcqoUDIIIFDj4bVkv+vTAk3wmUszfhjqidSf\nSkV8Ll1MjKAm4nBPaqf8nB2N/wBVPArTngS0Hhk7z+YChb2R93l5+tdC8ESAGR2GZNv2oG74BxRQ\nWKnb8TmrICNwHTJzmqImxijeQ47VdC3nC9utEAbauMsexNGRtlckgCiZFyDdVq4BoBLd3qRipp0o\nGLoyBUi2OlExE5JzUQcMSO9AJMtxVe7mgYX6vJ4dpIwUt5egr5zO34jE9zRRiC81LqKID2PWvE8U\nTHB0rnasAvtG2yjNfSPh5y1qNmAKJjRRKM0SvHSsKZL/AFDmKaVFGRlXbkg4IP8Aevmcjktyf0oD\nIqPWug0Ak0PNfcP9B42l0HUiy8eOAD68f/VTn4UgfTIp9km09uKNRwRxUrKFkc65xmiVbPSnTEaJ\nA8Vw47898U60Q+Jf6ifActlrdxqVkkkkUh8RYo48gf7vp3NYqLVpNK027hiXi7G2VmH8u08f/vVy\n23Khq6+GcvNSbPhwgIinAx6VVZXbpOrhiCD1rqUVQrZtNL1HVG1jTybpPl3nUEyAEqMjPP3NNNfv\nLuLVblGKxoZSYlXjy54I/wDNcHJOKnQI/wCHZZdWvrT5e5uozCcSKJMEnHo1LYJH0i+ULebpJMgi\nM42DtzVFL4HqaK7+OIr3Sp7M2MpunjMJORtyRgkHrn7Vhb+2e3mKtk5AbkY7DrRQs1hRG/OOlPNL\n1mKytHgksLW5ErZZ5lJOPQYNCfgsJdXYROtxrLNDpltH4f5nRVClfp7UTq2kvNNHbpB4U1vAviZO\nd7YHek45JKjoku2izTLC1mLvcXHgtCdzRtGT5e5opZfmrxyZGAZgsQHGAvr9qrESMaRM30tney77\nqNITjw16kn3obUJHu3zEgmUefeG6etbrtjOeUBeH4qxtKwhwvLbaF1TU1t5RHbr5B/N0Jq0SBTb6\nhFckKZCj/wC1+9erNE2jdXttdFS+3MhPOQOB3PFEwT+HosizlSHb8pGOMVyfcPRoSmO3bcsUcjMz\nrug3YBHWml/eXlnawTRrJChPAY7sqcj82e39qDSQYxpgWopf6gbV9OtkuHux+MRGAQ68E5OMA5Bo\nnT/h3WL/AFREF5ZieELiN5lLkY4xjP70UkwTHVqj2VtcpfDZcrj5hDyRnGBx1Hf71RfWsnhvKikJ\nboTlRjdn0NOmTeiO6inM9vLPCYkYZG7jOarhAvL0QbVTgbirjIXv96KYGqJXvw+blQsNxtRWIAz2\n7ULa/C93BG8RZCHO8FTx6c1uTVRNTX0caDp8MVuRexgTRSkqSM4BFNLjTYPknWGR0ZxkPuLDPpg1\nzNMqppiR5G0O1RpbpvFXd4aRR4OT15A9KV3Ot3GpzGO7vHkgBBXfudT9avCmrGb+IfaGdPsX8S1j\nWV2cIm5SSAACSB9Tjp2oy41VbiZzZWjRzozeLJK+zggjABx07VHkklshfTI6uJm1CWW8kV5pDyxw\nGJ98cUALuMSssgZcEAEd66+J2rXhzzQTZ6gviAHIViV3Z6VfLcqy4RlPPcZq4sVpbAEit2Jxk8kg\nUFK+9jnj3pSyZWVwpzS+5QM2Nyj6msYWXlqkMQbx1aRj+Rew9aoiyG4OBVEIwxZfT70RHLtKk/ei\ngBkE/Ug49KOjlDgdKIEEpJt+/pViv5vagxglcY56VIt5eKBiyJsR81zeWbjGKwSzdhaivSsY9I2M\nCqd9AIo124KWcnuMdawkn5jRQp5RUgKJjhrnasY5muZ5ogLYD5h719L+HCFsUGKJjRQkGr+lAAp+\nI7G21LR5ornG4KWjb/aa+OzIUkZSc7TjNZhRXXhQCWLzX6C/0aVbH4DR/wCead3I/b+1T5PCkPTU\nzXS/NIh430zRiCPpUCp0OQ27jPpRMN0SfMMUUwNWFrIMcUPd38dsh3Hk1VMk0fJv9WPjKeHT0srV\n3iadssynGQK+OT3byKctn+lFL9msAZs1dagvKFHc4pxDeaXA9y9pHZgz3ELiQJt64wSP6U91OLT9\nYMiXE09nemRiUMO8R8/l45ry58SlyFIx2zJzWl5bOSZIXjDFFCjJPf7VAzu11vdfCZTgAGrqOhni\nHek3/i3F42oQm4hMP5o0wVYdCcYB++aAlsZbyCJbZPFmyYxGq84Azn9j+lB/izncrB5tCurXTJL2\n4AgjBxGrjzNz6URp+kC8heU3CwQoBmSRTj9ql/07p0GMdCLFJLK8VEuF/EXKmMMNw++K0thpM17M\nqTXOVnDMhVhvDf8AuHTFJqlVFezWGc1MzaZePa3YKuCQSOMjt9vrVUUouHhwu3GFwa7IrDOVg2Ha\nRzJGrLklNw5qmCCOCQskjoSfyg5FOBlk0rTFYsFuoAx7f5pRdWMxnX5lHjB/mZeKKdC0/hFUgskD\nMGlPU8dK9TaxGj67dquouVgZZHllYnD7cL2AxQsukQQzhrqRmDREKsmQA3rnPSvPUzp45YV25025\nglktLV47m2UtmVztZenl5o+zuLDUZILTdGZ1BfZIgAC8AYz1Oa3oZSY7uNAvruzilgE0JYcgvwuA\nRyPQ8Vm5r6805Y21G3tpdjFVuo5AWOM9B9qdxNdgsvxVJaSs6yQTxLjaFTz/AHz96nDr0d3CTcFY\nCX8hMe7j/wB2OgzTqLMgYfFdrczeFqlisgVtiSxjLZHoD2IqF0uk6gpu7Z47K6D7VVxsVV9fL9O/\nrR6tBZCyt5rTWZUnuElDKHBQ+RhjsfWnaKDHuGMGizlmVyHapJ71WWL7o1crlaSgRdMUXt1cxI3h\nyDdjbyBnH1pFJeXXiH8dRk84OCf0oxVYi6dkGv3GFW+khlRsgrIeft1pjb6xrdu8Ud7ItxayDBRl\nEhxj16g0z401pm6KlMM83kjyMkjxfMRQtwiLMN4QgHOMUV+OEnoOtsCxMK85zhiOlcjE0bqsqBFJ\nzgHt6/SrxdmQ1jtmW3DSNkkdjwaAnCBhhiTn1ojo5LzHyePWlFypLnJ6VggEo83tXCE2DaSGp0Kz\n0cmGwe9ELLuxjiiKwqF8t6Cj47jYpwPvRAFRzDbnOOOKviZj5scUGFBKSk8sQPSr85FAJKNqnn04\nrBO7vWuFv1rGK5m96q3DbSsKEevTf9KwGP0rHOfNRj4Bkl6V3NMAiWqOeaJjvWomsYsgY+IAK+kf\nDrf9EuawDRxMPvV+/jFYwo1u58KykO0Pxjae9fK7lN9wx24BPSg2Epa3I5qk4HFAJbbRPNMiINzO\n2FGOp9K+/wDwzHLomg2thMFzHH5mB5BJz/ep8jKQGULm4vouTgtzWoSNt27dwB0qSKE98ZBIZf1q\nxCOueKBgqM8YpB8Tq6KHGcY6jtTxFZ8Q/wBRZDcSROWJMZ281g2bsO3FXIlZ60y0y2kkcOkbMFyT\nitdaA+jfC2jXul2M2qK4WUQO6IRg8D1pReavNuhnaYTXEX5pFOSc9ea82DXJyNo6FcY0w1by1jKS\ntcQDxUy3rz1pKBFPfuqzIq5ypNXSp0xZ11wdfBWsafbT3NnqLxIsrAo0q5VuoI/vVNpeSaD8RMGk\ninjyTG8bAjBBwQevtSShrOcP+K9Vt9Tg099PdSqxh7g7hlXb+Xb1OMHoOhrPaz8RIdLj0iCEqkMm\n95t2PEPbjnihx8XVdQ3tnNEeSzxqMlu7QRINw4zgnGRke9MNB1aWfX7fw7iSONpxtSRs8E0ZzUrp\neDdrCvjC9+Z+KLhrZsxSRoHG3Iz361mxqE1hc4Viygg+b34qvGvxBY3kguJIVdUd4x1YDyj70pYn\nfkcYPamM3gRbwzvaT3ik/wDTMoYhhldx4OO4yP3phaTy3mm7bl1XYx2M4yGz/TBzStBTaKby1j0p\no11RQqXMR2+GAXUHocf2r1PGVoSVpmnOqSXSFdCZbaG0XMsjxhWfJ49f60Xp15OlmovfBureZ2MK\n7PMMcHnHf+1cMWVg7wDmsrRbd5l3RyGbKRHI69ce1ItRWC33TN+dn68nA/4KVv8AJUaTd0fV/hm6\ni1f4fQPM7oPwwVdgVx755rBfGGhWdnqUnyN6hjGGWBpMOnHcE1eN0mwxVMz8FuyRMGKru6ksD79q\nnAouphGZDHvG05XIUdqsOcuYLczusF8JhGFVJGQruY/X06ZqkRSIgZ18RTxx/Kc+/WlTC0OdM1Fk\nJWdi6YAAY5x9KeQ3O+LI/KetBo5pIHe8E10bVFJc/wA2Dirdhix7etCiYr+ILV205pYwCAcNnjFZ\neGykJLtJEm0Z5f8AtTwjY8XhB41jmDGdEDjjeM5+ldt0kMgWKQMM8AEED6VesGsLeN4mx0xUfGjQ\n5lUygdFDYrnkZA/iLywA9gP6UXczxfLoN7IQuODnHtRhKjUz1tG0ceWd3VuRk0PdDafKKsnYUShu\nitjJbuqkOwbPcUtvF55FFBFkxxwOveoqM7QM49aZMzKj5JSD3PWrhJnp2NMhGXiXC88Yq6OcnAzk\nZogGEM3mO48DoKMjmwANwrGL45OveilkOKUZE4Xy+DmrJX2nrQCeWXjmuGTnPTisYrlkyB3qiSTa\nmM80GMhNrH/6sT1zWVkBDUYisl2qJNOAiTXsVjHM13AxWMSi4kBr6H8OSZslPNYxo43A+tWbjjrQ\nswp1mOS5hKR4z71irzSzC3PJz2oNhSA5IQqnIxS5ow8np70EwtGm+FdLZb5L5mUw2p34zyx7celb\n5dYutQl3LIUyOnqe9JNWPHDUfC9s8l8ks825T0Wth4wi4OCDzzU6HsCZofmAIFXBOWNHAZAxSsIV\nH0qu+tlurZo2A5FMmKz4P/qVoktqzv4bsAfzL0X6jvXzNl85GKunZJqiyO0lkcBY2JNbL4e0v5YA\nPIof8xX19qnyyqLFXpqtY1uF7JdNtmMTsgaSQdfpzWHiVpNRNoi5LHCsDkHHc1yfxePpFtl+SVj6\n1+HNMSyJ1GR1nc4Hhfyj3rOXMIhuWWJg4U4Ukdap/wBLlRzshJYRtciSKRlQYLBvze+MVfK48bCj\nHpkc1V6Z+FUjlHC85z9qncW6TWedh8UEAMD17YoeeAWH1P4m0jZ/pyqLsiLRW6g9zgAkD9BXzW20\njUobxJILV2MbbgzHb0PvXPwqrQ/wP1SO6PiXEyCNmO4jd09qQzqXO4jd6mulNeAaL4XvIrVtpYRd\nwG616BxcybFHI5PNFoUbaAgupryyRQZ7iP8AB3D8xXkr7ZHQ+uKLgt7PTkFzK009sbb5hkZdoVt2\nFT65Bz7VKd/C3El6yem3+jzyHVNUkEVyHLxRt5lUnuB6V6jBtIly25YMtQe50+5KRTJJHeSbixj/\nADKOMimOiRq8Re+jXw1JSAuShUc8j161xOLXgsZUwWGxdbOeK+lCeGS6Stkkr04x0pPqFtYiCNpx\nNN4cYZ3WTYJXyQFxg9uuO2aMHuHSkn6Nvg3XWhkubXBSBw00Y6+GFHP14/pSbXtWj1bVZbzzHxFA\n5TBXtXWkZqtFMkqklSxxxjIqSSPE7Ojo23+YdqsgWGLpk1+iwRzgXEmAFYZPtyO1XSLHpdjD81ci\nW48V4Zol4aJs5GPbGOanP/BkwPxVLlQGGGypLU4tdXjRo08RC7HHHrWSwjJ6GaVNG1658Tw97HKv\nyB96ZzPbOuBdQkjsHBNBok9FeszpBp0qyOuGH5SetYqacLLuRSirwBjkU8FhkqITq19HCEJG0kEG\nqtOSSK45QqAfzYxT4hkN5naUcnNVx28kwOFOPUmoTkl6NFNhtpZW6l/mcycDAVsYqm+WCIbYVKkn\n1zxXMpNypHTSSIW8nlILDBPGapnHiE7eecDFd8fCL9B5o5LOXEwUN2XPNUPL4jEA/XPamMByW3n5\nzVF0hgAHODWTAwItk4NWI+KohCwv9816OUq/XNEwwimYMCelMLdsrls/WsAKV8Y60ZG+V5pWMicT\n4k61bI25hmgEirgucjgVGWTnjpQCQZvLVDHiswgGpf8A6sRxzWYmTD1kBlZ4qPaqCnK9isY4eO1c\nrGJx9RW7+FpP+ix3oMxpojmrmfCUAgkh3n2FIdSRdx96WXg0RJNbF9wAzmhI9Dnnl4ZQM96EQyNf\npGmGGJFUnjqfWtXo2nxvLumYjHRVHWiwJmp0/bbTIy54H2o+4u1lBTDAnqRU2OjtueenApnBKpwp\nOKQcMGAvBquSYeuKwDK/FmlR6tZSIQCWXAr4Ld/DtxZ6xJbNG3D9QM1aPhKXofdSHT9i/LszbevA\nqFtfTPeoxDwRq2SVTNZpP0nRLVTEuqK9nNJOhGXZxjn0qW555EjhtmhYEZZep+lK6SHovmuDBKYp\n2IbPAagpY/En2gjceCAa5VHbEaDTC0aBmjCY4J4pZdzMt1vPmBqkZaFrCmCZZLjMikqD0U80wZlt\nlDPhoyfLt9x0p37QtH0L+Of+ovga0ikUxRpGIy45864B+h4zWWt7y4XcJHyR5SHbdyD71GD1oaxZ\nq13deOr/AJ0B6A9T9KUG5n8VwQoU8lfSuhRXprLvGzbkHpjtULRfCcyIcEdqZily6nPp8rMrFSD2\n64rQwfEkfyq/xG1MsLnbh1yM8H6d80vgOr+AmvaRbTxxvBPb2enDEokZSWIY8Dp+1eodjXXppL+4\nlhv3s5FheMHdC4H5Fzng+45qy00xporiQz4CvnBbHGAcZ+g/WuNyrAddDbWfdBtffIHC+RgPynsa\nRavpMl8jS6YVllUu9xbq3nVQOuP8etbjdF4yFXw5clZL+Y7lWK2fA/8AlhMe3X9qBVpZJw0jMykk\nHHtXXGVjPwHnhdycvsBPGa9BZpOWTK5bq7dBinEshZXi29+2xCxAKh4ic59a7I3zCMHQynrljhx6\n/WsGyu0neIOzKHX09qtuNzRK8I6cjA5FChGM9Llmv4NssrKX4ZtuDVN7ax6M8cCKXaVsiU/mPooF\nCUU0bqP4dEutStlS/jCFVyrMR/aql+FZHnASdRz3XP3rmjyNYBoX6hYw6ZebGmL7TlsDvUH1aJk+\nXiiJ3n82BTtOQYo6RG7bVIAX35qmefbxExGDjBpHd0UWFEl48YBklbBxnbQ887SsdufvVlBegs5H\nHjGX+1Ws7W4G0jPXNWRgOaUyytI53MepNMljgurGHwIGEqttJXndRMKrsmJ2UghlOCpGMUtvJnlx\nvyAvStEDAs881JTzVBC4dM1HcAaxgm3n4x1PvTKGXy8HjFEwVBJkjcSaZRsCuBSsJ1W2nPeiHbCj\nNAKKUbANckfjjvQCcdwE+lDCQsCc1gg96R4fJ7elZy44c1kBg7c1GnFPV7PPFYxHNe6DiiY6h5Fa\n/wCGLtVIjx170GY10M2QKt3blxQMVSAKh4pLeeZzjmkkUiDxWoLdOtNLbTVADlTx2NaIJMZQJsXn\nOc0daXxtWJJOO2BTMWxpFrhfasEecf7jR9veSzONwC+wqUisRzacoM559aMQYYUg1hXi4TrQN1cY\nHWikBi55PEOM9azup6TDJeFyp3kZGD1qyWEm9Mj8TWMLTCO3cJcKBhZeVYfXtSmSznht4DcKqsy5\nO1sg8/4pXJBUThCM6hF2gjzHG416L5uO5jmgTBRhtZvX1pZ1RkW6tMdSvpri9CvJFGAdoCg8/wBa\nrE9tDdriE3CEBjtHK+xqUEqBLEOLhLB2QtZou8bl5PSlGoWltthjES+JKCxAJygHembSaoWP5LQP\n4bS0j1ZpJtskKqxYP/LgZ/59aY/LLcWO+82LJICSOAQO3StKP5WbtUaKdO1C60mze3s7uRIpmLSJ\nwQWxjPPtVU92xtASoL43E9M49/WnjEk2E6do0msyIglCqV3P4IMjKD2zjg4pfqWnS6XdSW8+TzhC\n42tjtkUO20ZEtKso7i4PzHiRhRlML+duwoXU9N1DTlVbqNreOdiyOwwGH+famUl9GRQLYuii5fJY\nfnXnIzWg03ULT+KtbShjZThIpImA8rBQu9fQjrQkiqdF68ar/B7hCbOC46nkhQTgn1Awa9UaRCXp\nr7vTFWz3SkpLENsm7kNgev0+1KYLn5ox2MEigSnf91BA5+tQSKv9nNVv3WcNb7R4ChXJbrjv+uar\n06K/0TWYNatlXZL5ZG35JB/4KMrUbQI6yj4gZYrDVdQsLYNHeXKoVX+UqNzEfU4/WskZsSjwmIB5\n+tW4Jdo2XnmBIkYwKchiMjkcCuztEtosjMUVvKzIucH0NXItg9sbbxw0Mm1sHcSpX/NUy2U+4sJU\nkXrjJojF8DqqhZI9zY/lBJxR9g73EoWKBlXuSOlakYdTKltaggqrqc47kUDZwpqGs29xcMzwxvkb\nSAQ3bIPahLwxppNbtBM0bsRMp8ybeQfahNT1b5a1kmhQFVG1nJ27T/muTo0BmOurxHLOXBJ9KHju\nFDhx2HBqsEzWQedppVMcm0fzY61ZFPFvxuZsHkt1qriGzsjQyD8pUY79DUC+9FBAznABOK1As5PO\nU8oYEkdu1V+JIy89PenSDZVkb/xGCrnk46D1qxLx47kJbSER7s55GaILGXxGIrhLLUI8MJ4zHIEO\ncMuOue5BH6UqaOB7d4iGEmfIxYbce9LEZiaaPwnKkg49KpU81VCMtDcV4tzxWAeVvMORTG1n3ALm\niYPjkwwI9KZW8nk9KBi0MN1Tkk8p5FKxkUCbjFT38ZoBIykbcZ4HWqFfcfYUAlF0dyEnoBWeuW3P\nTIVlGciitK099U1SCzjdI2nfYrPnA/Si3SMlbNB8QfBtpp2mPeWGqQ3Py+EuISfOr5AOMe+ayR4P\nHShF2gyVM5Xu9OKdHBpjpl00MwKmszG3028E0C4JPrTmFcjmgYhdeVDjrSspvfpSMeIba2eeTxij\nwoVcdqMVQJM4vX0rxO40QBtgAkgz0p7bqFZSOntUZDpj23YbFwc8VeZQppShXJdYU4oC4uC2aokT\nk2UxN5xmp3cAfa5HIqqRJmd1VdNuS9td2zM2Nwlj4ZT9acfDHwRY31lb3l2GkiQMEgfgHnrxXlPl\nfJyOC+HXH8VZP4l/07hu723l0tLa0CrtkTbgMPXjvWO1nQrzRrhbebwwjAskgOAwFGfbxgwBsZLK\n0uz87ax3kUmNzbT5f1Ao6O30pVkWxCtHJIxMZyrY+poQT+CyiKLmSCOQJGcR5wVY8qR6ULJHDcbi\n0yxvs2bgeg9K6oom1QB/Do7RSkAyzLy2c7qJ8V2AWRuqgc07RNgjRXMlzKkK79i7gAOW+n+KDt5F\nS6C3KnDLtII5X3p0xGjUfBt3rmnzXSaVbRz6eRvnkkGFX3z647Uy11YZdPS5SOFpH4GFyUH3zXD/\nACuXo0kVhG9BYIJtKtoNQkkhuXDD/pl8x2Y65HQivpwi0f4w+HYlniWSCXoMYZD0yPQ1xqb/ALWV\nUVZ8v+Mfg2b4OO9mEtnOdkUwGMd8H3rGNcrb3LTBS6Mw6DgV6vHLurFmjW28LhYdRilEj3ib7d88\nb15Zfr/mvVx92m0yDiPYtUvLWzuBdTQXgkyZl67DnouO2KQp8i7vIG2zgZWNBlTz+xqid6PQVZxr\n8uxeEYYgEuwGO/TvV1wWaGOMg+EV3BEOMjOfTjpVGsAloH8SyXdpFZ2thEqwrAGZWfo7+bn14xSi\n1jFxagXMcS3Cnhojwc88j1puKkhpsl4SQWM0TiMqzZLlCXHtVkfyQsQi+K0bgbgwGM/SqKWkmy3Q\nPkLK9liBMhuBjzLgA+lXarpdq0X4MIjYZBx3NOhotig2VxApCEoMVQXuI2/7rj6HFaXg6PTzStAz\nCU7x0J5NWQtJKiecrgYIPH3qMXY1BKptKlEDMpGXPXFHw+Hc77Wf/sTsVbIBwSeDRlKhG0A3tnAi\nskcZBRdv1pLcRME2Qr5j2zWjIyRGGzlSIySALj+UNzVJmQN5EIYdcnrVkwsks8jnb2HY9qKVA2Dx\ngdSTWABTHxbjapCg8ZJxTKQrb2yx/mcjoen60xhZKGl53KB0yTioJOsUgVuRnkg5/SsFGxsRp118\nKEW8E+xbtfFVmGfytyD26YrJXkyR3G2E7kHGG5xUoN27HfgukRpQz9l6nPShs+lWRM6Gr2eaJjob\nmroZNkg9KIBijnjBHFNLeUvHj0oMxeJuQKk0uRg0rGQPvwasEmBz3pQkZHLLiqdxHlrGKNQn8O32\njknvSKRvNToUh7mtP8EX38GubjVFSOVoU2CNs5boeMd+vNCfgUtEWqX0uo6hcXUnBuJWlZR0BJzQ\nZ5oxVIzOVymAdzXUkKnrWMPdB1R47yOJ2Ijb+tfRrb/tAjuKwCq5GQT+1Conn96RjoapEFRfpzUW\nPPbFMgM9ldvNV7xniswBME+1sH9aZW92fy549alJDod2NxiPk5560RJdjFIrKXgLJdZ4FQzkVVEm\nycf5hR7DMPrxVETZgtSmjh1h3kv7dh+ZQ35R2wcUVY/HclkPBdRJAjHDRH+ma8mXE3yNo6/+iSSD\nbf8A1BN7OscbSxsT0PRvvTHUNf8A4osQkvLezMOcmVAc/encHFUJ3Rm7nVC+nXU0kiXPy4ZFkiHk\nLEcHFZgXDQxM8UpZiPLkcBvf2peKLQZSTFjWnnae5uFnYkEjJBB4q+ZCpkbI8zgBRwMGuxEmeicN\nsUHaTwNtSeEO4fJ3AbW96ICaB0mEsIAljYOpOOoxzVPxAnz18Jre38NWBJ6BmYnJJH/OKVJ2bCnS\nHu5Z/wCHwTPEbny7SSqMfejNLtLvUdZjsyShaTYzDnaB1PvUOeCkwxVKzb2nwxFaX7iW/IgHl3CL\nb4nHTn/nFGfBFtaXv8TsTPIFtbslNsmMqOhrhXC3jLtrGaf4o+El+K9FjsJ7mRI0cOrLjkgEf0Nf\nFdT+GV0bVJrFrgSGJsEjvXbwt8X4snyeWGaSU0+zm8cGWK3dJo9v8pLBSAOxOc16s6bbJemh1fWL\njTNOMsMPhOrYUvHyR9DmsoAwna5m/PKC24+p9qTik5oCYbb6jNJeRzTbJXUBfMoPTpxTHTYzPqEE\nBuJUSWbJVD19R9K6PhlLSOq6zbSavNcCxjlRpMhn547dfQUfb/EWj2wCPNZxPjJUAcf+a0UCcrYo\n1zXNK1a3HydwIplbgsuFb9OtI5GRLUmUhDnpnGfp61qpiOwVpRuRoSdxHBz3pj/6kt4IEjvsyyDj\ndGOB9fWrW0isI5pxfiOybqTt9SKjNcQXIDonhoeAWHDfSk7tqmPVHRsAxHGCcYDYrsNtJK3PT/3f\n4pYgbpBRhWCMFvKAOeKgZlU8MgBGcsPvRatkU7ZHUyzXCTxqDHPFuGOgPQ/vSWALcSyKBkg4zikW\nM6KwjIJ4bgiJY+B1bnFWS6el+0SxPDHKo8yt5d5+vbr+1XixCVr8PXd6rtbIXVOdxON2Oy+ppbKj\nIzJLlHU9DT0KEWdlBKhkuioQHhehqi7wr+XOwcAE5xTGAQfE8mSFJzXlKoxUc89awTQ6NP4fwnq6\n84klgXC+gLMf2BrOXbrJcSNEu1CxKj0GeKRLWO/AcyMqFc4B6iqe9OhCJr2eKJjoqSnB4ogPoOl/\nCump8Pxale3jzIyh5BbkHwsjgEdfajNNX4Xu7ckt8tLKxVQ8x8nHBx39fvXP3lK6LKKoEsvhz5uS\n4V71IngfA3IQrKOd2ee1Rl0f/wDmWHT7aRmWdVYPIm3aCOv0rPkp0bqqwE1jSLnSJE+Y2tHIxWN1\nON2DgnHWgPGyOvFMnaA1R0S7h1qmSTB4oigN9LuwM9qAanQpO1tpr25WC2ieWV+iKMk8VvtCl0zR\n9Mu5rgNDa3tssMjKfxEkAIYD75/Sp8n6Hh+z565G47Mlc8Z64quqrwRnOhrhOTRMerw61jBdgcXk\nZ6eYV9YtD/0kfOfLQAeILt2qccWDzjJoDWFscLQ0hCj60RQeSU+oIqsSc/asEsEvGeeast7gK4wx\nB9DSMZDe31Ro1wWU+1FfPh1OGpaGslFNvbOaOjORVETZclFSvtsZT6ITnpTIVnxnUZ/mLpmjwBuO\nMNmqYLuQFUdfIcjI6iuRx/Jj3hJ8+OrqwAUcAVKW8mePDSvxxgms0IULqPgxSIoJMi7c9uDVURad\n4lIRUz0HGaKw1h8pEyxQBY05wSByaldI7LIfDZBncCf9opeyuhkBGBIYgYJg3IYgZyO1MYmLxEOA\nTngg012EnA7o7C3B3vlQVbGSe1B3Ebzr4VwngTwtuDg5/wDFb/wxdaztbSmYSK0pXCkoMipabrkl\nj8QQ3koWQFsNu9DwTU3H6BydUau78W8vwxJOxQEQcYFJALjQ9TfULGcpJNnchB24PU8VxT5XBh7Y\nab4d+OtR0ye1TVZIbmxnk2LNHndET657Ud8XPpbayZpzEWcK2IwDkY65qsJd14UbUomUl+ILKD5i\nO0tosTgDxHwRx/7TXqp0JJVgXrd5cvpQ8ZY5VlO9iD5h71m4wt1lQwJzzk8img2kIQk/AmXZk4OC\naZaFM1ncT3FwztiJlgK9mbjP2GTVHqDEqulhW1llfiONCf7Ck2i/A2p6/L4wiFrauc+LKO3qF7/0\nrRwaj6VoPwbpehoCkfzE3QyzAH9B0qPxrY6e3w9NLc2aSMo2xtjaVY980L+jeHyaa3urNztVZkC9\nVB/tTDyT28CX8dnabxgq3mlz6gdvuRVYSUlYESt9PgivFSCGMA//AJZV34+g6UNOk1pfyC7uDcDH\nkKtx+lO1aNZNL1WX8NmRx+XPej9MkvJNSit4Y47ia4YKrscBT/7vapdeqbCl2dBGv6bqOkj5y/vL\ne4ifyots+QD6VNbOO602O5jubaUmMu0fiYkXb1GKlHk77RTk4VxuirTbaS+tJobdGdVl8bxHX8uO\no+nSi9A09bnS7i6FpBPglvDjlKsoA449KZ0K3gHJZx3BWJ5JVmYYWNwADzVcPwzZ2d08k+Z/C8/h\neJtXOO5p1KhE0yOuai2oG2vrISW0kHHhIdqqPUYpHcTvdy/MSsWdgC+4D0qkZWZqiguxQ8nGe9Dy\nv5dpPIqgAbLFxRcNtNJGpVOWJwX8ox9elYwyj8bT/hfURt8xuYkzn/2yfrx/Ws+24HnOPXGKVDs4\nTkVUTTCEM17vRMSFeB5omG/w98QTaHfLIMvblgZIsDDj0qM94lxdSyQJ4MbOxVc/lUngUnWnY14X\n6frF3pl6txZzvHKp9eD9QetayD4gttX1KK81G6EVzcR/LuImCqgGBnJ/LSzgno0ZUEa/oUt0trJF\nL81IgMJ838oOEPJ6njkelZrU9JvtIvFtLuLbOyhgoYNnPTkVKM/gWvoH4mCQenpUGYVVCC+c5Y1Q\nadChWjajPpOrQXlu22SJww5wDjsfY9K2OsafZTW2oapcY8KeES2sCMQ29/58emc/vUuTJWV4/DBG\noirIkzh5Nc4omOE13PFYwZpUZkvYxjPmHFfVLfIgQHggAYoACUFWqmZA3asGzshwOe9ATFjnbyaI\nAY/m5OKraUKeQaDCjwmyMZP0rqu27g4pQoKikYtz+tMYn8vNYIfbHvmmcTcCihWEowzV9z5tLuBg\nHMTYz9KZCs+NeJbStsW1WOQEgFGP9DU0tZ0KB4nCdQduT+1c8mkx0sOiJniJERBTqTkf1FK5bjMr\nLjJBxQ9EaZ7w/EiKx4D9duM/errK3kTG8YA7nk0Ww0NLU/KypcvH4mBhVI746/vU9Qlm1ayWSN8+\nDHtaILghT1IPeuPkX5KRpZ4BQhFt1izl5DuJXpijLImKVc+YZ8yqcfWuiP7Ci9EWaeTKukLAiMnG\ndx6Cl3yjQTSLNNtCr4i7+d3qPrmqIL9PK8RXysMntnNDXdvJH5kjLjqD2FZqwUaBtWS/tYrol7aa\nJAs2W4PoQBUJbiSeQbiq4GSTg1yvh7S0DeATvJA+0sCDny9sUXHF4yIyuZAdqsuevtXR1jBYInTA\ntS0q50m4f5SRnceUo0QyoPoSa9QjJNWijVj2aziawjW6uPARJPDkH844znHpQUkFpHMFspRJtGFO\nAp57GoqSWMVgJDPMysrllboo6U5ttOSZki8cL/uVQWOT06dOKbtaCkT/AIZpGn3RN9HLeF2G1N+V\nLdsj9a2cVzH4AJ2xoq529AoopDonaX1vd7mhbcF4zjisZ8d3Zm1CKzk1FLW1VN0kYyzO2fQdePWt\nVgZm/mrS3ieOzSRyw2iVQC556Z6ClEtlK8pBgZZG5LEniqxiltgQC8s8TbVmfynAHpUw8kilpSSS\nOpPWqWFhFjAZUaQSIqRY37n556YHHv3rQJ4OjajDcZkkiIWeHOPxBkcccDoe9Tl4ZY7FVxCl80zw\nTr8rE2SznaQT6KTzQSo0LfhuGUjnv+1CC6qhpS7O2PvhRJUuQ0+Vjc7FUjtjFbPTtStF0GGF8G5w\nVeXZjKg8A+vGBXNyJudoKkq0zWsyxwa3DeNlwvCIfT0FAX9217E7wr4Ebt/OOfpVm2Rbov8AAg+U\nijlmmBHKlV4XHUe/3rP6lEsERKlispLKSO1UgynqFfiNs2BWyTn6/SqS29+nT2xVwUXJFkBwCTUG\nfc3mz9MelYxq4bwR/wCncs1/vnC3MawKwBDMA3BJ7YJ/SsiNXuQcRv4S5ztjGB/5pIjSYK7M7lj1\nJyagVPoaoKcxXQuTWAcNcA5rGLNjd6uiXoAeKxie4L19aqaQhs5rGNF8OfF91pU8MVw/j2iSKwWU\nFvDwc5Xv9ulbPTY3m1XUNa07UYb2WeKTwxjzM56KQeg9DXNyKnaLxdqmY0aVqWp6wYPBf5qctIQ4\nCe+fTFAyQyIH3rtKgg8dKeMr8JtULZPrmqiasvBDg6896+j/AAhfS/Fmk3uiXjRovgBI3VRnGSf2\nwf1qfIsH4/aPn19CttfTwo5kSORkViMZAOM0OadeWK1pHtXKIDlerGGWhnZqkLEE819Qg5ArACF5\nko5I9ibiKxgW6kUHnJNAvJuPTAomBJpAO9AvcLuwW5oMJJJgfQ+4NXLJ6UAh1vzjGTR8RwKBg2Ny\ngBoyG6LYB4ooDDYnz70xGHtHXpuQjr6ijf7FPlN18LatbSgSWcz7iSvhebOf6Gh7dLqwuGgDzxOi\nsSCcYPpXI5Rk2iupWEWcWpXOqfK3E8j+JHuKyMWCj39Ko1L4Tka4EtiwSH/8hkP5G/uD2qD/AJMI\nSoyXZEbLQrq3vEM8ixhjuBXklfSr5IrWG9ljVZklA/M+MDI6jNUXLGbw1FKzR2aHDSuMZLNg+1X2\n7WBsDIJT4jcflpmhWgKIILouC2SNpOOMUQtwyTfhKDgZJJ65o3QyDraze7R0UqoXz5YclgOg96Db\nSZGjiuHkHnDAJuwVPoR2NBTQWvoPb6FHDKrGQo7YwSRtBNM57Ca1u5IbsqsSAkFctu96fsKKgyzs\nfChd3Q8tEjdOwxTFdOuJ1Ajj2tgbgxwRn1FDso6xGtB2sb2C880BlaN8ZByD+tau8urK9+HBLa2S\n20kLAGRVBG4DJIPfpXPzrvVPDRhugg/il1NB8/p8luxUL4yrwR/mvV5fLP8A4y6xZRRf07r3wvLp\n8sSi6a7EhKjDZbgZ5H0rNJmKbKbjj8uRg/pXY12JtGh0tRf4W6aOMkZ3k4yB6mp3zHS7loSNrjBB\nA4PvXTxxqJrEOoarIZS6kZByPYiuf+rbyO2MLGOXAwWbOQKt8GTND8O6lOI8S7FaQZQDP5fej9Q0\ndNRme7ZIHcpggpycDrk/QVOW4Bsz0wltLZbh9NnWCQBg6KGU+/FAXnxHaFNkdu8jv5SNuMUsUzIG\nf4ciKiS2kZlIyS2Mqx7YoK+s2itIVUAvLIyEFeRjuK6Iz+MZoqh0tpHHiyLEy+g9KJaO4uLcQ7/w\nocsGkbCqD2pnoosumQyExkbcfynofaj9EUyX0BmUeBnzn2rNYY0bOh+ILScyZhXAWJOjD1NCyXLO\nFCjamcAlsYFTjH9m9DbqzjuzE8rbXj8y5PFBzQo8gjLodgBYldwH6d6dJAaLfiPVre2s0tdsIdlB\nEiKQy+x7Vnp0XVLDxIZNzWmCUxyVJwePqayVDoBu9ZuYruRLeXZH+VQnTAqMwjvbOO4jjKTI4SU8\nBTnODgfTFUTMBiUdM9OnNSUhlyyg46c0wBxrt0n/AKK0GzRjlxLPKD67iAf2NZmS38HaXbG5QQSO\ntCOBZIOqxkrwQe9cM+RzjH0pxSIKOeVIz3FWCAjcyedQOorGByMmrEUY57VjHd4zg81wS7MgYxWM\nRebcc1ENmsYkjYIpvpN1NaTiSFjuHRex9jQasN6fQvJe6DHJ89HHeSxceCMtg9UYDmsfrANhb+DN\nBLFLIMgyLgGuaH9mhpO9M0Tz3+9QbnmuoQttLY3d1HArojSttBkbC/etDbWV58H30ksqxyM0LGJ4\nnDjPqcf0NLJ/AozsyzSbrh0ba7HLhfLuPahjRXmGapnjUaID1drGDdOnWC6jkZsANzivpdjceLAG\nHfFYAcr7fMOxqqfUnUnLZ9sVjAj3zyflA496Hkum/lI+5rWEX3V6yRlm2hR3rM3utySsRGAo9axj\n2m6pMlwAzbgevNay2uFk5BzQYUHw3ASjYbjcQR3oMIyWVBA0sh2ogyT6Ck+mfFUeoajJBEAoT8pb\n+aihWay0u/KCwp1ZzLINpbaGGD9PUUyFZ821O81v4e166s2vpyGbehL+VlPTH0oaya9kl3NZvIVf\nxDIW/MSTn2rj5Ixi7LwuSwfadqSXEkyXDW8UpAwgI3EepwKI8spdcghkJ/Tn/n1r5r+TFr+S18Ke\nIWI4W9XcCvlYjBGaIktI7uEK2VJbJY8mr8U3xtUJ6BfIWnzbR3OdsS5wVxuz05FX2fwnaTxkyStt\nfLIkbgFSPXPWvZjO1YrR4/DK2lk81xMCrDhUYbl4OCQffirbm3srm0txaWiW43Es7SZbgdMUHJiX\npBZJNOhQS2z+HcMDDJnIUjr0/Srdct7fV3iW1WNLxl3JIchJMdVbHf3qcbq2Vu0RsYfl5oI44od0\niEO9x5o1OepOOP61TFqMVlfNLKssy+Jtd48FAuecZHTg00J2rFUL9GWmazHBpup/IqJvDPjqkqBf\nIPce9ZWHVZ21Oe7uk8ATESEryMEcccdsVZJTVMRppnLzWHazQgOoKMCxPL8/m/TjiibSA3ugwxxx\ns+y6LBVH8u0df0NJKPWODJt+DuH+MeBLumkRbeAyKHXIYgZAwD3r1cj4I8v5SQU5/TNDV54i1wJJ\nPHRcLIBkjtVFpf3E0q3Eh2uudrHy11y4VZKx9LcQy24aQAGWLY3GMk9TQF3DKPxZLlZFxhcuc0Yr\nrgjYI9kt3GyqxSUc8jjFCw6TcWSS3M6lwD+HEq5ZvfHpV4hTNNpFxYxRBjc7GK5dpV2FT6Y608XV\n9P8AB3JqFo+FP/5Bn96m/Ri3Q9Qjl0i2EMqMqxheG5GOPX2r2paLp2pJtuIo43Y+V0ADZ9femozw\nxd+LWwu5IoZWkZSBkLwaLt0hnjik2RzhiVIYgFenSkaob3wW6hDAHbpEp4BU5HFZlr2aU43BgeCK\ntDUZqiSvut2hKLjeH345+lSWWaC4DRthMg7TRAaPRRJc3cSuRu37lAHYdqqaVEjIAU7ecHtS/RgD\nVNXuZlEagkBcEqvTvSiO/ubaRl8SRUkwJApALDOeCe9OkgHJriS6uD4kjOoYkBmzii9KuZLa7VoS\nA0oMZDdwf/sfpRaMiNtoF9eQrNBbNIsxwrDGCc896t063mtpb7T5VdXeFwUHXcvmH9KHb4PQlaPc\ncn8x5wa6xPhHsT2phPpo9Ttkmi0JwgaCLTw8jHgfnfI/eszqtw91cKznjYAqgYAGBihEZgiIT61N\noyKcQjgqM/aiLV2TcU645omLGhEq70BDDlhjr7ihy2OcdaxiljzUaxj39K9WMWRjcwpnbsYwD0+l\nBmGtlqUyjw7fcj53BwM4xTK/t4tY+H5b67uJjexDZHCG8jepH+Kk8djR/RiipJ/93piuzQSQELLG\n6EjIDLtOPXB61W0K0V9B1OKa/D2rR6bfMbmH5iCRdjxlscE9a0vApjX4mv7OHQYtMtFA3zm6IjOV\nUHgDJ5zjBrIkUnGmloZO2RaudqoKertYxJG2uD6Vvvhq/wDGsBubLg9PasYeh8rz3oC7lwxG7ApW\nYCX1yaD1G7FvCTWRjL3N7LKTudsemaHU5OTTGJRko+RWs0lXW3UnOW5oMKGS5ZuhFH2ed4BoMxV8\nY3rWnw4sCkr8zJtJHYDrWHsb2WyuUkgYBk9RmigM+ofD2py3tqGmXDH06VpLaQqck8ds0yFZkfjD\nUrV/iCFpU8SSGMDaTgdc0sfX7y6ghjXESQ8KI8gNn1qHJDsykW0sEcvifNNLEzieA4bB8xA703+H\ntbubiS5jnYXDLAWhjPBZsj79MmuXn4YzVy9NGVy019hLa6feQ3GoWaz20kIJAOSucZ+lPLvQIZWj\nk01ZYbe4cKrTLkc9Mc9M8c1x8X8eKXV6yuJmbmtZRq93C0fiCJhE+AcZH/BVEtrcNbpNZmS3kiky\n5TzLjrzirdXCVDKrBBOUSNpoYzvYO0ksvLn2GOntRc1oxsY7iKPCeLktGfTt9PpTSdC9E2V3twt5\nceM+87RhUDYVM9cCi9NU3DOxBRVIZirYbPTj0rjlzyWFFBRQvvNV/hl9Ms5fKSAoVOWkHfHqfvU0\n+Wu5p2tWcREB3DEgknsf1rrXJUO9CvSCN4F0IUihTJ2gydCM459qayQTX13/AArUI4mk2mS3kZ1K\nHHYcc8dBVovtFMFYJJL+K1gjgvLPMlvlUV4wu3vSqDULqSWTz+HHI+8oOgzmjVsm1Xg9guZ000yI\nERUUEyeMMgfT616s+NMFie2uAkUgI5PoKInm0/UbYpcQv4igbDnHIpmmyDKTIz2qq7gYJAXHShRI\nwThCwXvRaFkXfM/Lbmc4wuQFPU+lTtvia5hBEu58+wOB6VraRkHtqUdzGtyZQoA2E42kUGdMj1KJ\n2jYzxr/Mh6/ekhK9Y0XorfQ4YJ0dpp4oww8UZ5K98e4961i63pFhHBbQTPOMbUYDOD7n/FVUk/B5\nIB1aYTOFiXaCPOzD+lByM1tZxtuxyduT/wA9KDBHD0N0Li3ktykbGbALE4K+4/xWVvbKSwunjOSV\nbg+ozTxHbssi4cFhkmrpCccKeOp9KYBfpN7dWur27IDguARn1rs0GGLSSBNwPQ0roKsuvpTbafEs\njgjZlAOCaz1xMDJnv3owsLJWwaV2bnOepppZwqfCA/7gl8xHpxj+hFNJmXpdq8qR34Kx7BbgR7VO\nMEdcffNettS+auluVVzdROH3k8soGMfYUq8sf6V60lvpNxPbKgkEwDwuDnANZ9s8becgHj1/4KaL\ntWLJUxzqrur2dqzP4cFsiEA8ebzEf/vUuns3ac5V9vTkenH2ooxbJBHGgP5sfmAHT60M8TldwgYL\n13c1kxGcSJZLaTsV8w/aq12qGpjBMMo8QFSVCgk4+n/P1oCQjqOAeaJio81wVjHcV3FYxbbAeICf\nWnIsJZIPEjQso6EUrZgzSYXtny++E55VhyQf/qtHqWnXst4yaJLa/JXIjm8PeAUOAO/Ocg9KlJ0P\nEGuLKI6m1jepaC5XEiy2fDNnuD3YdxVPxJard/DM9zIDJLayKEkkJ8QIeOfbNLfgzWmGxx7DvTvQ\ntJ0/UbKc3l68E4YLEqKDy3AJ9s+lXk6Vk0iv4j0dtM1Hwg/jLgDxFHUjjH7VVqPw5caVplvdXU9u\nGuMFYA+ZAD3IoKXgeonYVGnFPV6sY70pro2rtYuVXGG9axjdWd0JrdWDBsig79wZcUrCgcudnlak\nGsTMXKk0EzMTNz1qQXj29acA90L4bn1UGU/hwKeWI5b6VrRpq20YAAAUYo0Cz0cW7nHAplpNqJ7l\nVGKUYSf6obotQs7XbtjWEv8AUn/6/esXApaUAAlj0A70fAI+nfC8TRWCK2cjsetOr/U7exsHe4D5\nUEgKcHNaxWfM7y/a9l8WQ7tx4PGcVpfgjTbbUr8C5kjVo2DLFK2BLnjr7YqcpFIos121htNak/hq\nxN4qgEJzg9wKEu9Oht4LG4tLiS3vBktuTAyD37VFywzVPDXw3cd5pcdrqGI1CHfLtxnJ4I++K019\nqx0rREtZhF4KRqiqFzJuA/Nj61zxyWD7RgrT4mtTqDLcSiXZJlkA85P8x5q251LShBdHTdQeSaRc\nlJI2wvqOnpTODsohXbarb2YVYpBMW/Nldu0kYAGf619Gm0mC1+HLczwxkKisq92JHbHekmg0K7j4\nXXAkWExAqMDFL/CttNbwXypcE7yDgkds/wBq86cJPwZyQk1+c3MYDQPHZqeGcdW7H2pWt1JDEfl2\n/PwSRXbwcTlx9X4LcUyy1ulWJhch5AG/NjOAaLNxc+Mtu8yJCjeRgM7Vb0PrzXd0SVC2M9QexluR\nJCralLjZ4lwNquRxgYHWu6Z8NLda+YtQh+WsxFkMowu89Bnjpn9qn26rQMI1L4X0fTtHmaWzlu5A\nGQSwOTtOP5lBwK9WjNtaRktMde6Tc2s8qFcFDnIbysPUUCbsl8IAcDkqc1ZIk1oUHc2plbzlR5Yx\n3HrVdjNIZ2DRgxLy7KDwKLM44cvhLdMJYhmDoo2gMTXEiaNgzsqH25NK4moOsbRp2zjAPVnyM1Nd\nXaxmaOxG5UyCSM59amkkZYPJbjRbzT4pTMkMzLl4zzg0huktEkUWijCHIZehpacXg3Ypub47eQST\n1bPJqm9kxb2/IOUJ6+5o9jIWPcsm5RjDcZIzUkufnpljvCFcYVZOuB2z7VSLGLGs9sw2kKA20Ejy\nt7iq72VgZVRVCIMEmqhJaNNu1K3aWQACReQvYGmutx2JnmEEcgkicggYIPPJ5qU7Uh4eaJ9Y2PYx\n3CBlKN4W1jkeopKMSPnoDyKeDwEkNtG0y61a/W1sk3uRu9sD+9aa/wDhiTQbS2uLiVVmlfHhHqOR\nitJ/DRX0Uiym1W7dbW3E0hcswJNGRhdI1K2tpLX8ViN53Yz7DH1Nb4ZvQfUks9W0wXM0ZhezZowI\nudykkqOe/WkAt43uI47beS7AIGHPPQU0cVAbthGrlo7+Z7hlDO5YKp5xnjjt2qd/cTqkEvBS54xE\nwIJGOD74wa10H4NLJLjT74fLzNHkYbI4P2NVX1vPc+I88xYdySahHZWSbEsReJ5IUXIZGByO2M5/\nb9qWsvkAJ611IKLGIjtFXB3ynJP/ALR2/Wh3ywBNEJXiukYrGPZ5qYA21jF9qm6QYrQ2FlOkiBJ0\nGTuww4+9KwF8/iguWkDPn8wP9KRTzTR3HiJIysD1Bx0pV6MWaJrs+h6kt5BHFLKu4AygnBI659fe\ntTYayPi+xu7KdEOo3EOxE3bQ5ByCMnt6GhON6ho+mf07Q5INUul1GLC6chkmRs4YjgDI9Til9veL\nDqi3HggKHJEY6KDngZprsDRpdBbTruVnnZ4hCm6NXYbWODwfvWRu5pZJ2MshkZSQDuzj6e1aK0z8\nByOKgRVBT1eoGO9a8CVOaxjQfD2stFcrDKwKHpntTm8k3SEg5z6UrCgcStjB6UBc6e12SVbntSJh\nBBoV5kAR5HTNaTQvg+FHWa+YyMORGOF+5qyEZsIUjhjCRqqqOgUYxVdxAGTpyacBLTdOBchkzkda\nc2GmLDdJJtHT0pKDYl/1T0f5vQY76Jd01o2GA7oeP64r5/oNkfmC0qHA/KTSthR9I0VVSNS2AqjJ\n4pH8W3MWq3ywbH8JV8rqf1oN4I2Za4tFii/BXGzueQa8mXcKjAHGMA9/rUuwykaS2/6e1hx5ZFBw\nRzgetQLPcGCK4mkdVYbFdiRz7VNjr9jOW9nVpJAqlY/KBkeYj0BrlxqEl2B48cu52C72OSOaRQS0\ndO8KbzRLSO+kkRwCSAJAOpOM1Xe6RcNhbYjdnC44J+tUTsZyolpGjXkd8f4jDsiCElsBmOOgXHc+\n/FN/h7Vb5Pii1fUEu1s4s7YbkE7PKceY9ecf0qUlujxkmjcyfF+kRo0MrSPuOf8Atnp6ZrOa/rFl\ncxK2kR+FMgJHinhm9ealKS+COEl6JITcz6HdwSiOcctI5P5STnj70JaWy7EijUbVGORmmg3RKXpc\nnwybMESzNJHLyAOgqC6Ci3Rm3vkLgJny1wfyP50+KXWisPyVld1b+Bp/ykviSQCTxQCejHjNDrdv\nqV1DbXUswsbVT4kavjew6Vbg/k/9dYs0wzSfiq60i1uobMpbw3AIG5N5J+/evV3rULQmu7KfTb6W\n1nkmkWI7A0meR2P6VF1jisx4EUSEsc4X81UTtCfQUH5eVJpjsGRuB6k+3tRtz4k8eyM7LYZICsM/\nem8GQokTeCvRh0yNtTTU006RGaMM6DvWasVo0OiodeHizMyQeidT9Knd6QNDu1eKbxYZsr5xyp9K\nXrpOToBlt1YcKoPtQrW0gfrx707ipEO4x0H4ZOt32xptkceGc7c5HoPrXfjuDTodYggsEWNYotsi\ng5AbPNc8o7R1xa62ZJ05yeBVTNtbIOCOAaeBkEw6m0cKxY3pncQT6eldvUWa1FxG29CcE91Oehqy\nCD2bhLyJW4DH/BrQyRRXWs3Sjh4pnAYN+fBJ5qc9Y8AGW2Vw9vcAbJGD7vQ4yD+9Irq0WzumWN/F\njXgP0owDNI2H+m+s2GkXt1NfyeGqRZ/LnuOn60RrWqfx7X7e5Ry8GSYgwHAFCS0CeUKk146JevFZ\nhJMf9x/XvVU+ryajqcdw/Dhhj2pvgrQvaeQXDW7PiKZyGPueh+2KjYqbS9e6cgC1Uygf+7ov7nP6\n06ALd0k8rSNudj5mY5JNaDSbSO32/wAQiaO2uR4iSxkMUZOc4HTpjmpzbrCkVYdf3c9zZPqVtJHc\nRKejR4kOOrbR/L2zSiW/aciL5gziSIEBE24c9sGkgvosoIGghuLPU7cXEbIZDt2v6HrQJhZ5SoBO\nTn7V0J2JVFMxMsp54HlA+lUlce1MY8CCo4rhQVjESmDUlU49qxgmDhsrTS2juyFciTwgcZ2kig/D\nB3mmwqpznAC80HqUCRjHAI4x71NBETgbjir7C5ezvIriMkNG2cg1T1GN2msTPN/6g02JXk2hL2Fh\nuWVcdcfbp7UsvvhKTWJBf6NFEltND4qxmTPmB5Htyeh9KivxdjXYjVJtOe4gmjKOqlGRuCp+lKj7\n1ZMRkTxUaYxE9a9WMeHWvUDEo3KNkdRWis9RE0SK58w4zQl4YKJDNxRFsgqVDDWzjG4DH603iXAA\n7elWiIwmOjFVSozg1RADraNQOMCmEfkXFKzCX4zudug+F2nkCn6Dmslp8SPII4ypfOACcZqbCWDV\nX3NAJBFglfN/9UMIpZ/EKjeqcs2RSvwRgsPheMA+FUtk0zlmsbWNQscbEjKhEBzUWgA3jjeDLiPH\nRenWpQzRC/iUEuCcliOOnrRotErvFe8gSRmR4skqD6njmmujwRrZpJMpjjDYBDHy4pZPCsY6Fyok\n/mX8Rgu7y9APU1Ql6tsFVsnPOTyaEfBJ+0GW2rRSckt5T2pva3EZD7UkZWGG44rSJLGIvimVdCuL\ndo44AkyHAlG6k1tqbSWy26hVhAIVV9Sc9etBJUdPZtaGx30Y08rHcqZWO1ogOcA9aN01S5yqltvH\nFSqiEhxJBetbCRYC6r2B5rinjA25AyR3FeL/AP1IfmpF+DFRVPEsqlWUc/7hWYv0tNMHhCdTKSSy\ng5OT61z/AMGck+sSk1aA5bcGCTh/KA4IzivV9LCeac3U1PxHB4RF5fFyCdoTbnH3Has0+2WMB/MO\nvXGD9qjPmcVSDKNHnjE0TMWVnK7fMoJA9qvhs7Gw0aSe5jkuLhXBhWN8EjHfPvWhyyb0yFcyzxWx\neR433DPrk+gNAt8tdMI5InSRuhHI+/eu+LtAZsPh97LRtOS2edPE64Jxn6CuandpfWsaIsviCQkk\nrwB2/tQbojKNi6Lyja3WpblfrxjtVIs56ItqFxYxsbWZotwx5TjNKZGaaUbvNIevcmldFYt+Gp0X\n4G0m7sUbUNQKTzICESQeT/grE6/pw03V5rWGdJo42wsgPUVJXZ09aVlUdptjLOTuxkcVK0kaAsVj\nLq35lI4PtVkANs9NW4vopYTtjY5YOM7D6Y60Xq1jcWWqyGGbInkyrY2jPWlbGimetmj1SN4Xbw7y\nM48NmxvOei/+aSSxQyXRWRXTa+1weoNKsY0mOk0GG206WRWcLIyjf2x1P9BVdjdrLc5jQ+Faxs2R\nwSQDjP3IotsVCWSLCv67+oq63n2ldqKJEYEOTz19KZGZGdCbyXPmw5P96K1CONdLhG/Ml4fFYY6b\nSQP3z+tGzCoApxETjqcVTNMScZyprJAuhomvtZaQlrYmSJpUZLpyciQHOFA+mOaXRwlbXxd4Vg4G\n0HBz1zRSr0zlfhbYtJc61bNPI8hMg5di3fpU7gfJxSBGBd+D7CtdCi134wKrY7voKcx3aEXPWvRD\nfKBx15rGLLpQsmBg/SoDy1jF0LYbtjvWjhmvp7ANbLOtsqHdsY7eOtLLwINDK24FWIPrnmq79i48\nPGT1zUIy/KgiWSMqxyKgn5wMHJ++a6BT6daaS2i/C8EocP4pDeGVA84U8ZPrk0pvrO4isrnbbS2s\n0x3PFuK7T/uTH5h7etcz9GQr1G9GsweLJbst/GAs744dRwG9c8Cs/JbSrGZTE4i3bd5HGfTPrV44\nBkLi1mtsCZDGXAZd3pVBXBpwHCK5iiY5XaBjh61bHKyMCD0rMw2s77y+Y89qd2kofpxSNDDmzPmG\nKaIRmniKy8MCMCrEZgeacQY28p25zzTCJ2btQYTI/wCoN60VzaW/lVVjLkk88nHH6Vh4LyaC5SWN\nxuU8NnGD61Jsw6trm71JlL/iPu8ztjA+pqTafcyySKrwMFPO5+lLrAxcww5zgAHa2OQPcVemy0vo\n5UZsxPlT2Ye9KBA+pGa5uZJ4wTHJllXPK060u3FvZPJKirIiLhicjmiy0SJUzTeHGxJ9Avb1xV2o\nSRxWMFl4skTM27OzO4d81KW4XWA1v8/p2X00i6JfcVb8pHfNCXV9diUtdrHEWOfDTtT1lEXrJQa1\nbxNyXbAyccU70n4kvp9iae8dum7LtKA+729qDQtHL61vZ5t1yqzBmJDKdwA+lL5xFbYwOvYrikqj\nORL5iOPybAO5KL1pvod5IS/g2s0mQCeg/vSy8Ebs32iyy3mI4rG5jIOCGA4HrnNLf9RNIbTNJOrQ\nZjmjIRlVc+IpIH61zz4lyel4PTF22tXbxbUuFaZePCkiwW+hPf61G5ju7nOy1tJGEXiPiJkZfYnu\nfoTUI/w4Ql2jh0N4R0i3mur/AOW2yiN0/EMBz24H3NerrT65Yqimby+8O6sD4iMS0ZBQ8Er34rCH\nTAWDpIEtmYqGPUYHIpOSFslLywALJGQxV1PXzDHHrRc8rSaez5UMGQAY+tTSaJW/BSdJ1Fke6hik\ne1GSSF8gFVCF7e58SGEOyjKnPftn2ruhJJJg36QexmurhZr6YKzMNwHIXJrdXdr/AAz4TuLC1mW4\ndSHJTlh0I4/xU58tvC8IJox8d+iyKs4z67W5qN3q8VoELwyKsq7lzznBxT8fLeHPLg+lNvc/x26W\nGHEKp5nc/wAq+/NCz6gIGYWqPHuOPEdcM3oPaqNgUKIQXnhESHlhxyaNj1WzmnPzlrEydQclcED1\n71v/AAdYUJdS3JLySIoxgDw8ACpnxUY7ZY3AUflHBpzELC4ayvlmJYnOPzY49vfitHql0msXlhdq\n0oVmjSTZjGRwGA9eADU5FE8ozWpnwtYuJjJslWTII45zzipX0j37LdMpeRwEkcH8x6An9KZbooya\ndY9Eg02W4cRPK7SHryMYA9AMUNHBDbaYywSFjPjynuo7frQaCgC5idLVpGjIJORntS3ewfJ6cGmQ\nGRuZJPGdmbBPNNp8y2Gk4Bb8Js8+kjU7AFyzWPyZWJF8T1J746Vlph+Jn3yRRFL7eBpJVAwM85o+\n2t/AuN0kaTBT0Odv3xWMN9DayTUWkuYvO3/bVfyqar+JYbdrXbCCsik7htxxXO3LsPlGTK4R39Ol\nUhvP/auoQnjgk1yKTw3yBWMTVvEkz61ayYOO1YxBT5s44rW/Devww6NfafcFh4kTmIj1x0/pSvww\nst3API5ohwrkMcfWuCbqQwDf28YG7d1HQClQYxuGXqDkV2cb7LQG/wD/AFlpdzZxfOkzEwFZY2XB\nL48rAgYBH9Kx7fEGpu0W++lYQNmPe2dv0plGjWHW2oyeLLfwQqkm0rIONrZ74q2z1RL9LfT7kiG1\njk8QgDhye5/ehJP0AH8QaRJaeHKJJJ0IIZm6IO1IzkgZo8cuysxzvXDVDETXqBj1eHXisYsjcqw5\npxp96VcDNCgmq0+cFRyKaCcYxmigMtjnAwc0daXEbNtJ5NOhGNookxkUwg2BQCeazMYD4xX+I/E0\n6rHv8KFVUt0z7H7mszp1tDDdMl5sVQOd+TXNJhJyJmcLHcmKDbyQ37U1ivYRcCSBjI0UJUkLwfc0\nYt1oJApaOWXCFlYnqw4qE8jt/wB2PgHrjFYCRdawqX2EjGB1NOUtswmAsgQnCgnjPqaSTOiCB7S1\nEN14UDSLIDgu3fHp7VfrkIQ258NPEIwx6nH61K7eFnkQBJLmK0MXikDdgEenvStdHuprwO7oN5ID\nMeBV/wD05W6J31hDpsBjN1HPK/5tqE4o7SF8GELBsldumBg0nI6VjJ9sGBupbXcZJHUgc5PSll9c\n/OvERtVRxnOSTXLxzcvRJRphUULLERjlByR2FaP4XMQu5FlkU70G3PfkVWXgEfRrRV0yzW5J5OBn\nrisj8dfFUc0ZsZY5pPMr4Tgtz+3aoSk1UUdMEjAX0tv4AlVbhppM+RZjuCk483H+aKd7mGKCO/Up\ng7keRuQMcAY9ODinuvSr/wAHVrY6lpuoKyWsogcDw7iEFhISMgsOSD+gr1Z8vH9ZMjf6jPrEyXiy\nkPInmRCQEPQiqI7d5LRIXkVd8mI4kUlnJHJoy9EXpZqGk6rfXK5gZ/DRYhjAP0xSa6lWKwCn87zZ\nKdxtGP6k/pU6sSXo00H4gW30a40u7gLQTKQGz0OOlWQfw2P4dmjVE+aHIbufYU6/rQyVmdlAEh5B\nwcY9/Sr0Yi1uJ3dpLhSmOuFHT+lImUSE0/nkZuc5z1oXVWivLezj2uZ4QyNkcEE5GD9/2q/GqYrL\nbadbS0aGzBRdpDyd5Cf7elcSe5jt23MZomOCrjcP3q3W9J3RJbyz2bbnTkyOjwyFTj75FLpArSMY\nt4UnhX6gUyVCt2WW8/hOCRwpxt9RRVxfRSRkRQNEz8E5zn9aIUCgrMyq5YKCASBmm9gYrHVre0nu\niIZX8MsD+XOAD7c4/elatDL0j8XQvFqBiaMp5mfB68qP75pbpOUMu5jgrtUE/etDwMvRjPEsUdi7\nKduwlsHr5jQQnMsw6rGOi56CiwF8syyWTR+bIOee4pa8RGWXDL3J7VkApuV349xx700ubhYNG0+F\nAC7RsScc/nP/AJpwFGnwG5325Cjechj2NSutHhjJwZWcDkDoaxgRpY4GGEfK/wC40Ut1KtrwxjBO\nQoP5qJi/RbgR61aKwG5p0D9+Cav+Irpf4xfKPOokIVvUdqSvyN8M5IPJsGOBVUNuS/J7ZqlgLflw\nEwWBoeSHa3lHH1rGLIV28CpsQRgVjFRQ5rm4oeKwRnp8yTnYZEjbH85wKeWUembFM02ZB1AfiuWX\nG2zN/oM1OezOntCEjbcvlIHIP1rDToUfn1rogqVC2x5o1pDdfCOrbIElu4tjhtvnRdwBKnr60nNh\ndGza8FvJ8sjlWkCnaG9KKlTpjtfoL0Z4lnMdxHuDKRyxUD3/AFqmRQ2SpKjvzmtdih/8UuL7Tf4f\nuUscBWPVgO1JJonilKSKUYHkEc0UkvAFZFc4pgka9WMcrooGJcVfDKUfPNYw/wBI1He+xjyelPDO\nQnGP1rGFt1rNzZXG4qHhYcDHSr7L4qgMo3Eq2Op4pkxWjU2WvwGEb5kHGc7uKOOvRQ2ctyG3RxDO\n7PBPpWbAkzKT6+uuMLeCOSCe4YLvkwB+tU33wjeafbvPM4faeSGz7VyuWhdISizmExIICdORR0ER\nVTjO3GDz1qi8BJk96BRjgZzRsUhmtsO+Nv5fcUrDE6s/hAjBJx2GTTPRIJr2YXG+Pw1BZ1kGOlSl\nKi8fSWrakITB81H4DSDciBSV2n3zStvCZ2kjUbtucs3bPatBL0fkl8Ju7OvlU5YZHTrQzvJDtV+H\nxyDTy8sgVPYyXSePJbSeGQQJVI25HSmujWvhSh4oyRbr4hB6HHWo8svxo0PQC7lM5eRiRuJbFDeG\nsi+UlCRnjsahDBZ7Kw6KBjbLJ4mSgw25sZphoQvommlsQqMy4RplDKCCDgZ9s/rXR8MjZJrV0+nO\nt9YlpkRRF8vJhWPckEjvisM41S4vTPcw3DTN1ZsqemP6Vyyi+1lFLBleWF3qkEBFrFE8cQQSs/OM\n/vSWXTZQ+JSGhJKvtySpz+YcVdOkOmh7YfEl/plrNbmVbgbsK0hP5Pb07V6uaf8AH7uwOrOtrVu0\n6fJwLOXkJdFOMVoJIBcZltGQMFJDpxtA68+tPJOMgRdo9BqNqtkWe+kWHxFDMRv8xHTOcj1pbHoK\ntrM9xdtFLZrAZyY18MBsnCn7Z596NiyApbPThdxyQBmUjzqXAUn29q7p8FjcamEKLtZWXBcAq3Yj\n2rKVukPBoqaTRZ9Xtl1HfauB4Vwo4CsOA2fQ8dqbat8CWtxYGTRbhgxG7DnIf0GfrVOtaUuz57qP\nw/qtlh7q1Yj+YxnOD9qCtpJUsriK3t3a5k8uWXOxO5H16VeNEWxeZJoZSkqlSCRgjHNMdOldreVG\nUbX75708mqEbChawBOVDE9aGubB2UPbqAqnBVRz9qjGTbAKyxB83XHNWlThMg5HQHiuixkSkcW8e\n7PU460DLI07liecYB9KKQTYXMq/E2i2d3K22e0/6abafzd1b9M/pQ0mmxRwKsL42rnLd8/8Ag1JY\n6Mw630261O1042lq9x4Ub7lVc9GakV5G1vIwkXw5B5SrDGD3p7CejSdk/CRpMEYIXINCtIVIXGex\n4oppg0kFjMC7iS24gcdKbzQxQLbNdjw1WBBGe5yNxx//ABGmAJbycfOBomAQklQvb600gmuJlPjN\n5VhLBgvWs8QUXz/D8g0qDUWdZoiuW8PnbyOG9KVMfDUsxBZjlRnOBSxl2C19O6Un/wCmbbJ5Myn9\n/wDGahcjfcF5CRvXI9630V+ARH4vTqamU/28YNOAqkyp45PfAryqJG2qpJ78VrCWT2wjRhwhGCVP\nWhd3m5NFMzws3jGTVEjjOaICstkf2rqnHSsYLt7ySMjzZA7NzU7x0nUMiBGHUA1qAyvTtTutJuTN\naSGN2XafQj0rWJ/qbdPprWU1jbyRMu1hjANSnxqbKKVIatpv/q3Rm1IrDafw+P8A7caj8TocH7Vm\n9cSx+cVtOARZVDOgbIUnsKTjtOgzWJiaS1kil3xK2EO4n0rRy/DF7dfC0esvHHcJIP5Gw6445/Sq\nyn1VsSMe3hj5BgkcfSqz706d6B5hw9K90omOGuUDHQauhIzg96xgm2cxTr2561oY7ghcE5461jEJ\nwsqEOMqfal50YTSYjYjPQYoGQy034ZHiB7qaRkXqi8Zpnqdws8MUMYCxJwEHT7+tBswTaPf6JbRq\n0kHgqc7Cu5lB54/Wp6vrq31u0BkVN5HGTnApHVE5RtgtjDbs7NcvhSDtD9z2oOZWjmKmIAE5XnqK\nSDf0LVFp0eaWGSXxIEWNN5Bb9h70PDM24MSAPTHatN0GIQ75jwH8ME8lTgn70604SW+lPvm8ONWD\ntnBLAdq5lJydHTDNEetSy6hfNcYKq2dpPGAen7UFLdRxJslDblGBt710RVCcjTeBtpcw3ESb5xCz\nZCFl4J9CaH1G7m8NjKr74iF27MH/AM1m7wWqDtHvmv8ATPBcMio3A9acSqkOkCSPxEdiUbLcMPbF\nc3IqNH9iQI003hoMk9K9Y2xe7Mb58ucikiSY2t7CAbZJVkdx0UHy0aL94xsRdqjoPSrJugkHunY8\nuf1qm51F4IiwOMVhWxNcahdzZK3ci57Z4qyOVriNVkk3Ngg/5p/QOR1LTxGHhXA3sCG3V6h/zf7N\n/wBB38PfDmYjc3DJAG/LHHIHYDsfatDFpU1rZwlJAyKSqbM8jvn964Zcj7UdF1HDsUyI7wRW0aJn\ndhFwQRS34h1hbOOexgtxcRyopaQudoJHTFGDcnTFW+gNpf2MljHttts6KF2q3lP60NKy3kpdSsAB\nIyO1ZQlCbcmFKtQwudNsL6ygu/BMl0gCSgdCOx9zjFN9I1mO2doGkwowFXOcV13SHiyr4onjhZZl\nZUZxjaDjd74pFNFCk/4ar5lU5xjPGf60Eyc/bKLqwtrqILNCHAPY80vn0+KM4hTYo6CnrBUQjsTM\n+0Oq/wDypvFoyfKMBKEIB868nNCKCkZddHiMwz1z1bIJpvqMVlLpzrOFRxH5H25II+lM5O6KVgls\n7aIXVtJdxiWAMPEUnBx/9UF8RaKNN1WRraRZLWf8W3cfzRk8fcf2qkW1L/Bml1/0loqS+NJHGxAf\nAP1HINEXuokXUg6ryRVGtskfRf8ASu5VI+RgMjAZ7c5rJ/6k2nyvxIxbH4i7/KOOTU06kUa/E2H+\nl6W958KOksSNJHIRyPUf+a+ea9oclprlzCCI0SQkAntQUqmzSX4gktrtchctuIPH16UV8Ro0174S\nq2y3j8JRnsOKrZNaJorJ2bDeTPvWiyv8OCRnlYypUdzkf2NZ6OkQ07VZdCdlSTETHEkbAMH6ZyD6\nCjdd0S2vgb7RIwQefDU5DKBktzyPT7VKT6yTGj+Spmf0NS+swMedmWPPTg1bPAZPl40Id1j2/wD7\nx5z6U96TflA7WRD7fFhLD/8AyCvCAKTmRD9DmnQhCVLcdGLN+1cDyJ5o8YU5wBzWYyAdTn+YvGl2\nsu7HX6Cgw56HFGPgW7Ols8ZphafDmqahZi6s7KWeFnKBk5G7HQjqKLdAWgc1rNbSGOeNonXqrjBq\nCHPGayd+AaoviTLgfvR8dhHIB+KAfQ0TA13YGJt2TtPfFDSxxKPJIXOOQVxQW+GPqf8Ap6Vn+Bb2\nAHosgYDqCR/isDYGMyyR7UEgkyrN0xnpUYL8mUn/AFQZqS2XjCNbgvvHndeAGr6H/p/Ml18MtZOQ\n6RsybfY9P60eX+oOO7PnmuWml2Wo3lnKskcyyEpJnP2xWZlChztbcPXFU4/6oWX9iO2vYpgHGqNY\nx4VMHpWMERyAEA9KY6dZSXt0IkkI3d89BWAzVpDBBa4GXkXy4I6+9ca5aCZT8mREB55EGSPeksRM\nJuNZsSFgEyMZBgYHPNBz6X8jG80buI1XcUcZJHtTJBboGsPiK0m0eeylSczSNuhZME/T1qqz0+81\nCYqInQoQTvO3is2L/o2i+H9RaLbLNb49C/NLr+0k0y5SKUiQ4B8pyBUrNYS0Cy6fHJFKrSfzx4xi\nqY9qORLDlCOcHkUstQUeKRyKVRG2gfzUz0/4kj0tFtG0+KUBsF3j3HOODzUkndFYsi1x4kfhN6/m\nFK7vS59QuctJgKuFIGCR71VY9Jt6V3GiWyLHBc6jHE6r5UWNmPPrxRlnElnBIDf/ADAICoXRlZce\nmaEng6aaLrZWaRQC78+mc8UZrLQxmGOBWQqnnUn+auKUmyqVRE28xSiQeXHORRdnMHu2kLckdR3p\nonMNlddgAZgDVTOB3qwxTJMBE7BWfaCSFHNIEvLrUHBS3n8LOCxHAplBsSRZKk8DAOikk8MBjNXW\n4DgM7AMeMA80YxaZO8IB2RkwSOvBNepmhaZ9DtPiK6S2eRra3wV5O0nkdz/iu2mtTT2UwhEb+OrZ\nVJMHJ7D/AG/pXmyf06+3wv8AhvSnliW41G4Mc6gxiMN0X1PvQl3oqzO8cU9uIomyUkYhm98n/NB2\nlaZmsEF/o5hmE8MPgW4PJBB46ZBzzTGOyWGCzknAW3mcIjYBJHc460/b/pX+Aixz8SaXZaRZWEzs\n6eOChRGxuwM59qQ21lDqEksdtgCSEFY2PJPUnP610RRdVR23sNPgi8O+lj3ocgNKenfH9fvRFwdF\ntLtw99brJHlWjknHUcf2qsYkpAkmq6S0WI40kVwcPC4OD+tLOZei/rTNViFRCXZbQO8iFjjCAf7q\nSNrUttfqFZj4eN4B4yayRREtV1fGoRujFgRkg9qd6XqKKRI7eXGTn0pJR2w2LmuEv9TuxcIrqwyj\ndMfpVNxp0lzAsCOQ0Ryn/wAfSrR8NYTpGizvcJJgKqnO3GN2KE1LQIrYs6Slnc52sf1ot7QK+jr4\nL16PSbrEoZoJG8IsD+U4yKV/F2uvqOuteGEeE8W2MH09f1zS1+Q3bB5/pf8AEcNlfyadcgKk53K+\ncc+lVf6kwG0+JQ2/d4qBsY4HOKnJVIzdxEWmOjXMbOucNnn9arZke9YzM/mX+XmrImiw2kCRuzuy\ng8gqu4iu7Y7DTWnjkMsqzKUBGOCDkkfas3Q1ie+f8QHqGy4PrmjvhfW/4Zr8E8+4wnKSKDxhqEti\n0CLqRqNc0ZZtTfULeKGC0SIK4Tyk57j7Gs3qESSxNFGVjToSvU4qPE6wpyKhOieG+B+or0svnAB3\ne/euoiVt/wBzyntk+1H2cMwRCY+JMeYjtQkrQ6QRNaQySNG7xgqCRkZzQTaU8i5it8swOzC8cdc1\nOLaDVlsXwRrlyviW9izIQGDDABBGe5rW/Aum/EXwrqLST2cvysgxJGrD/wDi4PWhLni1QVFpms+L\nPhax+MdMd4YxDfIpMb4AJPoa+PH4X1BJiqxA7TgncP8ANbin8NyL6EjSTpxU3Wzcw4Xd/iopZi4n\njO5UR3C5B96t2JEdSngLywBg3hsVB7HFJpF5OK0WYb/DfxFcaHcSCE5jnUoyE8Hjg0oZm8UnlTmi\nkk7DYQv4oAyckVq/gb4o/wDT88q3MbSRyDCkdjQkrQYOmZbXLptQ1q5uGUoZJC+09QKHisZriJ5E\nQ7EXcWY4FNFUgSduyjIxXD7UQHMZrmKxj2PSuisYmvFMdPu3t5QyPtOetYDNbZ3XzvVQHHX3op7R\npBj8o7mueeMm8eA1poC3E58FBuHTigfiBp7K/NpJcM8jR7CN3Ce31/zR45t4ZaP7XSrHTkhS3gUz\nofOzcNz05pdLrMltfSkwLIc7GEj88elZtr01EbrV3aISpG8ak4yH70uluZJ5SzsSxxyT6UEzUMrO\ndrmWKFIgGc7WZBzg+1W3tuv8Va2tZUuFVtq+XGTQu8HijTXHw7babDbrc7gZEDqMjqevSs1qdikO\noyhM7EIwCeRSw/tpWUUo2UoYIyzSsyICMnqaOgnikkCW8wcgHAcdB96rIjJAWoxXd1cxyNIm1Btw\nFxx9RUbeB7p/B3BRu/MzcAYwaSWqgx8NBomkzWOrRyxvHcxLy+xuQPX+lJNTYvqNzIEYK0jFcjsT\nXHKLLt/iEW+mwXECq4OGHJU9KnJoaWwDQPIv/wAuavGGWct6VT30dn+Hc719JMeWqZ76NYt/iAKe\nhz1+lMojJi+a6kMySW12qSAcbH6+1MNGuJrtJTLIXKnByBzXRFYJIKvbcNF/uYDjI6VmWu0hkzsZ\nGU9iP70WTjoC95LLcF1OAT0zmvUtFEj6veadcXai4S8W1jyQFixGFTsfUk0C1lBYS+JHM1xclQ3G\nMDJ7mvHfhSS0IkupptW099RVrW1Pm8h8rqPYep/rWjvLTTNWCSQwFZJ4yYWVtnA/b60YxT9Crfoh\nit5Y7Np5nt5YxJ4JDsWJOcccdPeva3Bqem20HjBWgjO6KSPrH7HPSljxyTdMNOKsBvGvNUtUF3JJ\nLHGxYNvztJHWhluBaQlkYNJDh0287sghhx7fuK6eOT+jwdi+0gtpZheSSNFKjrGr/lMoPZwe+AeR\nSLWdMSLXLv5kyvmXeJEHUN5gce4IPaumMr8KSWaF2qaPZo3g6sd59YmwDRdvNe/LtLFeWV2oPKoS\nJMZA4XHNO1tkih7sX1ss0zFY3nIEZ/lVcH+9Z4n8eSUnduYk5pqMDXLtLKW9RjrT3Ri0loN2SMY5\n7ii0YkjiK/Kk8saYFyFJ79sUUqCOfhBN3xDG+44WMsAwyM1T8e6ZJb6nNdblXeoVUJHQ9SB7f3qd\n/nRR/wBRR8OW5EWJERRHPDI2T77T/wD7ClOqZWJGLq4Viox0PNV+kn4W2dorWscsefGzncp6Ef0o\ni/nn1GSK5ld7uZhsY54Ur2/Qg0jjbAm6KtMOzUYoXyoeRRknpk4/vR00QjlLEcjgiiZC97iNJ2Lg\n4PBXNEeHHdRXaogwkQZRnpyBQfgRdLarJD4YIDg5TJ/ahksijqjDLNwADyaCNRp9L1Vba0lsr3fc\n+NEYkLHhT2FS1jSYrb4WtL7xAZfEaNl298nvUP6zR0P8oaJbGyN6xVI97FfXA/WjI9G0u1h8S+vF\nEo4McQyc/XpXTKVeEIxs5HN8PwqXaGdgpwAWyW+uOlRfUbC42GO0njAOAvzGQB+lCpP6U/EjdSwP\nOuyF0GQTJkMOnpgYou0lFjqC3QgN9DHyYkY/vjkYpWsoX3w2en/Huj3qbTILfYBkMRx9hR8vxLo6\nKc6jbjPTz81xP+PJMN/BbN8SWOSba6DnPBjPFI7q7tp5pJIssSfOEPQ+tdEIOJOTFV3pl1qMoMRj\nA7ByBXdP0aWOCdbtYUYAlDnJz07V0eIVCef4duyfLC0pY4BjOanB8F6nLIizRpaK5wGuW8MH6Dkn\n7Vu6j6NVvBNd2MljfS28mC8TlSR0OD2qfybySoVRyZDhFAyW+mKp2VWZraHWpfCWoaPpEd/cKE3M\nB4ePMM0qTxI7kJKrKQQSCMYpVKzNUW38cFyzOzMGBwSBipiQT6E1jDuuJA29VReQB1+tOhPBEfzc\n/wD1RmmaVc6rdCC0iMkmNxGe2ep9qz8sZKzVa18D2mg/CyXt3eObxgAI1HlJ9qxVJxz7qxpx64cx\nXsVQQko45omEdBWMab4f1G3t5gLh9ueASK2ipHdW4MZBVxwRUpKyU0WzT2+iabJeOpIixkL1J/4a\n+d+NbX+tXV1es4WRyyg9R6fpSxjTDEbTamjpLKshkTYFDEebPrSZ3MQYOWB3cErmnasJSJQ75Ls/\nsRRlsjO3CnGO9K0EeaI62161w2QtvE0jDHLYHAB+pFURLdXEjzxRmSPliTgZGaFDVSNPJ4t3cWF2\n0axW8UQLKGHkUcZ6eoz96zutNE11JMt0rpKxIbO4kUkNbZTkxIVlDL5lcnHbtVtvdfLXAadgEHmJ\nqpEf+HDc2HlkjVpCdrlsYHvSq+tfA8OLd4ued0bZ/WkaClQ7sFTT9NaaO6aOLw2LopB82epNI5Pi\ne5XdskjdF8qDGcipw/Juy00uuDawmM6ozkZIBNNMBk5qqRxi+7ijdGEihl9KzyaNFFIxTPJzg9qz\ndDoXaj/0l6VB6AYx713TrsrcRvINwzyM4+9OnZmlRqLi5iRT514UNtB5wayt9cw/OsEYOM/Yiiyc\nY0zty8LwL4MKIwGCQTzXqFFUj6Nq9na32mpLpG944eXBOdoxnvWe8ORZjGXKFeWGcemB9ea8hSym\naa2zRfxn5qYaekCyvCnhphcnaOBU0+IV0WGXT7qAzyRklMNwpI6fvQTvSilgugn+e1EMAkaFshB0\nGOnJ6dK1ralFL8PXElxGkvgnw2QODvHt71oNqTK/2iZ59DYbJtMkeS1mB3I+FMZ9DTL4a+HWOowy\nvtdo8gRKM575qii0mg8caekfiv4bsJdSWeTxYCPzmNA25u2M96p1T4OPgwXwhu55Vh2eQIXCnoSM\n9e1U4U4RotyUzCal8N2VpdM1rLcRPjLJNgFW713S7cwubkz5ZfKuByD7ZrtttHNVF081gbaSJ7Ji\nzKcMkm3b7+lIWtU8JmXdsIyM4oJgYMiKegGAOpou2uDZzBi42helU+ACWxNeRyg5LjnFHod8eBnP\nSsYYaPfLpuqwSMWw7BCFPY+taf8A1F08TaRb3u1mZGK70PY9/wClQlakmUjqMV8OyLc600buMyxn\nr3YEP/8A80EvgPYyKYy4SUnketXsR+Bx+HbmH4NOsQThEL58Fkxlc460r+HMyNdxsPM6CRBuz09/\nvSwldmaotEQa6Un8ytkcdeauvrqSO6lbGSWJ2kcDmiATy7TKWK8sc8HgUZpbiSeSFpCiyxMMg85H\nI/cUaAgaDTZPFEzybiDuUdetNmWC3z4YzMyjc5H5D7VhkCSQFl5Y7uME1q1aHWvh23gZYgGnCXO3\n80f/AL/oa5+RfS/G7wzeuWNzol3JZMDH4YHIOQ46g0ouGluCWk5A9BinjKxHHrhWLdmA3AYHIqUa\nEviKMsR15pxTVfDunWN/a/8AUyspjfdIyuMop7gfWm93p1ktmiaO0c+pPIRtuXJygz5kbPHPFc0+\nR3ReMaVmZnVNZmlTUdNEUsRKtNZoQU9cr0YfvSHVfh+405FnBFxaOcLNHnAPoR2PtV+Oa8JziVae\nB44BXoc8Uzi1dLG68qOQxw2B29ado5n6N4L9zIPNlPzL5TTA3yKjK5KFhk8dv7ULGo5D4UEZnTUB\nFMQTEHBwCO5r0l9dX8y3V1cbljXglsk+gFTpN2FYA3En8QuZJLuOORpDyxHPNbzQ9H07SrCKURIG\nVQfFftnnvWm8oaCt2zHf6ifGg1aX+H2sayQx8F27sP8AbWB3vsJyTjvmqQVI0mM7Ig3pJXxPww2w\njO7im8l1FB4VxFHHbz+G21UjKt074waqiLMfMH8Ri6kEknmvon+msEFrpd1fnEk7/hLEPzt3wB70\nnI6RSHot/wBQbL4gMsV5qlr4No65hWPJSP2b3rF7eeAaPG11wM9YbeaPeafDBJdW8kS3Clo9w6ih\nAMgUydiNUdxjtV8Q6GiYvU/ynndnvX0HSLhbbSLdGJJVPWgxJ+Ft3eQ3UDxTf9txhvb3rJxS2MsF\n1ZTSIspnDQS4znnoaAOMGl/6O+kgBBUsOjZBq26Vrk72UBQ2KAzBdrKP9v0romcSruY88cnFYZGr\n0a2WXSb1mdMsqqCP/kKXPbXkNyAgNwUbK45HsKVYPRor95RaQXU92yyyxeE8DryD9KA1Q6LNFD4b\nG3nWIBgo4Zvf0pYL9BmxEbQtIy+KyKTxtNWpZ2e4NcXBYI2FVj+fjufrTkYlpliF2sLudp5YqMgc\nVUtxJLqKQQOqrIdm5gOB70jxWOlbNB8R+DZ2UVrGqlpV2ypt8rgd+O9ZBpIvEXwljKD8yMCCD+tD\nj1WU5MweWO87dvlwKcxO6pljmnRxMDv7gDgfelxmLDjGfQ0JFEK5dNvL1Li7VA0UK5J3Y6f3pciy\nz2EtxHjZCyhucEZp1noyO2RdgSxYkjGTkYB7c1TexrDMvgk4Zemawq9IpLuAPNeolD6X8Lapa6fd\nTR3MhVbhQoYjy7hnr7c1ql0K1ubu5u98RuJ8FGY5VOAMgdPevE5IuMrQ0UmtGegJbtbMwWGS5tWM\nMsiRgEkH29qx2ufCt9/EpZoRHOZpWfYnG0HkcmqZFJAkv0A6XYsbtLW6hYKWIZ3/ACAjscU4uLG1\ntSUgTbE6+dkHiAE/SkfuMvxqogLxQrBDHHeXE4R97qygA4+2ecYr1x8QaxpjR/wubwFnLFYowGwB\nxg8euf0ro+Wii/ZZYXTX0W69vC5WUbtxILccYxTO0+KIYdRSAFGuWiYJjOS/ZSaMPyditnz3XtQG\nvyDx5lW5R8eVMY9c0VbW5hslSSZZGGTurpTyjnlK2QvZhbt4Lxpkc4ABB470sdF8EHohOMA9K1UY\noaCJEJjZDnjGaruY+WIjzGpAJx7U6ZiqS9EThkXbgcDPSjYNb8ODMke49QwFOtM2LZdTlvL/AHL5\nc9FQ9BW0h+KLnVPgiTS2ieeV+ISFydqsOtLNIaMiHw58M3EGt2tzO4j2vkj2PGP0Nem+EHiaVY7t\nsEk7SP0pXyUB1RuY9Njk+EorU5KpEFwDwzV8x/heq6ZflhZSIofIKpu4z7VOMqY8qpFk0EkVwwaK\nRcNuXcpHB5qq+zJudQx3HJAq6kmTYHJBIYzjIJ5qGnSSW2oRyOSpTkZ5zTAQw0tXF5LLPuMQIKge\nuen9KJntt8jSDJLHJGKCYSkBVidmGCwwpqiC6ktZGaEgFwVYE8EEd65uZ/oeOM0Op3lrqF1psxTy\nmAQyB+fN0/xSx9EuHZVFtJnBIBXGQO/0qXHyVg8k5OwRtJZhvYrHHjPuapnsx4AMGYwRg56mumM7\nQrW0V6OJbO8aJxlLpfCOPqOeK2+p6Bp1/wDEzMVurSJYVzJF0LYzgE9OPSo8raeF+JZoPJILKBP4\nMJrVZ0KXInO8Mc9QSOuAaSXNzrVp5YmW9hm4kjeMbSP/AHD6d6bi6tEZzqVCY2kE15mzT5WTo8Jf\ncoI/2n0+tUXVvHb3St4jlmGcKv5T6HNdCZCWvDeaV8IvffCx1CSXbIxBijwOV7/3p3c6BpOo6THf\nxqqPYj8aJP8A8m3rn9P3rncnZeMcsxF3dW13cPNsMbOxzEANq9sVVu8pCALTokyy1haRguNzscKP\nU/8AMU21DUPCtv4e0zyvt8+OQDjOKNWLdCCz021S+R71mdZMq24jAB7/AL0JqWiW9rLiF3kjzgEc\n0JTcZFY1KI70X4cwYZnkW3YDAPViPfmrfibRpYbZJGujcZbChgBtXHarpnM3pgtTglgvNsg5IyuC\nORT3/T3WINH+LbV7vcY38nXO0nvj0rS1FYsZ/wCpXx5Lrt2dPsZl/h0WM7P/AMjetYmzlSK8ikk5\nRJAxx6ZoRjURpO2fTv8AUX4l0u8+FrOzjxJdFVZQOsYx3r5bv9qHGmlpp+4d35FWxNxzkmqiDSwi\nheJnmViVPGDTJ9eMEShY1IHAy2Kwr0UX2r3N8NhbZGf5VNAZKkf2oBiqHFjBJqVg6wwh2tvMSPzE\ndOfbJphZwv8ALFCMn8xz0+1ALF98XRsIhIz1oXwriSQLt4PesZDXTLkppt8hZkYKn0xuAzWt1fTI\n7FLGSyWaMzSKA8chcYxnhev3qM+Tq6L8cewH8RTGxvEKTyySzsciZACAOMj+lJp9SkXi5tYt47Ot\nNxyuKByR0TJf3El2EYjaXGBjpzjFaPUTELkrFEJPCIAVUzz9PrRl6RSBjpN1O5lMDjdz+XFFad8N\nagtxHMbZmQNk4z0/SllLKGS0daha3fiXMi2M0rLAI4gYy3vkfTpWYfQtReY40yfnncYD1pePEUno\nyt7W4i2pNG0TADIIxTAEQRkZJ+tUiziktFlzLuOffmg1MN4/y/zPgSHlS3Q/etelEg6xsf4XIZ9V\nkcxqRtgC/wDdHqKuivVu5QlvZw+H+cR+Fzwc80rbbpD4gf4guJjqsdxLEkMDcCILjB9fpWc1YQ3F\nxuthtx0Hr606EWuwdIgAD055r1MMackKGBIIAOTkAZ+tb74eMkHw5ZWN6Hc3alo2UflUnIBrz+Rp\nR0aCfpbA2p2OsG1szEIJp1d0ERBKHqQ3rjP6Uwj1GS/1OeCzAeG3/wC9I69DnpXHNJx7JlUkdv8A\nVbXQ7aOa5USK53LHGBkn19qz+iaw+rR30cTJbsknjwxKo6HqPeocKcm2xlL4TuLi6U29xew8hZCn\nkGJOOAR26VyXR47m4hD27W8b28eJoJcOhbqCvcA55rrimURG90nT4Sm0z2stoR4TBg4IB6kdepJp\nZd6vpt/LEZIEN7bOWiukOAzbgclftXTBKXoJ4hbrejw2+pSTRg7boCdMejdR9jml+zI5BUAHHNM8\nZHpllclvNO+TgMR/MRzROk21vcySwllkkj/MOop7tEmXS6BAhLRW4Gec54/rSS6trmaR4xNBHGp/\nKZ1z9cCmg/2ZWSXTNMS2Bvb4PI2SPl1LEe3OBRFklqlmkcFvLPLyQW7/AGA/vT2UoJXSribS/Bi0\nsQSeJuWdl2sfUYNM9DhOgW6rfKy3JJZAP5lbGePt61NzwLj+h/FY3T3kW/8ACjbBEjcjPbPpVWrk\nwX0qzo0TknbkYDjrwakmZQwpXXpY9MayUAAnhjS+PWpoWwGPHUGtdCO2HprazWwZiAM7SCOlBX15\nHPauqImW6EAUVPTdRKI1X8wJ47CgJok3YjDZZuOa608AMrktFttkYZg4kIP85HP+PtValzA258Hs\nQc0qCwRrliFjZGwhO3NMLLS9NubRJbjVUtJ9+DHKgIx65zUOSLfg8a+jGW208xlrG7jZIX2tJIfK\nWPQjHbj96t1iS5t/h+L5yYNmTcCJPyjHY+hNQ6tY0dCpK0KtYkX5KwmRm8NotjEnKqw6j60FZ2dx\nqcU/y20pAm5mZuB/5q0HUSM3oPAG3gltjICRhsdq2t6Re/D9nBIu53hLK7Agq23P77SKbkrBoPGV\n6bb63rPw7Da+DttYXLxyynzE/wCOTQevaRdaJLFHclH8QZUJ2pIY6Qk42rEclusrF9pyO9EJ4V/b\nfK3u2Js/g3OOQf8Aa3qDx9K6H4RijRvrq6Z8Gw6bfK6XSkqojPYH82fSl8moG30qO2t32fPBnckE\nHA4x+oP1qFbZ0rFQitFN/d/L26O8n/xwKev8LXMEPiPNAoC5OWxj60ybRFr6FDToNNRWEo+YmiAT\ncc7D1LCs/ewvJO5jMYwerHr6k1SOkG9EguJ9OuJbSTzI35cnpkZ4qyK5ukTwp8oF5G8c4p2kyilX\nhrfh7UElgWIq7SEklz+UCh/jeKW5eFUbdGkeSPXmiRfpm7L5Pf4F9bkxFf8AuDqtaDR9FsJ7hLeQ\nxzTRfixPH5X29efXnFQnJrwfUYC6hCXbxxN4m1iAQMZqdhZtc6jDAVwXcZB44710p4MmQvbhrq8l\nnbrI5bFUAHNFDFqrmiYkVOTjnsaIApXG3agHPbPFWSSGaOOPKkJkDH1rGKntyi9+tDyR4B56UAjX\n4Y+Il+HZp5jB45mTwwpOB1yc1YusxzXRa2hdEk6Rk7u/Y1jMKXUJFDQx26jHJMnNcctfuFaV12jg\nKQFH2FGgFUOlmFLsPcK3iwkDAzg5B/tWo+ENbuoki095YZJYf/1ZyDleuVJ9CDXPzQ7IvwyqWmR+\nKr43/wAUXE3mVY2xtY52Y6/vmrLfxtSs3tYo/Glc70Ynkj296MVUUjS9Y00H4USeFpNQDxurcLuw\nfuK0N9EtvDEYgquvVgOWpJybdInVFdveSy5DSEe9MbGdwx8KYkqOR60jYUO49rxCTad/R89qiJPI\nWDHaOozTpmZlNTkA12ZkYMjwjzFuARSq91JPE8OMhmbpzRUq8IuNsAmuYkO2RmY8cL3NXNapYKry\nwAiQZAeh2HivoPdXs07q1xIzqOBnkADpRXw5cGPUBKoBUKVY5A2g9Tz/AEpo+2LN2X6rYy6jMqwS\nKwXJBkJwKD/9Kl1zLchWHOI16j61RvQQR24+G7eyITxTMWVWIPavVrK9T6R8S/B1i4tYLaxSBw6P\nI5XYCq5zwOppVLczadqstxfucG5KwRSHblCQMg+wry7UlTKQRrY7kiJWgZnRx5XHpjg0Db6dLYaX\ncQpcrIJmZmY/nYnt+mK5nLKEaadmYvdMumRlKuT0256Uy+BdJsrK9e+1RnjuYGBgT+UjnJP0pYzQ\n0U7tg158RG91+4lF3sUDEMJBAxjjtzkH96Njhm+ct7qOZvmJSDz14XlR7VZZR1qn4C6d49nqU88s\nSSpeyBUaVj5M5yMf87UZpOh6dMoaWO0acK6HwlIBPTgk1ZJ/GTe+gGq6cqWFvDK4gNrI0bvKuBtb\nOOfYg/rQWnafbJq8aXJjltw+GZWDBh9qeU7VInTJ/EPwjNbSPNYIJrdm3IF5K+3vWfsrhvm7e2gW\nOBpJBHISm08nmm4eRTj/AKScbY3bRbfXLy6gWZ/FUn5UEkIDnhTnjpVtrHpWiabLpepQuJXJaWWK\nNc5B6At2HH608ZW+o6h9BtPhttVx/CdIgt5mfarSq0uPcdqdabNeaFuW5ZdSEjATTxYCwe20f146\n0vd24sp1QFrFxdNrEEV1KjaXLI0kDKxzjHOMelDC3t7e5jjttRk8BxvVbpS3iqDwQe2BxSfUBBMu\nsS2V7HcWsy3ELt4M0edw4PB+vNbaKCx1jTkGoRtLG3lQMMMpBwfpVVH8Rl+jGfFfwldaNE17Ylrm\n0zzwS0Q9/UUi0rSbvW7lEVJFjPJlK8Y9qk27oEuPbN3D/ptaTac6JdSxyyAAMRlQawWp2VxouqT2\nV1gvEcZHRh6ij10zjSBIbvzNHxsYgEnqtFaPp+3WlupfEaG3V5dp6HYMrn2JxXXG0tIJWxfLuZnd\nwpkZixJU4yeevehrqSaFF2Ic4ySozWQj9A2uS65f7mhZkk5baQp9BToyCtH1BbW8Kz4aC4HhSq3T\nB6HPYg1rdQSK106PEJuobYbZBLyfMODgHp3yKjyxpo6OJ3FiXUmuJPhxQsUIto5wQI84JIPQ/pTP\n4jgj0T4estLsg0Zuvxp2PJJx0z35pa8SNL/RZpzoLqAXEn4RlUPj/bkZ/bNa63C6il5BDK0q20iy\nK5YkYLbdo+go8iND9DvWdcT4f0KSUcMq7IYwO5r5bPqWoa1qCG6nfxS2Ax6D7UsF9Zpbg8sEt7e7\n8HUFklQsFPhggfXOKYfEul2FncWradE8loE3Tu5YhPuOhpnPRVx5YtM660Ba3ciK7bjbP02ngBT7\nGr7+aFdTjthC8ktrGkbBjgZUAHGOxrf4ZP6wZ3uLeWY6ZbP40b+VlyePQj096bWemyXN7p8ty8jL\nIgklSUgqx5PAHbigBsBM8Zv7mbxRLJISf/gPQe1L7+KOS3djhWPGaokccn+QZ8Ow6BqMCQa0ZEni\nBEciHhsdB9aVrYXWpXWy2gkmYrxtGcKPX0rKTRdq1gx0zQNVuYStpGTtfDKHAPTmgvime4n1YrDv\nVYVWML0wR1FMp2I40D6PolzqmoNZ+JDHKActI3BwP7/2plpEUkOorMqhjHbuNyYycE9P0qDlcqKU\n+tgt78PveXZuIgsJLc5UDPuKDGkzWF7dXFzzEls+2VemSNoz/wDxVSMqdElPTNbPOc4HvVrxKBuX\nnPtXREc4iNu5GPtRMkIKgqDjFMY1Gj/B0H/pu61LVmaPaMwgHAP1rLzssEh+WOecqxqcZ9mx3Gki\nL6lM64Kr+lDmdm/Mo+1OKdt2HjoXQMpI3A8ZH1rS6Ntvtegt7eFowXAxE2WUDvk0QG/vdBguJxjG\nCcMx6kYrMX/wo1q0jWiuXXkgnhh/mtZinTdJmEr/ADqlQUyAP6Gm+hWMempPNJFHMWQjBGSv6fau\nTk5fiL8aV6ZrWLZLRXW0iDxKNzlgN+7PP2qzRbY+PHPJuGCPKh6bun7VRbFAkusqNZps0EE3hpwv\nck5Oav1GIOrsMEBetIkB+meWQpJlUmKE/mVcinOnDFxEzxyFCeQ3lpGjIbXt4II/+lwJHPOF5HFJ\n3tJGhaS5upUDrlFDZLH39qRy+AasXXEUEUi+HC7Lt829upxQkWn6e9zveGWAcZxICfsKKj9FteEb\nrSo4dRJ02WSeMYKtIm3B/wDuuXXzlzbC2kKEb92MhiW/r9qZL9jJ/EBizCs0c8pDIfMpQ5FT+Vgh\nmAeVgDyAE9qqgdV9CY9SsrWSKR7ocfmR425H2rQfMJcqghNuu7lceXP3Na39HjGPwhf2914RlC75\nQQPI4bIxxXqRzGcZfDf3Goxys8U7m3LDyk4BzVNvEuuWbNLbw+Gj+HmYg7zjGf2ryIytgOwaf/D0\neIIsSx7QqRyblAwffirRbnad6MOcg44pnxuTwH+lo0KWePcsbYPOSOtR/wDTUuQQmP8A3MKlL+HL\n1B7lWr/CWlzWni3lvEb4LiJ06L9azNvYTR6kviSuETlXTsxHGM+9W2FRseEgi2W4hNu13bzMVbGW\nYFV5xmhr2yIsr14Hx+PviA8xxnBx9+ao5U6KpWN9OvLu/WISWUc9vLAY38UA7pByDg/THpzSSAtb\nXRM2kqomLR+EkYCqe3IqkHhs+Dm6M7aFGtjDb+NuO2MyY2fc/wCKD0j4dfVrcXF/HbWkqPwiJxIR\n1Y/+KzqC7EnhR8QQppRjW5ZvDBO0JIVUnB6etJZtdt2hs7WPTGvZuizXmCE3H0HYe9aL7NMKlg41\nHVjpENpCmoR3EwZvHEK428cbccACkMuu2dllrG2YysSXnlfzEHqBimbfaxbFd/rqX0yK9nuCbirI\nu5/MOf8AgqzTdT0pjClxHcrJBGYUAjDA8dW5B460yg3QOx21niXXTp6SFrUyHDY4bcchsftX0rQD\nLLA63KFWic4B756VV5hRM01ou6KVWA2lcbT0xS64+G7e5ZzaN8rKo/lGF/T/ABQa+jCm2uNY0/UD\nays2/nw9+MMPY0r/ANSrKXVrOzura3JuYsi4CLyRxz/WsmkxZLD5RdSFJPDDEE46Cn41GbT/AINi\n3/ime5aMHqQvBP710Sd0c6+mi+GPhhGtY7vUZPEWRSyxOMBB2yazXxVcacdTlOnjZEnkwB5cjrio\nxTcmwzpRM1czxgAKucHkYxROmXYnkME6k27kbhnkD296u1hKLH+kW+lptEcUOVZjJJIAzIuOCAc1\no73R5rrRzLZS2V2kMe4y2yBZJPYr1HB7VGd/To4/0ZqzsPC01rC73pHLcnOVIYDAw3056Vf8U3vz\nt0tu7uot8eAphwWB6nOe5xSR/smNNHdF+DL3Uo0kdGhi3AneuMr61qrD4YudGEUGnKZo2dzMzcM2\nQMcexFPyTtUjQSTsy/xhq81/MsYh2LDlWXphvpWIF0yXaNIGcIwLKDgkU3EriTm/yPofwm0nxBfG\nAKIEERZi7A554B4z39a18uk6ZPp11pfhhWuI8SInseDUeSNPC0HaMbprWFkf4LPbL80J8CVl/kB3\nZz24oTUHN9HNDCyzaranxGeNf+8nXA9wD+lO7tE0qsf/AAbOyfF8khUlJ7WNQCuMLtBJ/rV3xljT\nnmkgKiZlJiVB+VOh+9ZJJ2BvDC6Vpur6lcLLYWErQtwXbyrj/wCTACmF/pdvFIsV7rFhDgflRzK2\ne4IUY/erb8OT/nbsI0PTdHuHksYL5Lq6ch45FiZNhHTOTzmth8NaYuiXl54sqRzmLeV/LgY5zntm\noSk7po6YwtYZrT76bTL6S8WB0SK8VyTyGRickfY0z+I7K2X4ie4yjRTETBeu4nHXHbPNHxBce2EL\neN7z5uxhTT1F5GyK2zaVbno3WgLPTre2+GXgvrVku0SUC4jJ8g2t1pPo6VIS2OoQppzQ3EhG0lYy\nDz612DdquhXTW4DhisMiyHn68VVxfpwV+TMxPYNbztuhdsAg8HrRtpBHLHGNyLnyk56H6VdFUw9/\nh4HzrcQHaMNzRei6VBbavBJOYmRTnrkE0WrQy9HXxLrNlfWf8OgVgpJLFeB9KyUtjaRR+QAdgWbP\n9qXjj1Q03Ysk0+SUsYmRwD/LURpksaszxHIGfKc1QQu1GyjWOIwKuWQMcdc+lP8A/TqGL5q7lmQi\nWHG0sfXORisA03xVcNBpyNFOsMqt5SzEB/akUOqTfK7riUhX4Vg3GfSlmrQ0UW2msIs6h335IXlu\nMmtNDqElheme1SIMpKbHPkI9TXEk09OmP+GM1W/MvxFJNDb+PGzHxcKRuY9ce1ckuHO5ovwQzKcA\ncDbwP2rp+EPoScw3MjRs5Rju8wov+ISmIxsRtI78UEMxpo0cU1u/jPGioQqquSRR1xHZz3atHCxR\nV8rM2ST/AGFTl6NHyyE8ixxmZZU8h2gDqRiktxdiRtxLA1JLSUmD/NM8cn8qIM571VFOHfEatnOS\nGGQaosEWC64knEhw5Izkgc5qKOBLHtRlAYHI4zz+1Uik0ZS0YvfyWmYZGiljMmWYAbn9snmq72Bp\nYN8ZzvyyBudvbHFNVFPUI0eWK5X5oTQxpyx2Z3c+9aSL4z0vwxHHBK7gYUOgxQd/BGhZLdyXl206\n4iLEeWM4HFeqLTYFN/s+8/EVjYy2266hRnZgoPQn/wChWA1vVfC3x2W6K0Rd0SIu5mI/3eleekoy\novNYG/Dltd31zBfWMcV6PC3/AIy+XPTb9QSf0rXTo1wTIjz2x6PDIcKG9KrBSqmv/wBBVFsTX1md\nxm3hgMq7ZxXrvVwir8xMEUds08+RqNGUdsUSaza3moR2wcBXO0P2BrMJeahdajdLIN1uWMaYHQI3\nOB39c1xL+1lor9C9p7i51QafZTOiuPGiZ+AwHb+vWnXw3aMT4uZVByZJEwygdx98muxawyk14Eme\nCKQ3dteRfLbgsJD8x88qSeOffFDanPcafPPHDFK3iv8AMLk5Ugjse3XFFr9BiqVsNsmtbnS4r5Yg\nssTCN1cZbr2Nc1KBri48ZUdNvHlkYcfrU5SymRctF09pb3MUY1K6leCOTcd0mQO1KbvToEuZmhtk\nTbHkMAfOvQEDt0zR439GrBHPGNw4z+9Dyof+cV00iDZtf9OLG1DXF1IivOMKN4ztHtSPVdIt734q\nuJB4dqplctk4Qe9NH9oovCNt8M7JfmHmDwghUkg82T257Vuvh7UILue4QyNut22EsCGYDvj9a0nb\nopHw0ljJDHa8S7g5/MepqoXoBuHUkYIUEDpRH0R6uj6o8bLqDJHEeCijduPvSC/vda0m8ubW4uVF\nv4YkEjAeZQccd92aTroeyaoyF7Gl5G0Nhp7JEx8txIdxZie5xwPpWxsfg3S4vh61ttRmfxIS0hkQ\n4VS2D37dP0p3L4iXStZmviD4gMd3dadaXSzWirgzIp85PbNZie1aQBQrEKD06VbjVKyM3bAJ7CVu\nQyjI456122BtfzYYn07VQRIYafqHyF6lyih4wu2RCfzKeDToWmo6bKb34bvVngB8UwROC0ffDD0q\nM3ul4eGw0HXh8RxyA2PgSJ53DqDyO6E9fp2pH8TfDtysr61ZXqXBQh38UhWjHY4PBHQdqhFqM6Hb\nKbf4x1K0sbiS+SNdkWYt4OWbcB+mDQr/AOpGpXRjWIRwOHU7o07Z561XqqbFUvlAPxHbXh1me5ij\naUSSBwyAucMARwB6Gk50PUpJzIdNvSTySIGwf2ownHqCUHZrdDsbvSbK3uk069FwH8wBIXGSOR9K\nefGi3CT6ZeaajrdySeHtUbSwPQVKTTZaOI7o3wjcQ3T6pqKg3ziQ21q78k4Iyfbt96H+DZFtNI1j\nXLu0WOaKURc4wT6D060zdieM0mjhLmVtXRooUmUCVpOAijsF7n6VX8VajBa6KurWVvBcPE/DTqSB\n1BIH09aCSRpeHzfX9UvnnaS6vJZkuF3QIGwgUjrtHFJEt2mHlz05q7dHC7Zq9DXT/haC1vZGWe4u\n1ZFDfljPTI9/emT3tpqfw7M000q+HKsUybizopIG7J5Izxz61zO7s60lGNBWlDT7S1ktVlQxqm9d\n5yjDPv06Uz1rR9M1uzt3lgkt5DGRbvH0wcYyR1xQlJx0D1YY+00HX9KkaGKVbZ5gGVi4BcKeME/v\nT1pNQtPh27juozMLuyJaVcbQwU/+KWfLBtXgVJxVMwEBDJtmCnnPB6CntnafK6BI+lKTNLLnw3OT\ngDt966n/AIcr12cTXgoCXlu6noxC+lIpZ43vGliBRXOeeDVIhS0LW62gDccH0FWx3FvK2HuFXA43\nDBphgpdOhEYeG+ZmbkBYD/WqZdPnwNtw7ZPRgKNhPR6EznfJOFYnqP8AxXJdCYOXN2oHspoWGiVv\no6RTI5n8XY24qU600sLJIZZJIpFVpW3E4xRTN1J6vC99YNBcvDJGOmTyOO1Z26trQabbxCadZkBE\nieFlTznINLJspGNg9nf2dqx2WM12VYMNx2/tWt1m7a0e1dwdt0ivgKRt46Y71Ka2xoeNEoLNbuaJ\nZTshzlz08vWqrrSLGOMePcvGByCSoB+pNM/MJxRXPJE1lEVeJgnBYPz19qW3sjXEjEsCCBkhTg9q\nCGYd8OSQzTXtqGESCINKX65B4xXRq1vaaqskTIFiIymf+7jsc+1R5Ho8XSK7u+N5dSzJtSOViyoj\ncL7VQJxxk5Fcj5GmSkyxLy1RSLmR4lJ4Kpv/AF6f3rSQaNa3OlpcaLeLeyA75Y1IUj7HB+1Xhquz\nUjN3do0lw8gWJQxJ2r/LQNu7xiSOSMBVzyDgg10RdE3HQdJHBdckuDkBhViPLKzEE5UDjNNKWBTo\nO8H56MLJGxkVhluDx6YqH/p2zjumkjMZc+o2mpxmwOaYztNPsvKC6o/YZzXqsoqhKN18U/FUUqxP\nHleRtQ4bK/zH+lJksdPu7sNdT+ArqWJyMLzwK8if9jp7KWDz4W//AEfcPBbkzQSOTkdOuM1p5buA\nSeHI4ZmGcdc1Xif46M40Z7VNTksJCiyA7xlA3OKzV5fXF834rBj2CilYJP4LdSlOkRLLdI8buCYl\nZcFj7VZb622myw6kcybyVBK5wvG8cd+Af1rnaakmX4v6hlvCNR1nT5IbaPcoL+IJRyGJ6jHYf1p7\nc6nb/Lfwuyi8NMbfEA2qgH83vk11q16TnroyelPf2ljLY2qxTFCfESQcAZOST3JrRWTLJYJeIhLo\nxiGOiJ15z15J/Smv6P2Xg1spRcpPF5BIfOBt649K9C0dyu0yJnPmYjP2xU5V6Ql7hTc6LbzWj2pT\ndnzrlCoLfen9vo1vFpgVolZmiVCzjOMDA/SmhVUikXh89/8ASlxeapJBBgxqxBlxhRTXSvgCyubm\n5ku53eKF9qKBgE+5p23RNQ3TUQaPFCwESLCgG0MFxn/zXzn4n0K+S+kErJJbPIdrxnHX/cK0HKGs\n6JQT8GfwdNHYfEb6EY1+SFrmSVl/NKcMDn0A4pHpl5LH8S3lzNueCzBgkkUFd5dtoPPpkn7U/ZS+\ng8dIb2OrwvA1ul8VmgfDh3AH1+9GJeTWzhvmFlMjf9rOC2eOKHgxTrOv6Wl1ZWtqF+Z8cMdvBj7Z\nb71QNS1S4uHNu0ZuEuZI5pSBwAAVGT070zYF4D6X8Wa7d38lu0Md1JHlWieMAEfbHPFG6m8GuaY0\nMbNAm0m4jbAPHcH0zxilUllCyjhgbt0tyZxCNjMQIFwcDsazEmpTPcOQ5XPYcYrsg7RyBME6sMkZ\nbHWjbDSLnVZJHg27Y42kIJ5wPbvTN0FF2jWw+dJeLeQjNHuxjcBnB/SmAnvrNpDayCznJ3hYx5Dn\nrgj+lTZrof6Jaahf2t1GbhJXkiDCYHbjIOG+oNNry21TSvh5v4pFba3wBtKlX2D1IwT965nVnRB3\nHRDc/GEFtYraW3w/ZRA9EkBcjP1JoW2+LZ2R5JfCsSD4aCO2QqTj125H71Z8eWFT2hpBealrnw3L\nHa3N183bRkx/Ly5WZSfUdxzx+1Zfx9YkmV21W+FsmDKxmYFfbB7np+tCCV0zSbrDTfCMd18Rpfs1\n7dKyYVAs7bVPbvV3wtbaumrt/GGnf5aTbE8rEgkdxms2lgE2zWyXEqXKyGZJV8YbV25aM49feq9R\n0mCWC0hmyY7mR7gwLwGYDqx749KmhpUL5757i9/hkdzsmlTbEyDCxt/LWdvbjUpLOXTJWeRmbzRg\nDLSZ6ftTX9YjuhRrHw7f2VojaxC8JiH4argl1P8AKPoetVaNDNdoZmtglrGvcYDf3NN2TRDrTJ6u\nkNzoUt28qq9q6hIQOQp4ytS0+aCaHUmDk/MtHtUcNuwzH9xQVlJVJHYJ4YZJRFu8N7d4sMMkZXj9\n6IstcvbewsbOBZZAsjkKOTg45x6ZBppK/ScHSNrpF9d3FuLyWa3lgCeE0bL5o2BHOaYx6TFq3w7J\nGheE+Iytg5UrjGMVxKKc6aKSdnzrW/gW80JJJUkE9snLSKv5ft6UJZK0lnDFFI6yo7OHU+4rvTsh\nKNMGu7qbxpFngSfeArNjnP1qq6j0/wCVI+TkikDfnEnP0weKqkDQe3McxaNJG2gdRztqi80q3SVf\nAkuJQRltwwPtRTCHWGq3kbrbuzGJRgBxyPvTCa5AkVFcMSu7isGyy1uVaM7uG96lK5JznJxmkZWL\nKRc54VfvipLIdwG7rQTGD03RRNcvCZERSOnBPp9aTtBd3FyZruZLdJME8eVc9BgVuwGensZ7C+aO\nJJGjG0PLH5dwYZP0xWltUuZZdPjune509YirIHKmQ5OM49BiklIeKsNigZ5p9K0/Sdu2PxFmnkwc\nE4yD3+lZnUI5Xc2qzSXd0rFWj8MFFGeeWPWnjQkovwkHh0mRLS6tHDS+VDIgCAkHB3D3xx9aloup\n3Eq+PqcJjtLdwI4hgl39gegHJrSdLAqJ3RrK4i1u5vtQtHmRZPP4oBWTPIz7Uq+ItEv57+W6tba3\nEEp42EIFPoBmoqWqx+trBfYaXqHys8rgRC3R2YluCccYxSddXuUAPiHpVFxQn6QkqDYLvVLhQ0ds\n0qnvt6/etJodleS3ELyLDbPu3B5DjbipvhS/qJ2CLi7L3UkkoyzEksB1NSivwscsUqBhKm3D8j7V\nurQVIXXFst1GGt5g2zaojI5PrWot9NsmhHhxICVAcAkkUzk/AT1WjslhHAN0aspPvQFyAPr7UGsO\ndAROG4/Mehr1IyybovvpWtrJLlcqpcq2/OAvajtGge/t7p2ZN0IXGMZwf5h6/T3rnpPSsY/R5oX+\nom3UBa6vaxpHK4jWVPKVPQZ9s03gnlt9Tui6vIYUYlE5b8wAwO4IPWlbdHT/AKAah8V6JIhW+iu4\n5E4KNA27PtikVhrjanrkNtpGnSKwcMGlJJA9SO1CMW5aTavT6l8QfClt8YaHHbXrbLiLlJoxgqe/\n2IrBPYfwnUPkbm0ktltCJIpGGUfGe+OpzV/5PCnUv0NwyX9WG2cTHTvCiTdcs23xBEEJByT29ab6\nJpFtDps99qrxlNrKkZbjp/Wo5J+0CbXphZB8xBdyxRMZBIDG+3youehNNdKR4NKO9NzN51KtgFgc\ngAfrRi7snF27L9P121SfcTMp42YGTn3pnqdkbuwna2EMTy4YSIcNkdRmimjdrYl074qm0547O/ka\n68u5mzu2egzWv0X4qh1y1lG0W7RnaqO3LADk08ZJ+FE14g1444l4AA7Z71To93GJXtTkszFvUZ9D\nTPBvujS6uF8AxouWYfpWY1e0UQ+JPtIJ/m6D60z1DrAAanAmyOIbyowCo68VLSrsTTypIv5088b4\nAIB4P9a4+tSszF2u/BNvbXovbaFb2RVwodtpD/yknuP8Vkrz4XntAb69v3F20hKiI7tue4x0+1W7\ntMSU6CrKS2vPD/iy+JNABsuY02sf/n69Kd3Nq38J1CSRoZElUSbo8E/U4780nJdjwfYt0j4ThaKL\nV7a9ZjHEHXJyDIB1NItRuJ4r46biD/q1bczyjqRwOT06VaKqrBPbSM58SfDeqfDEkLahaPEHbyOH\nDI5HYYrNanOt5eGYIke7qqjvXZB4cTTTPW0eME8YpnpFxcnU7a3tkaXxX2tEvBcHqKdvBkbWy0aG\n0S4W8SNWkjIjz5yp6nOPuKTm3Nzdsvgm1CHOCuAPcCuWPLFsMlRorACP4OnuLSYQXO9o1nKbQ2P5\nf16H1quP4qv/AODmWdfFJUjBGcYGDn71GEnJsrB0jJ3cC6nmSDKXG7JjPUr3Kev0ps9hDd/6fSXS\n7ma2uRuCpg9Mcg9O1dMsRlrKNAEyRC8sZJba6WPLLuwFUMOfp0omf4v0jXJvlddtSkhbHztuQhLD\nuQByPft+9GSuWBTpUwnRdIvdL1GRvh/Wba6eUBngnXwmkGeACeG+oIrX3zzSpbTz28lrKh9QQWI5\nGR1xn9qjKVjwiLNK1lbZbm4vi00hKpHFwWHqxHU1da6k+pTwxW5gMcUmQgk84B6nBrWM4h2taZHN\nqdi0REMjNneOMsB0/apR6XAt5LdnMlzI5ZWJwI89vc55oWK0KNR0e6uFabUbp5m3MsaKPKMjvSBN\nOu5wmn2UIe6dRu3SYA+x4/Sin8FlH6Zz4h0/V9LeWC+sniC8F1GVI+te0sOLa3kCt576NRgdRg5/\nrVF4c9Ybqw+BLSS08T52QTpkFCOD6VRGBpmnNBAdk0jbZpSo3Kg7LQbbFuhelvY6dbXMwuJ54SwB\nMb7SuTnkEc/atR8Na2ZrJprYssEZxMpJYscZUj61F/joVKzQG/s7u3IYna3BV/Q1ktb+Hjp2owXm\nlrF4cTFyjMcdc+lPGV6dHVSRnfjG3ube6h1MPBFBeOQY4j+Qjt96TypFOdysSjYIU5yKvCWEpRpl\nCh4pSqsIkJ2kupx/9VdMz2zjxDCykcMhyDVVpJuiMqzGwW7XmIyeGSB0NG6Z8P6vdn5oWuEPKl2V\nP2Jo2jel38OuI7f5gBSpYggODgjjFaL4b0WLUdIMtyXjcsVOXwfsKhytpWisHop+IbGTRr0Qkl42\n5RsdfvSf5jz8Kc+9QjyvxlHjG8jGXRvld8hklbd4YONp7fUEUlfUVn8K0j3PKrAHd3xj161a7Rmt\nHVzpNzq2q3U8jpaWSKpeffxGAOfuaYSXKvpcVpCsjpFEJFmRMA/UjuetBp0ND12Bw6+un2/jWmqz\noXUxvujGVB7DPbNP7bUdI+ItIiht5kTVoiFBK4E3sT2NZdq1GdXjFFvHY3GnTQalvSbxNshRgVVl\nPrjOf8Upvn+QKODHnzDMg4J7/ftSJ7ozQfHqLiCGTwJGjvAm12fiLsRwMcmib5EntoLfJjKZ8ZAu\nPMfT9qpCKJyk14Lf4XCuA7S4PUFiQaqNpapN4Yt4EGODs5/U1S6OZybLQqglcrjsAa6ybQPy/c1k\n0xHYvv5vBQkMN7cLk5FK7iS5bzO8bE9cGkk1YSyC/EaAxuqyZ5GeaYaT8UzWj+E6qYmOCyrlhyaz\nj9GT+GqiuFu7VJYpGlRv5iMUvvNsbYcgE1miTW4Z++uUjugrygA85yRXqm4sqkbKVptDmSO6dTat\nIImVlOHB6n04/vV0el2qXlvcWdqygSmUSY8uOcfv/SuBXHw6eONLRL8S/D1tc6te3ySNbRudyRIu\nfPgZx98n70VZfGF/b/Dd58y73AgnitotwAcjDEgn6DPNWTfInGiuGus9QtPiP4f8s3kmXEcoY5Vv\n9p9xTn4N+Gk0Ow8aYtLdz8SSnk/Sujhh9fqIzeUjT21xHEArEZc9BWZ+NoPm9SiVo3ljijBZQQAD\nk8mq8z/Bk4rRNdNqZ0hDH4Nud4USTPglKzja1NYX+z5pL23UedI0IXP9zXjzfaaZSaolNdfNxiKI\n+GkzZdDjBAPfFHXd7AssUPhkpbnaGHABPb9O5q/brpFSo5Z2EcV40sg/BBxsVgTj/NNT8vFOY7Ye\nUeYbuDzUW01Y0WKNdsbbwVuEyspcbigwce9LpNNWPTJri8uxbWkTFmljfzEkDyj3PFX4XhSK2xPa\n/wCp+pWY2JDHcIqlUMuc47H60NZ/HWqtqFncOcJFcCR0iGN4PUe/AIrqlH6MtkfW9Y+KbHTGtRcF\nokuQCXYY2Z6Z/pUGubW+JQFWWT8q53A0n/yWeaekso4UDIiLjjOAAPrSqWyW4mkcBidnO3jIHpXK\nm28Bdi6+t7yxvZ5p5naC88oG7/tgqMD25oDVtGvbiaJoIXeMRKGctgE4FdLaXpGUbBJdLksZkSRg\nzMuSVHCcnjPrTSwEccuxifDmjKPgEbsjFJKSbGguoLos91pxu9LcFYZFcRAnJIHUcexzVdr8IJq2\nnCZpZYLyEmJzJyCOxB+mKEuan/8AhTBnq+n3WtWtrpmoyF0hKmKbeASehDf2NZP4s+HjY6Q82mRx\nTQQTPBOqrloWB4LdyKbi5u7wnKC9EFjoyyWqNfStaCQFo2ZCQ/oM05+F0tvh3XrfUL6Nyscnhrt6\nbTkM2fauyU7TSIJaavUoI/GklsZFaGU5RQOOP/silT2cnzTu20K8eQHO5VGOP3FeWm4sPI7ZHXdY\nvtF0VrGKwjms5rfzvt8sbNngY9Cf2rG2F7dwq4jnkR42Ei7zkHsRj0Ndn8dLrYFY9i0mx+IL2S+S\n5FlFEN04AyAw/mHPetfoenqIJ7eHUIL/ADCd6hfz45XcO5+lVk2UjjMR8QNdaXaNDKximmVvFG3b\nwSPKPaqPgbT1nvZrq5jjdI1Cp4q71D+uOmcZqi/rYXH8jZo1xHfLZyot/buGbMNuEkjB7jaOua5a\n2uoaVbTNcyXFxGzq8ZlJ2pzgHHY84qU6oePpZJpCXmvyfhMJVcKecIwbnPuQKrvtKm0sxTWmlyO2\nWIlRCdq58vPrjFS/wp4OrSPUoLO0e5hkd5DuVXHK8Zyc81ZJdyRWUjwxSTzyMNqwYbZjvyazoxO7\n8eeWOGZ1UGQOuTtP3FD30Wnidr6dvlphIsEMikrjPX/7rXQOp6K9vIk+T1KNdQjlG0582/6diaQW\nWirFcwtaBlEd28q7+i4H5SKoiE1aNdBNIwDN4MbZwFzyazetXNkIQxm23LufwmQ4465wOPaiiPXL\nFDW5vYZVs5EnBG54HGHH0p58M2EkGgZtBvL+I0q7sFDkDn7A1Dl1UaMbD9NdbUoyYnEwCb2Ulc9x\n9q7/ABGKGKSO8MROcpgjlT3oRVJI6Y4hbo1pp2rSTW2oEFxKzRqx6/TtSj4j0aXSNSVGjZoTlkdP\nKMdh9q6ON/sWeojbaVdLtkEEmJFzl3GSPpXJrVZZPAnhTp5nIGB7cVRzXiIqD+kERYbZ4YUj8OP8\nQDt+9Ay6heNF40TzSmTIGUDBSOx9BSptsq0kF2+m3wshPLbosKHLgvtDk+g/50oH+J3eng+DcowD\nbo0353fen+CJqzR658U2Wp/DttE9u1xcOMuAf+2R7isiVthIviCWNW//AGjjvjpU1FefR3I1A0Vt\nShtotJuYHEab4xM+2U56gHHm/WlDwI2oESugCSYMqp+Xnmsk1hRu0aL4ms3hs9PtbS3ha2nO7xZp\nML9SB+b70Pf5sjBg+HZwt5hGc7uzZGOmOlMp3SFjGk2xBdafbvdNboUkLN4g38ZXsTzwMUbbWscA\njXTZFgIcvLIRhSw6cjsOfrTt0gJWO5tPsf4Ql5BcNcN82Wco+1SxGTnPUcfvVWqafFqvw5NcRRQt\nLA4dlJ52muRyLJAtjp8kkVnFbsTEkTM9vuwpJyRwOtZS11e6t2k3MSAc4cE4+9dHHLsc/ImsC5fi\nl7eXwjEhcKGPpyM0A+pXVzcg7cqxGeDxTyj2VEUqKUvbw5Lw7VB4OM0NPeXcrHLOAemFpI8Si7M9\nJCK4RQWkJDDIDcn9KnBumVjtJxwSB0qtI2FB095ZTgoh+uKvtbYrIArE87QR3PtTAGei6pcWlo5M\n+wbsImP+4e5Gewq+4vJUAaUsxYbsmpu7BiFhuo5LjEiB/QGvUaDZ9ZvJ21HRY7WKGRpUceJHKmQc\nsRkEjp71yG2fStTjthJI9lAmdhySSevPpzXnJWjtlJJHLi4ULHDdwW17A0+9ASQ0OSP81VqOi6Pe\naRJD4bwLNMZWCyDhwCAf3pofjhl5ZzTNK034YgHgXl2Yr3jwzhgr4/MMdKZWmqanDG1s15fhIxkO\n8MZ3L/WrrlcRUkzkWvXz3ASDUE9QZ7bn6cH05oD4oh+IL/UbZ4r3x4ZAu2SBdqZz3+lT5ORuIHGt\nQJrl4y2kOkmdrzwiWnlc53P6Ae1BwKtvHtgiHzL8Rv0CVwydNJEp6xhpWh7NHub+aQtuQ483J55+\nnPpTBVkt7K2e+gjmWdNrSoOcdgQatoItXTA4NNhj8V4Zt0UhOFGcx88V25aaCVJZZC2GBjPAwvcn\n19K53FJjuNBE929/Es0RWSHGX8KHeXX0A7GsT8WxX+rSKw0+9htkXyQrbELjP5iQev1rp/jtLWVi\nrQo1CTTRp8CpCI5iMkgkk8Y5+9A6bffLXSO77QhwrFcqp9cV2Rt3ZK6PojfEWm/GXwpcW13IFuYP\nKshXnd/KfYHkVPS729i+H1ngQJNaQqSNucqCwJHvwD96jNOmjpi7Qemp3mpSG2uLhGOxs4GNpA7+\n3r9aGTUHv7VoxM6xI3hhmOGYrgnpXnvl/wCadov/AM0/C0XctzbWklwo8KeIqwYY8wJH9MUt1nV0\ntmR7eZ/CzsYHOA4/8c10R5VN0Tnx9dGPwfqHzguZJZFuFGH8OSPnA/mBPB+lahp7WeF/AWLLt5Yz\nx9R7UuKTRzP0WX2iQWsfzSs1q0aEOwcsqDqe/Oavto455IWgTx7UxZ55y1N17azWV3uJZnSGNVdA\nQsZ6AgUNFp0vhyzzSKJLqEi5jX8rAnIPsaXhfVNIZMylpq0mnfNaZcqktgHYxluDGvTqeBRug2qz\n/E9pAUW6091LiZl8q8E4OenP612Qb62xGqZoviD+GPZ2dvbXaQyWrru8JRjHQkj0/wAVZPZ2mmQJ\nM6QyQyHw53U5G31/WudpXrJfRdqdnFqvwpeG0MkwCktAowy4PUeor59YWcdnardXcgbcxTwwASgx\n1I6jrVv47VOikUmNNKsVtrXUobQvLDcbUXK4yRz19Kb2OiTaRbyzR2/gM0WGLNkqT0P0p5zoLfVi\nW4i1O+0Fv4vbNd22WhMyY3DBGGH/ADmmWj6JaaTZacltbS3wkd5HuGfYik8YZRyeAKZcicaQU7/I\nf6pquoC2x4ngKT1hGz9+tJIrub5K9a5lmuYUUO8Zck9e3vnFM6oZPRnNfjTry1mkDGO6EeC/8px/\nWr9Ru5ob6SzlmZlfiMhiBjPSpeFQmC5hNtHGPGa7glJYFuo9MnqKXWLTXdncvCklrIsrMVLYGSez\ndfSgzIpeSVbtJLpnYSqYleTkbgfWjJLi2+JrNreCWCR//wAiZ/7RA6A9eRzRrQSaO2elXulzxM8g\nljRsgKcge49KZT2Nvb23zbyrEm9jy2MnAzmnbsh4hB8Q3LR6rpMsZLQu/wD3IxkHOAKWa3dacNRn\njvbVfFSQ7ZA3OCOD7/espaBqlppvh6HQrq2gS3t1yqArO4ALEdeQadNoMcVhdrpjJHJcrnc6nFSl\nFSYEnHwzlnHqS6bFp21TLDMT+GcZHpnvQTpb3EuyeIGRTtC45yO1ParBIt2VnQoXuPEedwQfyZAA\nPX7U8ubhdUgkgmVQYISVYjOR61ospFtman1R4rSASOAqLhc5zig1jXUF3G+8Fy3kXGQ31NHo27Gt\nIrn+G9TnjRA6g8kMDw31xV1jY3UdzFFeKsSkbSU43iqpUhXoxvbeC7uYIoHc29rGxKHqx2nJNZeK\nwtZw8TIQqnBOcUW6RGUNwNsbWDS2f5VW84GcnP8AWi47ksypNapdqeBHIMivOm5OVlYOlRC2jtoN\nfS5t33rEwfYSMKc/kPemGoss17LPa2scSzygsiLyOOcZ/tXZBv6O0qPPHJqq6eJp1KQOQQT6HoPe\niGsVu5DHBqiiGVyrhzgheenvR+4Mmuov175VHjhj3MoxDJIgxlQOM/ah2uYbiza3iij8KMguqcAe\n5z1NUkKn105aTvJcWUNtn5Qxu78behPYfaj4dYt2tFuIoPLJL4ciBeg7ZqE4Oh1P9h91o5ay8WyL\nQtJnw5ckbVPJ5rH31rZ2t3JAGM0qtlgowD3+9PxOlRLlt6Lr2NEj3YRMgHGBlR9aKFubezSWWUOW\nOV5C8fSupI50ctII2LNJIsaH8rnzc+mBzRqQ2xtidoLjq0jhFo0g2Rf5KVAqPAhznyszn9cUQ0lr\nEmAvmPJIHWh/4TbE2qWzXQU2MLt3ZsYA+9e0HSbu5u2EkJj2AlCTySfT7UG6Q/wq1TTIDdA210VE\nflVCC23HXn60fcPBeQRIsiiULtPHFJ3sS7FculXcEhI8Jh7V6nSNZ9it5tyOZGZgUAUo54Ge3p60\nF8U6bqct9ZTabcJGEG2QSvtVu44755H2rzYulZ2WHxNBcxsTHsZEBPcgZ4I9s0j12wlv1lETSW0S\nPtEuw4kPcL75rSal+UR/64MbeKeLTIbZoVfAGC2SQR2psuHgJ4JPBwBUk3eiRE13atabypLSPwWx\n+VfQUNbavcaZpstrFIoR87dy/lPtVvVRb1GfYTylZTcmedmySVHBotrfW/kBNpssPjrJ4eHI/DTH\nLHOR7Ur4o+sgl+Q90e+uZPguSK6aMxRjYsqg/i5PJH3rUXwt4tCtY5mjYgBSWIGF9aZUxK/IzyWI\nt5hcqxeJsxqVPlAHIzUdS0+S5gilt0Mu08r/ALfcVz8nG8o6JrssFyXV/YXTWttEd9r+MjIAysM8\nqe4Pv61L4msLHXrFdaZ7kGWNljVW5EoH5SO3SrcceqoRpxVI+ZT2cmEADEhcHJ7g1TIjGXBXz4x7\nV3IhdhWj315o18JodhjPDoy7lYc8EYr6voMU0sMGpPAbAPGVe3Vsq4JBDAdhn+lS5f8AC3FKlQk0\nTTVm+J5NMZhJ4pkfUJ1J4UgjYp9c8ml2qfCGpaRBFa2pkmje6cJIpydpA25/Q1zTlCKqX0s56F29\nhdSaNPBPM8NzplwzBHJKurKMcj3GfvV2nQfxt0FzG8KzQxsgePASVTjJPv8A0NZKMmpRYJy/GwzT\nL2FNShtZis4jcxxRxtsCEnknHJ+/FaHU9PR5vl7CIQsyEDeSDn60jS2zlTf0vtmdbKOyvUQzsm7w\nyOHAPXNKjeTTNdWUP4Uloxkd41wGTggD+n2oReUVVMlf3UD2qX9srqJTtdMeYMKBgkubae4vfF3i\nXg2sgB4GMYxUON9JMl3pgOuW1zevBFawDY7FnymCB6Z9av0OaGGWNLZGSPOJlUDDHsc/2rslL8R5\nSs0F7bWpKPPGqM/l8Qr+YdcZH0xV91As6KsCxQwoSMbQd7Y46iuPjlXJT+oVahXpr3vw7BcqZ7ea\nVF8TwT1wPQ0t1uPTdYkt7qXTflZbtcGa3kHX0Ixg12Ql1RVRA0SCCyXTBeXNq6SnZIyhlJPY+nbn\n3o/Qbi8i1MWeqbp5oyw2oclkHRsg4x9aaTUheSkcfVTp128LwNLHLJvAbBBPtij57iQXHCxrGFBC\n5zwRnFKsJxbSBbpIr0BLV5nZ+PDZOAfY0Lb6W0U8tvMMNKgUBWz0OTnHGfrXSmmi0RheWZe2EN2o\nmjGSHJ/IcZA+tekmdrtVtts0dwBJtlfGDgA4btSstZO+gEeTgGdHOAzZzxwP/NLrK/l+V+QukiW4\nm3FIgc5PvQMtFUmoXVhCLO+t5AXDHD52/UehoD4esprYC8iMtsiynGAT4mf606ZNmw0nU/mY5N25\nfCOxgxqGu63b22lqtxbPKpuPDO05wAAd2PrSSMlbF9muoXSGNdyTTSeJbuiBe3Hl9qPX4Vtdb05G\n1rI1EDBdXxn61PRpRTxie9tB8O6TcW9reyRNMwJYdUAPAHPGTmlVhrlybp521W73HjLkkn6c1VRt\nEpv9Gk0DUiNQjnNwZY1zvz3P+aZRRWaXv8SihkBVtxZm3Lz6jrUnHp6IluiL4zs3vcSadKC55aNW\nxuB4PP1oF77+B2cLXcmSUET+GD07++cUYNNDRVNmaVNSF148l54kXIQSbsFfsKIS/tEZWjvVLgZZ\nBEXUn2OBXYvMEa0ZafrEBPnmMH+wgEFz9jWg+eSawMrlJhHj/ufmH3FKm36UoBWKaeeZ4T5ZYZMF\nGztypxSnQtFuS6TSS5jMm2VN2WHofpmi43gl4HSLFa6pPBOpZI1Uq6t1Y9v2qWmxNdtLNHEkyZ2M\nQ2Nh7EVzy4urwMdJfwxbNGKqis75dieSPWqntZ7pWkDFip4x1xVoqvTMtspY4HLPAdo86sW6OPaq\n/EuUWRoZByuFUADb75rL0PbKB9Ot71PGMuHhcZwefN65qjUrW6aNVHhxkjCGNdxz7+tUxaI3ZoNO\n0x7PSLWG8IRj+IJiR6/l/pQGm6dNpt5cTS3CNbscMGTaNx6YqN3Y6Xg2OtXAt2tZwmbfhVfO11I/\nakEpmu4DCsUBYk7ZANzL7Zz0pYR+seaXwrj0t7cDfaNeOp6DlB9fWhjCHlb+JwFiWyOMFR6D2qvJ\nyOMbRz1p69i+UBeyaORGGQVG1h7YNds3juEEdxbNMcFsSqcGmhyqUQUSdBLBshURqOAiAL7mpaVo\nc0zpK8TG3wTwRyfT7mmWCULdSa/uA3jQzxMCQkSDaoA7Y7/WgrL5m0eS5lLxhEYJzyzEYUZ/Wmwa\nwFb1jhG8xHf1q+2l8R0wrsW6KvU1CUa0FDaG4ae4FoIGMnQnfux9a9VE8B1R9oxtIRsb4Vw0AYDb\n7nH64qlbyNp5YXTLIMgf7uO1cKdYzoEtzqUK6ZHe2MEqOkpjlRl/LTk3ED6SJ44WiQJkq3LDPehG\nLi2kwud+i+O8bwf+4sp744PPT9sUVaIkSD8N0J5/NkVObQEy2WE3CqigFznAHORQLaIp/EkRXUHO\nOlNxu1pVSpFraPZTL57eNMHIKjB/agdUtoYbeUwREM6GJCBnBIOatJfiSbdjFLaGz+Ezbj8VEgOE\nH0Pf61nLayi3RHUhNcqyYRhMSBkcD6j+1cHLJxWCVbO3E0ml7msiXtyArxSvll55OK0dhE9ikU8M\nyyW7+Ykn17Cr8HIuWN0dCaaoAEOnXl3Lc2hmDLMTKACPEb/Ht0qK3vi6LqFvctFEJZGit3iG0Hjh\nvrkY+1XSXrH+GLg+BdWvJWEC7WibDtKcAE98UW3+mF1ZQtJf3caFT+RDkkeoNUlypLEc/wDzrRno\nOkWmlAhWVvmCMtI4O3HTy9vrWja2lk/GDiMKu0knCnkHPv0rjUnyPszRpCfTLWEfE5l+YmxllMUa\nlRuOck/XjHvmm2n2t7BpaxPMtxPAD5i+NxyQBn6GjyRU42zTaor+HXnvJL61nhEYx5skHn0oGP4g\nt455o57fa0bFEK4831/52pFx9YpL4HtUUVXlkunmW40xfxr5TIzKMsT/ALR6c88U/spxc6faSXsp\nS5C7XJGPNVGxXov+LILl9P0zUYbtIp7ZSXVzw/PtV9pHaXd+rMrJJcxHxdpwD75oNK0GtPfEl2mh\n6JDHYIGUybZGZS4A9frSVtUg2RN8qniTDh4+dvPGQOlLNJPCbVstkS7/AI0VVXdA6krtA2cct79+\nKUX6rY6lJDGHjZSJFKrt3DrketLK6tFKtUan4e122+IYJLC9UpcJyJAfzj1B7GqNRnm+Glkt7qSW\n9guf+zJj8RCc/Y44/Wn6qVN/AR9ozWq65NqlnZvC7/OIDFMFXb0/3Y6VGPVrKzlBuIvHiVwGUH8h\nNUjFyLppLS3W9bt3nSXTMGBlBbr26qT9/wBqq0m8a6Wa2tMLdXAy8jP/ACD+UfU0WqZzcjthEt7I\nZGsZ51QwkgBMDGPSmtvDcXkFvJDaSRx42MX6sR/N9KSV0FeBdukltsVhk79xYjnHpVlm1vdau8c0\nYt7pTmJlOBKnfI6ZFdHH/XSsUyGpyNdeNDZIzvFOFZQeGwmeP+dqE0edIbCV7+QIxkKxRy9UB5/S\niVDYBFdsWinRlA5ZGyM9eKFtbVNU1aXDiO8VA8bEYOMYz+wpX+gxRM2013brK10biGN9ypKvJ/8A\nbnrQuq3r2lk7ErHIwzEQvT2HajHBJaIdNvx40rykRQOviXMzNgKR0wPUntVkkl/qt/FHaAMs/nLK\ncKi+pPbj1p2zLC+NLmyZjY3Mkwf8JJCPyg/m256ZxT/Sr+RYoGmjLzF9gYDPHq30qUpKrY+VZTrP\nw+mpRPGsjJPOeXJJXmsF/AL6z+I5tJnAVLfzNMy+VU/3Zo8XIpJs53L4M/EtfCaCxLIidJM+Z/c0\n40q1muIZruG5SAp5bhH8wKgcH2/8Uk+XaEvbG9r/AA5r58RxFFC574J65ofWNKMGnGSBd8AcysGP\nTimikWe+GSnv5CIZLVliKg+dhkkHtzSm2s7mG9V4ZbeX5jJcREKcemTjnntXTEm/TTWmj2k91JbT\nJtkCnEm/O0gcVKHS4bSGOTxfOzbcI3ByRz+9TbaZX1ELW4mhlnlt3ETp5FZh1PaibVY3kExZIZ1Y\nEmPo478dqtf0hJBuqWkMtjPLiQPFGWTCjzHIxS7TNGRrc3dy8sK7htwfzevHoKRuxo4Nn020axEj\nXibI13ByQC30zSq8sWa5QWzXOxV3EbcHHrRQH6BufDidztZhwiE+Y89wKvfw7e3RsSySkedDHgMe\nwBor0y8B0+IrQTKtyr26J/KibizehHpTS3+ILe+uooRbRRLs8QyiPHAGT/Q0ZukCKBI/iVprfi2a\nOFpCmQmQW9fvVt1dp4dubiSQxFuVRQ23nvmpqh6Kpr2O/jlNvGxRTg5XJIHTvSae3hciVHwytkBU\nwM1VUsEbBJbm9luC0ssgPsSK4HKdSST1JOahyaqFT0nFOc/mxjpgUUt/J/NM7AdtxriVxeDC+W+k\nS6KtyY1MgGe2DQsHxnd2dqIbeNF5OCedvevS43a0mw1fjye6gWO4tROxXB5yPrVOpa7ps0CRQrLg\njLg/lB9c1egAJtNOeQH5pkZh5T1FPtMubbS7RVEkHiDrKOpGfWklG1QSj5i388ltLKzyufEcHrnt\n7CvUFJLBqPpn8Pl8QytPIMneypkbj9aZ6YkErgLN4dzEfySpnPrXm9bZV+YLNehS1Ez2xklOQZ07\nYPoBSQwSWniXcyyLbY58aQjA6DjOaVRkkznptjPSbkNzbzRyxAElFq641v5eIPHEk+7O9M4al6v6\nVuj1zc6nIbNtPBtZJYy2Sc/YVnH17VbeZ1N3JubO5WA/vVKTwpGn6Pl+IrX+CRSS3IF064K9SD6n\n05H71fdXawy2vy4F665aRUbAzjGM/eqSkkhH7Rdezzn4duJGiSHfHt2q+7aScdfvSWJ57O1XSJYw\nq28vi7/5uRgZ/WuLltK0Dx0SmaNrW31KSIzMwbMKDzMRjr+v7U8jvRNpYQKkUhi3IhxhD3GKf+J+\nMaZWKA7D5lok/BPqdrAY704g0OyaBykCyPKxkYSHJjY9celda/0q3QaLG8FmCqmSVDt2qeSPese1\ntLH8RmW+up5ArYS2ZSo5/bikcfiJOS8IS6dpNtcyTqZbm4xmO1ikBBGecnrjPYVz/wBTTX2iG3WC\nCKbx/BEfG0DnAA+1FQSVkl6VtdPYa5DPHMN/iL5WJKsegz7CuXUc1nJf+NI8brOzo8ZIVT6UrxGa\n9G/whc/MwRur+LLK4MhbP05NZTXJzaz3yyQLFOZzjjjjOSPrTJ5o0lUUPNKuU/hqQXsAjkba6uCQ\nRyOn2o+CwleO4uCr3CtlhskGMDoMY61Pr28DWIV/EE3ylhYSXUO+ORDtRpCjKMnKn1x/mqLq9TSb\nCxulzciRijebGPYj6Uf+f0ZrLAh8YXQZVVkRGbO3G/ip6fDEdas5PC2PcSkyNjaCowRx+uftS9aZ\nz62OZvii0srKQKwa9k3FkxwpOev0qVvosWo/CCSWswlwSyGQbNjZ5A77cmi0qot6E6LZ2+kXRuXt\nWcMn5Ac4x1x606/idjqSR3drjcoKqso6H0xR4lcdAk60zXxMvy2lXMkVqYry4cL4Vsobee+ePTBr\nI6l8L3cPw6NQdWjk8TDxSDkqeh9j/mn459TKf7IHSrzTrQQXKlZs72jJ/L9RUIbVopFeNdzdcBsc\nUZO22TavRvbx29m0PzMKeHL2Ztzx+5NPLa4e3gW1huEuEOXDse2eBmpwldjxHoj8ax3hUEgAP/ik\nmoW4mvYZRJsaFwQ4PB9V9aunhaP7DSI7O1klnZEPi7d5GAPKO9A/IQyOrXtm3y7xsVuEJ4OOPrk0\nV6O0AaITaTGOCNnlc8+IQBkds08KG4uvm4pIQY5DGz4wsPTOe+P70svbGQBbaiNQt5XSUxCB8AEf\nmHtjvXZI4NY0N7a+d4zvLDEfnXHQ/fpWYpVafD2i30EUV1bu02MyBWI+556gY5q+fR447COz0hNu\nnM+64mMmGKA9Cep7+1L2bJyuwf8Ailo2tW+kxwGCEqCk7SbsZ9ffmnEMUUUrWcNyGSHI3Kv5gTye\nKSUWwrcZCCBV1n5dr1XtnXyYOW3fX+xqn4tuiLCWGWHJl8jyN1K0IS9RGZi9OsoxKfCIfHRCeSPW\nidCvEOqPas+Eul2PnnBHPT9qLjZNYh9GkNvchQQjXOVw3cjpRcutWkGn3On3xzL4GFUgnJ9KrH0v\nBujAEXHjrHbQq0Q/30Q8s1vA0klvbMij+VMEV0pJCSDvhDddX73EdsyRqVUFyQCx9j1q+6mvrPUm\ngFrAYVlYqxwS+DwR6Cpy2RaP9SV23jQma4gKhGMuyPqzDAVQffP7VTp+qbry3BWR/GIGJFXC+xxT\n/wDyTl6X3GtXDalJA9t4UcbFc5xnn+lH3ZktLaJjJE4bDqw/lx2+nNSuxkc1Bo75DIVgkXco/DXh\nc9iOo5pdqeqTNCbUK3ikBWkBHC+lZujMF09sPcSMu2KGHIOMmihq1kHjFxskfw96h+Bj1rR10FeB\n9hbRapYXUq29tAVUsNqZZ+vQ0ts7a8vJBDbeFHJGpiRH4JHU/wDM0HJp0ykYponKrwySQS5jidch\nuArH0BpfcRmRhEQAN+CT6Yp4r6Qn+iy2t7rRboTQTNIjnbiN/Ko/9w6mgUu1a5lW4PnclhhKaXtk\nn5RZJahl3JznvQE0DrknFT9EQMmQ3IP61a0R4KZbtgVFxVjHNQt1Mska4E0cAJYZ5B4P9TWaubSS\nCRxtYopIDjpXbx4jMmqNa2bP+WWQY6cqvr9T0qmxsLi/uRHAq7j/ALmCgfUk1ZMAQbZlke2mOXj6\nFORn60ZoUarqaI6JKBztIJGftQl4ZD+XT8ncrwJnO5QdpXn0r1cMoybK2fTmHz9s9vdzEOCVWGJe\npB4yevTBoLTQ9oy3Fs5j3sULvHncR1GTyOh6Yrnhki1HP/Ud1fIGtLONZfGMQMhBBAwCf1IrPzie\n9v5FvbkAKcsWXjA7CqSaQOqGL2k2hafBcQwRGCVQW2kliCOMc/qKpsr621GzK3cbwPkbTgAP2470\nktjYtIfA/JRRKbhtqYaPKggduuKRfEYtRqrm7mV0zhXh7k8kH0xQd4a0hEdQR3js4VRIBJkyYIMg\nJ6tWjstIS4u4hbODKSyBi+0EAd6lzXGqJqpMb28K2jNb30ytazR+ZscBwcdaidE8W6uN8oLTszhg\n+cjtVGuyGa3BddWF3ZeJKZHETREHa35HHOfvj96WW2seDPuv7mNIghIUgsGOOnsTQjCmVTpDiC4m\nuLuzl0+GQWMsWSCo6jrzWmvf/wBGXKS2heWN1xMCMbh2P2qrxjNk/wCPSR2crBZJGbbgcDFJdatL\nzVlkkv3i+USPKW8Ex3Anu3rWUrZKcMsxtvIsV29pA6NgfhSLhir45yfSmmg2cmo3Uj3NugSwdWVo\nx5pWAyM/Sq9REvodrT2rm18IrFKoyQRz0GM/Wua7rsVppr3jCSeS5Xyx48m4ADGaRRCLdNv7yC7t\nJPGjXaAZAeFXjpx1NaHV9B0nVby6uLp5fGdwcK3GSOg+vFJPzAN+C2LU7aG+MF4WHysuzdjBJA5/\nTFA6sZ2Rk05nO90nhw2GKtyGz6f4NBPqrGlLLJaosl9pyWF5di4maMssg42vuyFz9ip/+Qpbps0F\n5pKWlwxkkF40oxyQmAP7mnvLM5WhnL8NKdV0y2it2iYRb2yuWALHzf8AivokOh276dGqxILiMfhy\nsOVJ4/4KSDv00I/s+Sr8OX1l8VNpyyk3Esv5pI9y475/rWqjsZ9KvhNPetdEIE2qCqgcdF6DpQlL\n9Ak+rDblXvg0MMhgRVJbA83PTFB2+nyabZvsuZDuYlSB+XPNGLC5WU3F5cRRQSQRTzv4vRiFU988\nc1ReavrTKYhp9pGrsHGZRu4OQAWbFNGP0VJF0+k6tqWntM2kxiWQESO9yNy47+9C2/w1dSQRyLGl\nvPH5Ad5Jk46jAp3H4hseIq+IolL26zIq3kS4lJBG4Y+lCWWn/OxrEjIkkTAkbT5lJPf1FcaT47sV\nqnQ+X4nitleDwZHWNRyidP8APWhYLVZviDEKtJFcHxFOeI+hO79K6U20qOmGIZanq1vZzQ3OpLvs\nWZkB2ZRhj37570Dbpdz2Cz2uoh7N3/CVieAc8DB4xVdoy9JWSW93Z3M2oOkIiQJndtL5OM/WjrbT\nH025W3jCXFlMpd3kGWb0GenvWa+hsWX8P8Pvk/h1nHJEigpnowP85P14omaC+a6jLwqu9RlVkyBx\nyc/rSsBeZrfR1naMROHXDytJx9AO9L7XV1lN1Z3JWOOdAEkGFZe/GBissQHoRbw2jabJbrMlw+3K\nuT5s/WpWl2bZV/CdJ4xhWRgwxmoyei9fpTasIrgXcZV3E2Qm3GCfUdqLv7pri6eS5tVMkTrsjJzw\neu4UI4wONiO6uLbTmuXljFtLIQY90fldSOQD25rMWTGCPxbbyyqxJZhn2rp41ZFpeDQm7bw2uIZJ\nTCVw+cc5/rRusm3k1hpEcgYVyjNk5xz9qaXpfjj+InuYGe4kKSFEZyR24riwtPbpbS27TM0mFk8Q\nIc/XoadMk1pfbtNY3CWzzvHslBIYg4IP9Kb628P8YnWTLOG3IynaOcGlaTLJYW6zCksFpCXSNpUL\nFRxt70usLeKLzBmLc4XHfPWgnlCSQ/trcX0kniLG3hxbyJMYPrz3NK7iNZ1cZOCPKBxt+lGqQqbJ\nwwBJVmiLsJwEl2fmyOlQ/hZmmJUkRM20hm5HrU290Z+BVtpMFvcv4UpgjVTIxPI2jsR6Vb/CNK1R\nnlEcLuU2ojN4YZSc8Gi8dgi7jpYti2gaXIqW0kk1wcROvnRPYkd6jPdyQ6Z4BdUmK5yUAYEjkVmr\nKQfwVW+vFLIWd3Zi4jQBE3JjYPbvn3oDWvAi1iWC2kLIqoV3dRkdPtTK7wWa+it7iXT7vDIxwu7d\n2Jqu8+JkvLEeJp9vIynYQ+dy+hB96r8IICs7+YsPDY+wPIq1tdi5ju4nVlOMpyDUowb8E+l0V1pt\nyVEd4me4YEV43dvFcqmZHG4fkFZQd6Etj/H1oXLKywmIqQy8nH+arutPZLqRWZPAD525znnj9a6K\npGF9/Yu80gAwzYG309qrjsZ4EKNGQSclSM/tW7ClMelTteIxibBfPTjtTy6m1JXlgtjDZQM+MQIE\nJAPXdjJ/WnTCEgxW7qhAdXG4SO2S3rx9a9S0Gzc3epXY1eBk3sGB4Vefr7jHpQbLcWc09wJSbeNW\nKlMkljkZx25NedVadhfHZ3kulWtxYgTIsTAnoS7MSfL37fpWYN8p1MeKrTDeR4KdyD0+taTX0nKS\nNLc6razfDxEEcd2kJLeGH2SQEeo9unvWdnT+JSQXiSpCGABCoRtI7/StGvSaNFdaU0mmB5rmZlVd\nviRSbh6jiktxol/cWMs8sxitEUS+HLJ+djwfKO9IuTswy8AbXT1a9S1OFD8ZbpzWqg+H9Q8G1e1u\nFY27YZen396Zq9IQT+FeqzR3bx2/jOoRsOpyMdulObLTLXTS1wLhp3EZJUNu49hTR0rVMGTXLHVr\nIvp95Ejo4EizxnDYPTHWg5NCsH8SSLw592GWMPuAOeg/xVGvp0RWaONNvHgkCquyOU4xJ5RGw7D0\nrmo/GLw3E1v8nxEMF85Q5APBpJLB0hfpOpzSSSRMCnjKZAT0BBzjselCXes3/wDGAREhimYhZQNo\n46Z9q3HiJzR6fSLODVBd6c0UbS8OA2VD/wAxwfsap0jULi21u7juItiiI7CrYXsA3vwTxTtv00fC\nOqzw3erraRx+KlydiTr5GUAdfoBzVzLb3mnRwIym3hO2PuuQOpPryaUFJgF+DbWq21paphH8R5FJ\nZm+lErd3iiO7ErJMqkyqOrHA6/oKXrZNo8k/imwuWeKSWWbEscyY42/Trg9feoaOYP4faeIrI8LG\n3KhslVY5AzjkAk1nG1Qm1Q506z0mS4hsJUleNo3TxXPm3Fgw5/50pTpGkW2m/F9zDBbzKNm5mkOR\n5hnCn700YtRGVmplnubC/WWG3jl8ZgJJGkAKJjtWmtbpDtWNlcKNxA+nFHrSLIX6loUGoakLtnaG\n4MYiEisR3zg/561nEhfT9RmOo3EUkKHw1Cksd3uaDgJKFuzmr3mfC+RkCneNzqOFU9+aqa1kkeNT\nf+KY2DEocDBGOneg1+huqYou/kZLNd9089xDIQhhTYTg989aWzarohuJX+WkeQv/APknyMjjpT8a\nf0VwoPk+JZ4rEx2bAEqNgRCQOTnOelLV+IdYmljgSSSNJQq8AE56ZB7UXLRU68GGpafcLBtdTNNn\nmSXgt6AZ71yxtXWc+FcTpJjmGSIhVLDgnn9K5OTWZrSOmNdWUzQ3ccxRX3O65Ix6e1aC01ODx5nS\n1YB+doxgDpVl/VFo4E6lpKXlta6ffNHKir4gXbjqen9K5b2Onm3+SsmjjMJ80YYD9qppnrFWoQeG\n1zFNHG9pbYZVaMktk7cA0VaTapFa/KmI/Lx/9koM8E9Dmi5fCiSom1vLHaML+1k325Yxxq2FfPOC\ne4pE17dRXMcl6kkEMv5sJjC56CkMMZrOyuDGqHxEPmRywyB9qnqWmzXOi5TwtqTZRGXHiD3btSti\nSdFljO73MVs8SMqxkhEwAMc4Jq1JI5NEklRo0uZJCFhBB24z+1I4pit/SiG3kvxFLcmCKQRb3aI4\n2kHuPpXbO7i1CKZppopblcgGM4bYO+e+PSmX6NGVkHubabREt5SbnyvujI5YHp1qgwafPokXyVq7\nRqMOAcGMk9z1rOUovBJR0k/wyt+0kkPiPI4B2mTrj/6FXan8L/K6fbt8p4UjIyyMG3SHAHr7E1W7\n0rHFQpubf/8ASLrvU2sahEmZRtfj9qtitoIdMRrmEbS7smxsq7D/AMCt2dYavpnr1DfXEjQ7VYsC\nSOijNay5S1j1PxZFeTEKNEGPt1IpmZWZ/UbkXWpJlvHbIVTgr9qZW8aq28ReGQhDE4zmg3QGdSEy\n2glkjmKA43FqjtZo5HRdwU4BBzRtUTaCrKExRyW85ZJLiUIqoM7QRndntQDfhTRiAnliG3t0qfpS\nsHVk7SB0aJiBEwAP83HSler2S2lnDcrdhFlGFgKEYOBxVG0JBaT0i7vLWFVtpmiLcqC2V/QjrQ+o\nast8Ykum8dwSV2+Uo3PXGO/rWsZraO6jDJb3Ins8qBxndu2sBznPrS/VTPfJcaoI/EbI3hR0GOD9\nOKylWiziyCKmrNbW74hIj2KexNeHwmtxe3VtG8LGMKJB0POCMVXZeHOnXpVL8H6jZSNm2lQq2VcL\nuVh9RSuT4XmNwZLhlA6kKOv3qsYNIFqzkuiKCBAPCVeueSfpRenI0A85SXngFPMPfPSmQzLJIJbi\nWLwJChD+bd3FG6hbh5wkOwRgL5jgHOKzAhe9tOrsWnjdF5wzAVxJVkO7ejE9lNI4sFl673YbMnkY\nJGMY/tVOoXsvjBY9pYEk7ulFIwhabfc+Iqky7udh616nCfTLTUH1iS1+VkMJZTGN7ZUMrkA5+hH6\nUX8T6lHbQiHTrlGZnw7ooyMcnHqM+tefT+nU3ovb4ultbGOOMq7zKVy0OzHuOcH9KzVncgFy8CS+\nHhtwznr61Dlj2+kJ1eF2rsLe7le2tjcRygkkPnaT0FV2YuFgWF1jRT5cEj+lCP4r00cGZu2+HdPE\nduXL3AKMsxJVV6+UdqJ0y2m1GKG7GGtWYq7ZHkbryM0+N2gvQTUYYjGssMhIlkwHQHIIre6RaTT6\nETGzQsQAkkjc9Acn96rVoEFokOmC4e7kSdLloW8jI2eM85455zRscqae7sHjVJAWG7jB9Cf1qCyd\nHR1sXHSITq8V9Y3ENqjoQ0QYJ4hPXDH3qmXSL60shGbKeFTvVp94kUN1DZU1amx8WFcN38np8cOp\nu0s5J3PEAMjIGf3FMLjTBG48OSYw7NoYD9CTU5FFVFXiXEF9HK9yYYl8oWaQ7pMdAMD602lurrwj\nBZ3FuB4h2tIgOV/28iqxWEnpC7ka2giS4ksWMswQZQLgnn09qT3dxCxuoLaG0tSpDCRXYBs446eo\n7UWgUU6dIY7SRd1uxc+DFJE2SCRk4z7UTo9rJe2EttJLBEGVsusmRjsT75GK1CUSutLWyiupreRb\nd5UWKKSRiBHnrg85qeh2ieCYpmN0AuGmnQIjE9gOv/M1qsLVIH1U/wDVQQ2MhjjjxuNuqnBHY5z/\nAFq7RtJawnuIWkmuPmYWAklH/bbquPvj9aCmniEfG0rYLqV2q6rppuJZZWESmK1hXq2fzMT0HtT+\n/vxDZwT+H4glIUkclT6Zot0BWK9SQwanbzywGdZcKCrcL9QTWpdZvD8GyQmbCgLjG33OO1ZMZMVa\nhdz2Wk3NoySXjEEvMg5Lk9B9MfpSu1XxtOaSFSJmO5RK2FI75pXoZMD1KK7g0qeRUSXdtjEQ82Ac\nEnjpS6PVJobpZJI0t5R/KOhGRzg8dD2oRT+k3LcDNThLXdyG2iIvxkZypHB+tJLr5GG3Ig3JcxlV\nVQAQR6mqBkwU3kn5lmdd3DYNe0m4W81yCyuHfwZCFXYc+bJx++M+1KntAitNT8W2nzepWuntMIYI\nAolnY5TefQf3qPw/DMtjdQ6lOJ4QCsS7vxGweCD6cd6jzOkUoIvbr5iRFt7iQMIuYFyeccnPevaW\n0/yl0sNtJNcTpiNgnAxjr7U/Gm0hovBvqkSSzf8AU30EF3LEqlVbaYyAMY+9J7C9gsbicyWwN6gK\nGZsvuP2OB+lPK0zLGDa78UTJFaSW086LOCCSFClweRjGavj1O/jv4kfUJi5XcUD+UD0Iz174olLR\n291rVJb6GCDUDAMMx8XO1sdie3SjtU1m5tdJspYV+Ya5858WMOoXHQH61r+MHmiWw+IYLuZVvtPt\nvIxAktiYnDf0p5PqMGs2Qt4bqWKSMg/Lzpktj3HBFJJV4K121gU9m1qUlhkJikA3oU59/NUfkYVl\ngNs8ayKd5AbkjvkHg+tSchEz2j289tfXE3zzTQNnC+HtzQGpRTtrcV9bQrAsQ8iFcI3qSR14pU6k\nL4zuozjwo7hTFli25Q3Cj68/2qvT52+aFijz7JgrgRjBz79sfWuqrKmpgJsrpDhpvDAHjKTjPf60\nXrWpLcWFtczvlGl2KyqQwJz19qeKwwjk0yOTxJ3ldY9vCx+dW98UM9wqT20cVxaui4xD+Xdk84J6\nGkHO65FZi9ESRR2ly6l9pzyB+1V6lclViklYSK0IQHoVCkjFFIHgpt445bp5GI4GOO1W2uqouqJt\nUtbkeGQT5vTI+nWl5FgjZaJdRjzabnKl8GNe5HpTiNooLdpmjEcoxmBmyze3tUopisOupbZbWBbN\ndt5O28rM20Kq87R6mspq8TQXswuHSQuxkIhHKkn/AJ0q1bSM3mjRLzwtOaa2fxkjGAJeD96Emnkh\n+GLa7uYZZJFnkWLeMgZxyT6Z4FCvg0KWiG51XWdMntZLi2YZ2yjd9TsyRwPpQV1cTyvJI0QS4kyT\nJu4Y/TtVFEa9JyahNGAbS4c3BIMglUbAfX35rQ6Xdo1qzRAqNm25tyc7wCM7cnp1/Si44DvYuCW8\nlteGC4MDw+YIwwy88YoHTdYuBcyPK26V/wA5zVeN4cso6bbSvjPESI4I4+x4pk8FjrkO91ZC3JeH\nAP8Az61dMRoUXnwXIH8WyuUuCeAkvlYffvSG50G8glCyxNAIzlgwJH1rJBC4IUs4NuQ00jZBb+Vf\naim07Ur2MfINEGbIUyHgYpW6MlZm7HXrdWmh1GL5yXxNrN4YGz1x61o4dMs5YhcQwRvGejJ1Wmi0\nxZJp4US6TDIyvGzKA2X/ABCcj70quvh/5mZzHdZXdwrCjKP6BZ6S4W0URQQhJBxynXHevVPqPZoJ\no9N03TYLGWaP+IsN8jBuUz0UjsSKUxS2jWtwOWuMbY8Hbt5/qRXC5Fwa/tLw6bFLcCc+E5GHB2qO\nMc/rUtIcw4jydtxkcAZfp61Nu9JSVM0dzZreW6WduPAhTIKugIz67hRsHw/oeh2kJ1SA3t0RjbEx\nRVH9Sa0ONS9KKqKdU+JdKudMnij0C4jMJHgm4jd1b6+n60Re/F9tZaDaw6J8vb3EoDziGIBVOOeo\n61Xqk6Ru1eC1td1VbWWR9Wg3RlAkKRgs+fTA44rn8cvree4ZJ28e5iBKSfyA9MCknKhl/oBYPd2j\nzzR6gIodmZJVHJYckY/XpWhsL611myhjvfEDSKFaRACU9HI+9K0pfkWW+Bc2lQrJvv5PmbZLIRlO\no3Lzu9j1pdpVrNLpzR6ZqJNrb7lfc2eTyOtOq+MDu9Fdyk934AuIEu7rPmGCE2+u4H2FaHTWN3pM\n0O6K1xEI/CbLghe+SfT+lZJDiuHQbeeNI1v45hC5OFYuV9v60b8qxvLiUTqVhZmI/wBnfmjVAy6A\nbl7i9sSstlLJEzB0kOcAgnkHFVth9HxczKGj/EhRlGTjIwR1PP8AWlu8YXFeohLoFzc3FqbR40sY\nG3TSysAiZ789O3Ap3YXOn21nM0Ei3MkrbHldAm44ydqjtT3SEopnd7trdfEeMsxffjDBfXnpSzSb\nOW6u57S2vZZtjF49yZxnkHP/ADrSNtKyqSeFUN2sd60sy7ZF/Oj9GOcUbefFdta3bWs7PHlVIliU\nYQkA5I7j6VHir1G5U3hS8zyG2uIrlWiPBmOQgP06ijLu8jt0jtIJN8gwFbd5WdgccduQRXVKvTmS\nfgJp0s10LiK5tiJF84Y8gHPFMxJqk77lhuFjiliJkiO3K7SGOe4BxxU17g3hz+OgePHIZnJOCy4H\nA6g/+KQ3V1JezeDHbSs0nMaKucD6d6EnXhGTBR8zbTiFS8KucSIeDjucYolzbPMMqzwAY3tyf07U\n8W2BFes6lb+JapP4ipdQoUbdjAB2/ttoAaWsshSOXdkFkm4CMPr6g5FCTaQWiCW91HlrdI2jRtn4\nijk090bTY4BDJdW4ju1JcsMbQDjBB7GpxdlIIP1pIIrR7xNkjB/BYuTwwGB5e56frSHRI4odRQym\n4cuAXVgAFUc5PoDTONjUaqw0qO6ikmgkaMrJu3dByPQc1fb2+oW0QaZbciKTrgklfqOppo5gqVGd\nvTBDrEs5jhvGZwzrJKwMbHrjHFLNPSaC5uLk2UhcE+BK5Kqn69aLQ1XhqLOcXukJHqCxTXaSblIT\njnpjI9AKzA+Kri2juc3BjkM2xYgmBjHXpyeKdKw+elV6b3WLaOe3hBFqRuljjOG7HcBWi0V7W+sx\nptzcSPBIgKyqCpgk9PcUslWIKdoyev6DqeiagrbUeBJeJFP5vf7U+0nWzdXKSBIjPEQviAY3qB/i\nlmlKNBbweadFda1p73EexYYpickcYI55rLXNzZwapc310/jRx4t4kzhSTnnPbp+9JCHVkVrCLZri\nK0LSkwFyDGyN5cH3zTTSbi4WV7e/iSeIISk7EZUAetacdDJC97SK9cmxj3lmBUH8vuD9+9Io7i+h\n1GdomZZlYK4Xtnjjt1q8PAo+kfDUTac8XiyExSr+IjjO1yO1L9ajvLi4gtrctIscjPtcYzijZRCu\n4fVNIVGtvJJdA7khcEKvuO1Vm0M1skssbEk45GWU+uP70vgXpyCxTUNQge9ml2qdkcobO0++etd+\nIrd49OTzpKFuHjGCAfXmmQgjd2t7UMOZHYqoUce+TQ0Fybm+igjgZXkwFz0z35/zTTh2WE26NBNd\nXmjgW86YlZi6sF3ZyOzUbpMcd06NdeK0sg27T3PrXM4uIVQtaSS2vixbw5423IgfjHWhNclA1X5s\nBDMCCcKSSCMj271WMn9NJXiGFqWm01ILs+Gs2CISOePWmupXMth8PCFUiMUi7ljcZCqCATj6n9qW\nWvB4KlTFOVjtrdXJ8GW33SgDcrYc7Tz3yO1ATxWUbtd3e0qC+4xjpntj1qsU2aSQp+VjiYOhOxxu\nQ+1FxzO6xhJGDrwpHUd6s4/CKZGF4724C3JQylcB0wCRx1oS6srjTNbljAAjGQkmdwYUkLToX0Y6\nUHuZQkUbMwHCAZJNNtOknhkY2xIVRufuAO2fvVbSA0alLxVsoZMOlyUzIjjAH/ippqWRHHcbAsp2\nEFcgH9KdSy2K1oo1p9MhVTYhZbjftaPcQB2z+tZ68vLho4lR1QMvaUqQw9CKWTsdKhG8UDy75ra3\n3k8zC6Gf2pzp1xDFJ4dnM0gK4YK+7j+1BAlo8t7eO9iZINpJ8pGRQsLtZyPFJGfK+0CrJkmjl4Yr\nseZCo6ZzXqxhBNbGOVJp5Gnkmbc7N35rS6ppNnFqMjwRGNGcExlsjJHUenWvFnNpHS/CttNktBO0\n95NdwxwM6QynyhuMH/xQjfjLHJhFcxswIX8p9qHHPtFkn6M/gtZJrXULyaUyLaoW8Fhw7cEEn70r\n1iE3l+tzNI7SOgJ5wOnanlJxSoeWLBZJBgYSWVRj8u/irrKzzH4niEhAW2tyDg4xTJto0GetLjw7\n1J0UL59+0dOnSmV1M118Vzs4BWaFCRzx5AePvU3uFZIczWFq1wAIsFUZPzccjrj15qi0s/Aubd4n\n2OiCNio/OPf9BU+K1Fh88Gl9IqQISrCSNAVdHK9D3x1+lci1iG3lWFLIbrnG994GcD0AxVomS0Jm\n06KytYo18ymQMM8EA9sjtS25jhSUTRRlJIkcKS2R5TxxWiyyYwt44obyDwYljNwniMR16DAzWe1W\n+kAkjVY1Zk5k2gsaYHrLIdbvrrTYY7iYyR26AKuP+e1B3dtBdiW4eMeJC2M5/Nzjn16/tTLQNJFU\nepXl5LsuJ98KhQkIXCL9AKKihNxKAZGTYM+TjmnaQEw64WSSwtbp5pDcRSkByc5Azx9Of2rTfAMM\nTNdTNGviLtQFRgAY9PtU27jQX4Z/490KG0vp5oWKvJ+KeOORyMUl0r4eh+Irm3aWV4t3gbsDO7OQ\nf/8AUVycD1oabxGy1maNta2xQRwBAVXwwAc4ByfWgpdKtpdXhmEMaGSA79q43EYYHjuCP3r0H4c8\ncYRHBEL6K0EarHKpkkIHLH60TKTbWNyLdmRUYIoJ3AdfWpQQZPTPRmEukTQggDnBxmq5dSl0+9C2\napEzEZcjceaziiMvQu4dr5/mJnczxpsDk5znrkdKz965gvRagDase4EDHfofWjD0KWGgvba3uPhr\nTpxBEkkfiRg7FPGc9x7n9axHxBq8+mp8hHHCYW/EI2Y5P0PFO/aHSRfo138xFNLcxiQhd6bWK7CO\nPWtEw26HNNESrSIuQx3DpStUNEpuLeRrfV5vFBaFxJFuXOw8A4596GuCo0qytsNnUWBkk3HcB6A+\nlKno301b3jRabuhBRoHWHcG5ce+KJspQ9rLMykgk4XPTccdaaPpmtMxq6JY2zm1XZ4khDg85xn+9\nK7aFtUu7eG4lbEhK8HhftWk/yoRNpjD4gE+iW8kOn3DxCzyUJ8xIx0OaDa5GsWtnLfxRuzRhcxqE\nIPXPcZposo91k9LjuNDmaG1uAYZ3G5WTJIPbOaLs7JW+LZrSNjDHJGsg2AeQ7c8Z9xQkqdmihpNc\nvcWUEkwR/G2iQEcsDwRn+9KptAi+HPiKb5WQusi5VXXOzjFJ9CzZ/Hd0Ph74UtLSxhRIbuQROBxx\n1P3NfKruSKS8ESwKqF/KCd22qNEYFrW2yBlDDaDjG30pxFpKwW6TePI28b2XoD7USki9EaxktrmF\nzm4lCOrcjbzxWb2rNq4OCshlOWB4OOnFLdMxuY5wbURsHLbA+4Pjkfal8mqT3t3vmY7nlIJU4Ixg\ncfaih0BQ6Z85f3LRzNELSVT3YuNx4JzUtZlktplmgkdTLuOGOcYNazE1Y3GkSSNhZIAJwy8bj/b7\nUk1hXvdLMjysviy+KQMcMe+aYDBvHJiK45jGM55PvRHwxrT2c92s0EdyGjYgyDkHFXRzTQ7+GoJt\najg8W6kjCg7AACFB69aZfElkmhwhLZnLRxDa5POSck1zcqXobqkJZ7eO5iyyjdIDKGPJUgVeZlie\nzmEMZBgVmRhnJ5HXr2FTXhZIt0o2uoyyLJbssqxl/EEp5I56dvtTjWYEXVrV5AHj8ARmMcAg+b+9\nUSoLMxdmO7F9GsXhR2jsYgGOQAfy59KUoirfzxDd4Uyq2wnO04p0K9HUKW+m6BLeC3E0jt4IVz5Q\nD39c/es0YfAYTRu6luwPHWuhM5n6UfLLbyDwmYZOeecVc5Z9HW4LHdBJ4eM8MD60z0yZK31C4017\nW6tXCOGyQRkHJ2/0p/8AC07R6FqsEh8QIOWJwWydv9s1OUUUg9KrLUpNVMtxdKHaLYmD0I6ftim8\nGsXkmj380cixrbAbIwgKjB/vS/KHpPRB8R38l1bNqkQ8CV2yVXpQltPLNo0TtJlxMeWGRgjpimiq\nROT0Hvnt7ZAz2UErDA6bf6VNrQThZIpGiU//AI+CopgMvtl8GXykhumRxWls18a1LSEtvXkGjYrQ\nsvj4Ejqg8oXv9K9VEKf/2Q==\n", + "text/plain": [ + "\u003cIPython.core.display.Image object\u003e" + ] + }, + "metadata": { + "tags": [] + }, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO:tensorflow:Restoring parameters from mobilenet_v2_1.0_224.ckpt\n", + "Top 1 prediction: 389 giant panda, panda, panda bear, coon bear, Ailuropoda melanoleuca 0.90984344\n" + ] + } + ], + "source": [ + "from IPython import display\n", + "import pylab\n", + "from datasets import imagenet\n", + "import PIL\n", + "display.display(display.Image('panda.jpg'))\n", + "\n", + "with tf.Session() as sess:\n", + " saver.restore(sess, checkpoint)\n", + " x = endpoints['Predictions'].eval(feed_dict={file_input: 'panda.jpg'})\n", + "label_map = imagenet.create_readable_names_for_imagenet_labels() \n", + "print(\"Top 1 prediction: \", x.argmax(),label_map[x.argmax()], x.max())\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "PlwvpK3ElBk6" + }, + "source": [ + "# Frozen inference" + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": { + "autoexec": { + "startup": false, + "wait_interval": 0 + } + }, + "colab_type": "code", + "id": "o0BIbQUUlVrf" + }, + "outputs": [], + "source": [ + "import numpy as np\n", + "img = np.array(PIL.Image.open('panda.jpg').resize((224, 224))).astype(np.float) / 128 - 1\n", + "gd = tf.GraphDef.FromString(open(base_name + '_frozen.pb', 'rb').read())\n", + "inp, predictions = tf.import_graph_def(gd, return_elements = ['input:0', 'MobilenetV2/Predictions/Reshape_1:0'])" + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": { + "autoexec": { + "startup": false, + "wait_interval": 0 + }, + "base_uri": "https://localhost:8080/", + "height": 35, + "output_extras": [ + {} + ] + }, + "colab_type": "code", + "executionInfo": { + "elapsed": 1350, + "status": "ok", + "timestamp": 1521493472822, + "user": { + "displayName": "Mark Sandler", + "photoUrl": "//lh5.googleusercontent.com/-CjnV3zpGrlw/AAAAAAAAAAI/AAAAAAAABRU/dfjRy_tzX5M/s50-c-k-no/photo.jpg", + "userId": "108034853522252017283" + }, + "user_tz": 420 + }, + "id": "qSU2h5NRlN7V", + "outputId": "4fb09105-b729-45c3-b5ef-83c8da30a215" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Top 1 Prediction: 389 giant panda, panda, panda bear, coon bear, Ailuropoda melanoleuca 0.9220208\n" + ] + } + ], + "source": [ + "with tf.Session(graph=inp.graph):\n", + " x = predictions.eval(feed_dict={inp: img.reshape(1, 224,224, 3)})\n", + "\n", + "label_map = imagenet.create_readable_names_for_imagenet_labels() \n", + "print(\"Top 1 Prediction: \", x.argmax(),label_map[x.argmax()], x.max())" + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": { + "autoexec": { + "startup": false, + "wait_interval": 0 + } + }, + "colab_type": "code", + "id": "CU8dJF8kCo6X" + }, + "outputs": [], + "source": [ + "" + ] + } + ], + "metadata": { + "colab": { + "collapsed_sections": [ + "T_cETKXHDTXu" + ], + "default_view": {}, + "name": "Mobilenet Example.ipynb", + "provenance": [ + { + "file_id": "1ylt6hB0JlXmWU9Bm6O1zGKVPgc2csZf5", + "timestamp": 1521507068201 + } + ], + "version": "0.3.2", + "views": {} + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/nets/mobilenet/mobilenet_v2.py b/nets/mobilenet/mobilenet_v2.py new file mode 100644 index 0000000..d2d736a --- /dev/null +++ b/nets/mobilenet/mobilenet_v2.py @@ -0,0 +1,247 @@ +# Copyright 2018 The TensorFlow Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""Implementation of Mobilenet V2. + +Architecture: https://arxiv.org/abs/1801.04381 + +The base model gives 72.2% accuracy on ImageNet, with 300MMadds, +3.4 M parameters. +""" + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import copy +import functools + +import tensorflow as tf + +from nets.mobilenet import conv_blocks as ops +from nets.mobilenet import mobilenet as lib + +slim = tf.contrib.slim +op = lib.op + +expand_input = ops.expand_input_by_factor + +# pyformat: disable +# Architecture: https://arxiv.org/abs/1801.04381 +V2_DEF = dict( + defaults={ + # Note: these parameters of batch norm affect the architecture + # that's why they are here and not in training_scope. + (slim.batch_norm,): {'center': True, 'scale': True}, + (slim.conv2d, slim.fully_connected, slim.separable_conv2d): { + 'normalizer_fn': slim.batch_norm, 'activation_fn': tf.nn.relu6 + }, + (ops.expanded_conv,): { + 'expansion_size': expand_input(6), + 'split_expansion': 1, + 'normalizer_fn': slim.batch_norm, + 'residual': True + }, + (slim.conv2d, slim.separable_conv2d): {'padding': 'SAME'} + }, + spec=[ + op(slim.conv2d, stride=2, num_outputs=32, kernel_size=[3, 3]), + op(ops.expanded_conv, + expansion_size=expand_input(1, divisible_by=1), + num_outputs=16), + op(ops.expanded_conv, stride=2, num_outputs=24), + op(ops.expanded_conv, stride=1, num_outputs=24), + op(ops.expanded_conv, stride=2, num_outputs=32), + op(ops.expanded_conv, stride=1, num_outputs=32), + op(ops.expanded_conv, stride=1, num_outputs=32), + op(ops.expanded_conv, stride=2, num_outputs=64), + op(ops.expanded_conv, stride=1, num_outputs=64), + op(ops.expanded_conv, stride=1, num_outputs=64), + op(ops.expanded_conv, stride=1, num_outputs=64), + op(ops.expanded_conv, stride=1, num_outputs=96), + op(ops.expanded_conv, stride=1, num_outputs=96), + op(ops.expanded_conv, stride=1, num_outputs=96), + op(ops.expanded_conv, stride=2, num_outputs=160), + op(ops.expanded_conv, stride=1, num_outputs=160), + op(ops.expanded_conv, stride=1, num_outputs=160), + op(ops.expanded_conv, stride=1, num_outputs=320), + op(slim.conv2d, stride=1, kernel_size=[1, 1], num_outputs=1280) + ], +) +# pyformat: enable + +# Mobilenet v2 Definition with group normalization. +V2_DEF_GROUP_NORM = copy.deepcopy(V2_DEF) +V2_DEF_GROUP_NORM['defaults'] = { + (tf.contrib.slim.conv2d, tf.contrib.slim.fully_connected, + tf.contrib.slim.separable_conv2d): { + 'normalizer_fn': tf.contrib.layers.group_norm, # pylint: disable=C0330 + 'activation_fn': tf.nn.relu6, # pylint: disable=C0330 + }, # pylint: disable=C0330 + (ops.expanded_conv,): { + 'expansion_size': ops.expand_input_by_factor(6), + 'split_expansion': 1, + 'normalizer_fn': tf.contrib.layers.group_norm, + 'residual': True + }, + (tf.contrib.slim.conv2d, tf.contrib.slim.separable_conv2d): { + 'padding': 'SAME' + } +} + + +@slim.add_arg_scope +def mobilenet(input_tensor, + num_classes=1001, + depth_multiplier=1.0, + scope='MobilenetV2', + conv_defs=None, + finegrain_classification_mode=False, + min_depth=None, + divisible_by=None, + activation_fn=None, + **kwargs): + """Creates mobilenet V2 network. + + Inference mode is created by default. To create training use training_scope + below. + + with tf.contrib.slim.arg_scope(mobilenet_v2.training_scope()): + logits, endpoints = mobilenet_v2.mobilenet(input_tensor) + + Args: + input_tensor: The input tensor + num_classes: number of classes + depth_multiplier: The multiplier applied to scale number of + channels in each layer. + scope: Scope of the operator + conv_defs: Allows to override default conv def. + finegrain_classification_mode: When set to True, the model + will keep the last layer large even for small multipliers. Following + https://arxiv.org/abs/1801.04381 + suggests that it improves performance for ImageNet-type of problems. + *Note* ignored if final_endpoint makes the builder exit earlier. + min_depth: If provided, will ensure that all layers will have that + many channels after application of depth multiplier. + divisible_by: If provided will ensure that all layers # channels + will be divisible by this number. + activation_fn: Activation function to use, defaults to tf.nn.relu6 if not + specified. + **kwargs: passed directly to mobilenet.mobilenet: + prediction_fn- what prediction function to use. + reuse-: whether to reuse variables (if reuse set to true, scope + must be given). + Returns: + logits/endpoints pair + + Raises: + ValueError: On invalid arguments + """ + if conv_defs is None: + conv_defs = V2_DEF + if 'multiplier' in kwargs: + raise ValueError('mobilenetv2 doesn\'t support generic ' + 'multiplier parameter use "depth_multiplier" instead.') + if finegrain_classification_mode: + conv_defs = copy.deepcopy(conv_defs) + if depth_multiplier < 1: + conv_defs['spec'][-1].params['num_outputs'] /= depth_multiplier + if activation_fn: + conv_defs = copy.deepcopy(conv_defs) + defaults = conv_defs['defaults'] + conv_defaults = ( + defaults[(slim.conv2d, slim.fully_connected, slim.separable_conv2d)]) + conv_defaults['activation_fn'] = activation_fn + + depth_args = {} + # NB: do not set depth_args unless they are provided to avoid overriding + # whatever default depth_multiplier might have thanks to arg_scope. + if min_depth is not None: + depth_args['min_depth'] = min_depth + if divisible_by is not None: + depth_args['divisible_by'] = divisible_by + + with slim.arg_scope((lib.depth_multiplier,), **depth_args): + return lib.mobilenet( + input_tensor, + num_classes=num_classes, + conv_defs=conv_defs, + scope=scope, + multiplier=depth_multiplier, + **kwargs) + +mobilenet.default_image_size = 224 + + +def wrapped_partial(func, *args, **kwargs): + partial_func = functools.partial(func, *args, **kwargs) + functools.update_wrapper(partial_func, func) + return partial_func + + +# Wrappers for mobilenet v2 with depth-multipliers. Be noticed that +# 'finegrain_classification_mode' is set to True, which means the embedding +# layer will not be shrinked when given a depth-multiplier < 1.0. +mobilenet_v2_140 = wrapped_partial(mobilenet, depth_multiplier=1.4) +mobilenet_v2_050 = wrapped_partial(mobilenet, depth_multiplier=0.50, + finegrain_classification_mode=True) +mobilenet_v2_035 = wrapped_partial(mobilenet, depth_multiplier=0.35, + finegrain_classification_mode=True) + + +@slim.add_arg_scope +def mobilenet_base(input_tensor, depth_multiplier=1.0, **kwargs): + """Creates base of the mobilenet (no pooling and no logits) .""" + return mobilenet(input_tensor, + depth_multiplier=depth_multiplier, + base_only=True, **kwargs) + + +@slim.add_arg_scope +def mobilenet_base_group_norm(input_tensor, depth_multiplier=1.0, **kwargs): + """Creates base of the mobilenet (no pooling and no logits) .""" + kwargs['conv_defs'] = V2_DEF_GROUP_NORM + kwargs['conv_defs']['defaults'].update({ + (tf.contrib.layers.group_norm,): { + 'groups': kwargs.pop('groups', 8) + } + }) + return mobilenet( + input_tensor, depth_multiplier=depth_multiplier, base_only=True, **kwargs) + + +def training_scope(**kwargs): + """Defines MobilenetV2 training scope. + + Usage: + with tf.contrib.slim.arg_scope(mobilenet_v2.training_scope()): + logits, endpoints = mobilenet_v2.mobilenet(input_tensor) + + with slim. + + Args: + **kwargs: Passed to mobilenet.training_scope. The following parameters + are supported: + weight_decay- The weight decay to use for regularizing the model. + stddev- Standard deviation for initialization, if negative uses xavier. + dropout_keep_prob- dropout keep probability + bn_decay- decay for the batch norm moving averages. + + Returns: + An `arg_scope` to use for the mobilenet v2 model. + """ + return lib.training_scope(**kwargs) + + +__all__ = ['training_scope', 'mobilenet_base', 'mobilenet', 'V2_DEF'] diff --git a/nets/mobilenet/mobilenet_v2_test.py b/nets/mobilenet/mobilenet_v2_test.py new file mode 100644 index 0000000..c31d773 --- /dev/null +++ b/nets/mobilenet/mobilenet_v2_test.py @@ -0,0 +1,210 @@ +# Copyright 2018 The TensorFlow Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""Tests for mobilenet_v2.""" + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +import copy +import tensorflow as tf +from nets.mobilenet import conv_blocks as ops +from nets.mobilenet import mobilenet +from nets.mobilenet import mobilenet_v2 + + +slim = tf.contrib.slim + + +def find_ops(optype): + """Find ops of a given type in graphdef or a graph. + + Args: + optype: operation type (e.g. Conv2D) + Returns: + List of operations. + """ + gd = tf.get_default_graph() + return [var for var in gd.get_operations() if var.type == optype] + + +class MobilenetV2Test(tf.test.TestCase): + + def setUp(self): + tf.reset_default_graph() + + def testCreation(self): + spec = dict(mobilenet_v2.V2_DEF) + _, ep = mobilenet.mobilenet( + tf.placeholder(tf.float32, (10, 224, 224, 16)), conv_defs=spec) + num_convs = len(find_ops('Conv2D')) + + # This is mostly a sanity test. No deep reason for these particular + # constants. + # + # All but first 2 and last one have two convolutions, and there is one + # extra conv that is not in the spec. (logits) + self.assertEqual(num_convs, len(spec['spec']) * 2 - 2) + # Check that depthwise are exposed. + for i in range(2, 17): + self.assertIn('layer_%d/depthwise_output' % i, ep) + + def testCreationNoClasses(self): + spec = copy.deepcopy(mobilenet_v2.V2_DEF) + net, ep = mobilenet.mobilenet( + tf.placeholder(tf.float32, (10, 224, 224, 16)), conv_defs=spec, + num_classes=None) + self.assertIs(net, ep['global_pool']) + + def testImageSizes(self): + for input_size, output_size in [(224, 7), (192, 6), (160, 5), + (128, 4), (96, 3)]: + tf.reset_default_graph() + _, ep = mobilenet_v2.mobilenet( + tf.placeholder(tf.float32, (10, input_size, input_size, 3))) + + self.assertEqual(ep['layer_18/output'].get_shape().as_list()[1:3], + [output_size] * 2) + + def testWithSplits(self): + spec = copy.deepcopy(mobilenet_v2.V2_DEF) + spec['overrides'] = { + (ops.expanded_conv,): dict(split_expansion=2), + } + _, _ = mobilenet.mobilenet( + tf.placeholder(tf.float32, (10, 224, 224, 16)), conv_defs=spec) + num_convs = len(find_ops('Conv2D')) + # All but 3 op has 3 conv operatore, the remainign 3 have one + # and there is one unaccounted. + self.assertEqual(num_convs, len(spec['spec']) * 3 - 5) + + def testWithOutputStride8(self): + out, _ = mobilenet.mobilenet_base( + tf.placeholder(tf.float32, (10, 224, 224, 16)), + conv_defs=mobilenet_v2.V2_DEF, + output_stride=8, + scope='MobilenetV2') + self.assertEqual(out.get_shape().as_list()[1:3], [28, 28]) + + def testDivisibleBy(self): + tf.reset_default_graph() + mobilenet_v2.mobilenet( + tf.placeholder(tf.float32, (10, 224, 224, 16)), + conv_defs=mobilenet_v2.V2_DEF, + divisible_by=16, + min_depth=32) + s = [op.outputs[0].get_shape().as_list()[-1] for op in find_ops('Conv2D')] + s = set(s) + self.assertSameElements([32, 64, 96, 160, 192, 320, 384, 576, 960, 1280, + 1001], s) + + def testDivisibleByWithArgScope(self): + tf.reset_default_graph() + # Verifies that depth_multiplier arg scope actually works + # if no default min_depth is provided. + with slim.arg_scope((mobilenet.depth_multiplier,), min_depth=32): + mobilenet_v2.mobilenet( + tf.placeholder(tf.float32, (10, 224, 224, 2)), + conv_defs=mobilenet_v2.V2_DEF, depth_multiplier=0.1) + s = [op.outputs[0].get_shape().as_list()[-1] for op in find_ops('Conv2D')] + s = set(s) + self.assertSameElements(s, [32, 192, 128, 1001]) + + def testFineGrained(self): + tf.reset_default_graph() + # Verifies that depth_multiplier arg scope actually works + # if no default min_depth is provided. + + mobilenet_v2.mobilenet( + tf.placeholder(tf.float32, (10, 224, 224, 2)), + conv_defs=mobilenet_v2.V2_DEF, depth_multiplier=0.01, + finegrain_classification_mode=True) + s = [op.outputs[0].get_shape().as_list()[-1] for op in find_ops('Conv2D')] + s = set(s) + # All convolutions will be 8->48, except for the last one. + self.assertSameElements(s, [8, 48, 1001, 1280]) + + def testMobilenetBase(self): + tf.reset_default_graph() + # Verifies that mobilenet_base returns pre-pooling layer. + with slim.arg_scope((mobilenet.depth_multiplier,), min_depth=32): + net, _ = mobilenet_v2.mobilenet_base( + tf.placeholder(tf.float32, (10, 224, 224, 16)), + conv_defs=mobilenet_v2.V2_DEF, depth_multiplier=0.1) + self.assertEqual(net.get_shape().as_list(), [10, 7, 7, 128]) + + def testWithOutputStride16(self): + tf.reset_default_graph() + out, _ = mobilenet.mobilenet_base( + tf.placeholder(tf.float32, (10, 224, 224, 16)), + conv_defs=mobilenet_v2.V2_DEF, + output_stride=16) + self.assertEqual(out.get_shape().as_list()[1:3], [14, 14]) + + def testMultiplier(self): + op = mobilenet.op + new_def = copy.deepcopy(mobilenet_v2.V2_DEF) + + def inverse_multiplier(output_params, multiplier): + output_params['num_outputs'] = int( + output_params['num_outputs'] / multiplier) + + new_def['spec'][0] = op( + slim.conv2d, + kernel_size=(3, 3), + multiplier_func=inverse_multiplier, + num_outputs=16) + _ = mobilenet_v2.mobilenet_base( + tf.placeholder(tf.float32, (10, 224, 224, 16)), + conv_defs=new_def, depth_multiplier=0.1) + s = [op.outputs[0].get_shape().as_list()[-1] for op in find_ops('Conv2D')] + # Expect first layer to be 160 (16 / 0.1), and other layers + # their max(original size * 0.1, 8) + self.assertEqual([160, 8, 48, 8, 48], s[:5]) + + def testWithOutputStride8AndExplicitPadding(self): + tf.reset_default_graph() + out, _ = mobilenet.mobilenet_base( + tf.placeholder(tf.float32, (10, 224, 224, 16)), + conv_defs=mobilenet_v2.V2_DEF, + output_stride=8, + use_explicit_padding=True, + scope='MobilenetV2') + self.assertEqual(out.get_shape().as_list()[1:3], [28, 28]) + + def testWithOutputStride16AndExplicitPadding(self): + tf.reset_default_graph() + out, _ = mobilenet.mobilenet_base( + tf.placeholder(tf.float32, (10, 224, 224, 16)), + conv_defs=mobilenet_v2.V2_DEF, + output_stride=16, + use_explicit_padding=True) + self.assertEqual(out.get_shape().as_list()[1:3], [14, 14]) + + def testBatchNormScopeDoesNotHaveIsTrainingWhenItsSetToNone(self): + sc = mobilenet.training_scope(is_training=None) + self.assertNotIn('is_training', sc[slim.arg_scope_func_key( + slim.batch_norm)]) + + def testBatchNormScopeDoesHasIsTrainingWhenItsNotNone(self): + sc = mobilenet.training_scope(is_training=False) + self.assertIn('is_training', sc[slim.arg_scope_func_key(slim.batch_norm)]) + sc = mobilenet.training_scope(is_training=True) + self.assertIn('is_training', sc[slim.arg_scope_func_key(slim.batch_norm)]) + sc = mobilenet.training_scope() + self.assertIn('is_training', sc[slim.arg_scope_func_key(slim.batch_norm)]) + + +if __name__ == '__main__': + tf.test.main() diff --git a/nets/mobilenet/mobilenet_v3.py b/nets/mobilenet/mobilenet_v3.py new file mode 100644 index 0000000..3ea393c --- /dev/null +++ b/nets/mobilenet/mobilenet_v3.py @@ -0,0 +1,325 @@ +# Copyright 2019 The TensorFlow Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""Mobilenet V3 conv defs and helper functions.""" + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import copy +import functools +import numpy as np + +import tensorflow as tf + +from nets.mobilenet import conv_blocks as ops +from nets.mobilenet import mobilenet as lib + +slim = tf.contrib.slim +op = lib.op +expand_input = ops.expand_input_by_factor + +# Squeeze Excite with all parameters filled-in, we use hard-sigmoid +# for gating function and relu for inner activation function. +squeeze_excite = functools.partial( + ops.squeeze_excite, squeeze_factor=4, + inner_activation_fn=tf.nn.relu, + gating_fn=lambda x: tf.nn.relu6(x+3)*0.16667) + +# Wrap squeeze excite op as expansion_transform that takes +# both expansion and input tensor. +_se4 = lambda expansion_tensor, input_tensor: squeeze_excite(expansion_tensor) + + +def hard_swish(x): + with tf.name_scope('hard_swish'): + return x * tf.nn.relu6(x + np.float32(3)) * np.float32(1. / 6.) + + +def reduce_to_1x1(input_tensor, default_size=7, **kwargs): + h, w = input_tensor.shape.as_list()[1:3] + if h is not None and w == h: + k = [h, h] + else: + k = [default_size, default_size] + return slim.avg_pool2d(input_tensor, kernel_size=k, **kwargs) + + +def mbv3_op(ef, n, k, s=1, act=tf.nn.relu, se=None): + """Defines a single Mobilenet V3 convolution block. + + Args: + ef: expansion factor + n: number of output channels + k: stride of depthwise + s: stride + act: activation function in inner layers + se: squeeze excite function. + + Returns: + An object (lib._Op) for inserting in conv_def, representing this operation. + """ + return op(ops.expanded_conv, expansion_size=expand_input(ef), + kernel_size=(k, k), stride=s, num_outputs=n, + inner_activation_fn=act, + expansion_transform=se) + + +mbv3_op_se = functools.partial(mbv3_op, se=_se4) + + +DEFAULTS = { + (ops.expanded_conv,): + dict( + normalizer_fn=slim.batch_norm, + residual=True), + (slim.conv2d, slim.fully_connected, slim.separable_conv2d): { + 'normalizer_fn': slim.batch_norm, + 'activation_fn': tf.nn.relu, + }, + (slim.batch_norm,): { + 'center': True, + 'scale': True + }, +} + +# Compatible checkpoint: http://mldash/5511169891790690458#scalars +V3_LARGE = dict( + defaults=dict(DEFAULTS), + spec=([ + # stage 1 + op(slim.conv2d, stride=2, num_outputs=16, kernel_size=(3, 3), + activation_fn=hard_swish), + mbv3_op(ef=1, n=16, k=3), + mbv3_op(ef=4, n=24, k=3, s=2), + mbv3_op(ef=3, n=24, k=3, s=1), + mbv3_op_se(ef=3, n=40, k=5, s=2), + mbv3_op_se(ef=3, n=40, k=5, s=1), + mbv3_op_se(ef=3, n=40, k=5, s=1), + mbv3_op(ef=6, n=80, k=3, s=2, act=hard_swish), + mbv3_op(ef=2.5, n=80, k=3, s=1, act=hard_swish), + mbv3_op(ef=184/80., n=80, k=3, s=1, act=hard_swish), + mbv3_op(ef=184/80., n=80, k=3, s=1, act=hard_swish), + mbv3_op_se(ef=6, n=112, k=3, s=1, act=hard_swish), + mbv3_op_se(ef=6, n=112, k=3, s=1, act=hard_swish), + mbv3_op_se(ef=6, n=160, k=5, s=2, act=hard_swish), + mbv3_op_se(ef=6, n=160, k=5, s=1, act=hard_swish), + mbv3_op_se(ef=6, n=160, k=5, s=1, act=hard_swish), + op(slim.conv2d, stride=1, kernel_size=[1, 1], num_outputs=960, + activation_fn=hard_swish), + op(reduce_to_1x1, default_size=7, stride=1, padding='VALID'), + op(slim.conv2d, stride=1, kernel_size=[1, 1], num_outputs=1280, + normalizer_fn=None, activation_fn=hard_swish) + ])) + +# 72.2% accuracy. +V3_LARGE_MINIMALISTIC = dict( + defaults=dict(DEFAULTS), + spec=([ + # stage 1 + op(slim.conv2d, stride=2, num_outputs=16, kernel_size=(3, 3)), + mbv3_op(ef=1, n=16, k=3), + mbv3_op(ef=4, n=24, k=3, s=2), + mbv3_op(ef=3, n=24, k=3, s=1), + mbv3_op(ef=3, n=40, k=3, s=2), + mbv3_op(ef=3, n=40, k=3, s=1), + mbv3_op(ef=3, n=40, k=3, s=1), + mbv3_op(ef=6, n=80, k=3, s=2), + mbv3_op(ef=2.5, n=80, k=3, s=1), + mbv3_op(ef=184 / 80., n=80, k=3, s=1), + mbv3_op(ef=184 / 80., n=80, k=3, s=1), + mbv3_op(ef=6, n=112, k=3, s=1), + mbv3_op(ef=6, n=112, k=3, s=1), + mbv3_op(ef=6, n=160, k=3, s=2), + mbv3_op(ef=6, n=160, k=3, s=1), + mbv3_op(ef=6, n=160, k=3, s=1), + op(slim.conv2d, stride=1, kernel_size=[1, 1], num_outputs=960), + op(reduce_to_1x1, default_size=7, stride=1, padding='VALID'), + op(slim.conv2d, + stride=1, + kernel_size=[1, 1], + num_outputs=1280, + normalizer_fn=None) + ])) + +# Compatible run: http://mldash/2023283040014348118#scalars +V3_SMALL = dict( + defaults=dict(DEFAULTS), + spec=([ + # stage 1 + op(slim.conv2d, stride=2, num_outputs=16, kernel_size=(3, 3), + activation_fn=hard_swish), + mbv3_op_se(ef=1, n=16, k=3, s=2), + mbv3_op(ef=72./16, n=24, k=3, s=2), + mbv3_op(ef=(88./24), n=24, k=3, s=1), + mbv3_op_se(ef=4, n=40, k=5, s=2, act=hard_swish), + mbv3_op_se(ef=6, n=40, k=5, s=1, act=hard_swish), + mbv3_op_se(ef=6, n=40, k=5, s=1, act=hard_swish), + mbv3_op_se(ef=3, n=48, k=5, s=1, act=hard_swish), + mbv3_op_se(ef=3, n=48, k=5, s=1, act=hard_swish), + mbv3_op_se(ef=6, n=96, k=5, s=2, act=hard_swish), + mbv3_op_se(ef=6, n=96, k=5, s=1, act=hard_swish), + mbv3_op_se(ef=6, n=96, k=5, s=1, act=hard_swish), + op(slim.conv2d, stride=1, kernel_size=[1, 1], num_outputs=576, + activation_fn=hard_swish), + op(reduce_to_1x1, default_size=7, stride=1, padding='VALID'), + op(slim.conv2d, stride=1, kernel_size=[1, 1], num_outputs=1024, + normalizer_fn=None, activation_fn=hard_swish) + ])) + +# 62% accuracy. +V3_SMALL_MINIMALISTIC = dict( + defaults=dict(DEFAULTS), + spec=([ + # stage 1 + op(slim.conv2d, stride=2, num_outputs=16, kernel_size=(3, 3)), + mbv3_op(ef=1, n=16, k=3, s=2), + mbv3_op(ef=72. / 16, n=24, k=3, s=2), + mbv3_op(ef=(88. / 24), n=24, k=3, s=1), + mbv3_op(ef=4, n=40, k=3, s=2), + mbv3_op(ef=6, n=40, k=3, s=1), + mbv3_op(ef=6, n=40, k=3, s=1), + mbv3_op(ef=3, n=48, k=3, s=1), + mbv3_op(ef=3, n=48, k=3, s=1), + mbv3_op(ef=6, n=96, k=3, s=2), + mbv3_op(ef=6, n=96, k=3, s=1), + mbv3_op(ef=6, n=96, k=3, s=1), + op(slim.conv2d, stride=1, kernel_size=[1, 1], num_outputs=576), + op(reduce_to_1x1, default_size=7, stride=1, padding='VALID'), + op(slim.conv2d, + stride=1, + kernel_size=[1, 1], + num_outputs=1024, + normalizer_fn=None) + ])) + + +@slim.add_arg_scope +def mobilenet(input_tensor, + num_classes=1001, + depth_multiplier=1.0, + scope='MobilenetV3', + conv_defs=None, + finegrain_classification_mode=False, + **kwargs): + """Creates mobilenet V3 network. + + Inference mode is created by default. To create training use training_scope + below. + + with tf.contrib.slim.arg_scope(mobilenet_v3.training_scope()): + logits, endpoints = mobilenet_v3.mobilenet(input_tensor) + + Args: + input_tensor: The input tensor + num_classes: number of classes + depth_multiplier: The multiplier applied to scale number of + channels in each layer. + scope: Scope of the operator + conv_defs: Which version to create. Could be large/small or + any conv_def (see mobilenet_v3.py for examples). + finegrain_classification_mode: When set to True, the model + will keep the last layer large even for small multipliers. Following + https://arxiv.org/abs/1801.04381 + it improves performance for ImageNet-type of problems. + *Note* ignored if final_endpoint makes the builder exit earlier. + **kwargs: passed directly to mobilenet.mobilenet: + prediction_fn- what prediction function to use. + reuse-: whether to reuse variables (if reuse set to true, scope + must be given). + Returns: + logits/endpoints pair + + Raises: + ValueError: On invalid arguments + """ + if conv_defs is None: + conv_defs = V3_LARGE + if 'multiplier' in kwargs: + raise ValueError('mobilenetv2 doesn\'t support generic ' + 'multiplier parameter use "depth_multiplier" instead.') + if finegrain_classification_mode: + conv_defs = copy.deepcopy(conv_defs) + conv_defs['spec'][-1] = conv_defs['spec'][-1]._replace( + multiplier_func=lambda params, multiplier: params) + depth_args = {} + with slim.arg_scope((lib.depth_multiplier,), **depth_args): + return lib.mobilenet( + input_tensor, + num_classes=num_classes, + conv_defs=conv_defs, + scope=scope, + multiplier=depth_multiplier, + **kwargs) + +mobilenet.default_image_size = 224 +training_scope = lib.training_scope + + +@slim.add_arg_scope +def mobilenet_base(input_tensor, depth_multiplier=1.0, **kwargs): + """Creates base of the mobilenet (no pooling and no logits) .""" + return mobilenet( + input_tensor, depth_multiplier=depth_multiplier, base_only=True, **kwargs) + + +def wrapped_partial(func, *args, **kwargs): + partial_func = functools.partial(func, *args, **kwargs) + functools.update_wrapper(partial_func, func) + return partial_func + + +large = wrapped_partial(mobilenet, conv_defs=V3_LARGE) +small = wrapped_partial(mobilenet, conv_defs=V3_SMALL) + + +# Minimalistic model that does not have Squeeze Excite blocks, +# Hardswish, or 5x5 depthwise convolution. +# This makes the model very friendly for a wide range of hardware +large_minimalistic = wrapped_partial(mobilenet, conv_defs=V3_LARGE_MINIMALISTIC) +small_minimalistic = wrapped_partial(mobilenet, conv_defs=V3_SMALL_MINIMALISTIC) + + +def _reduce_consecutive_layers(conv_defs, start_id, end_id, multiplier=0.5): + """Reduce the outputs of consecutive layers with multiplier. + + Args: + conv_defs: Mobilenet conv_defs. + start_id: 0-based index of the starting conv_def to be reduced. + end_id: 0-based index of the last conv_def to be reduced. + multiplier: The multiplier by which to reduce the conv_defs. + + Returns: + Mobilenet conv_defs where the output sizes from layers [start_id, end_id], + inclusive, are reduced by multiplier. + + Raises: + ValueError if any layer to be reduced does not have the 'num_outputs' + attribute. + """ + defs = copy.deepcopy(conv_defs) + for d in defs['spec'][start_id:end_id+1]: + d.params.update({ + 'num_outputs': np.int(np.round(d.params['num_outputs'] * multiplier)) + }) + return defs + + +V3_LARGE_DETECTION = _reduce_consecutive_layers(V3_LARGE, 13, 16) +V3_SMALL_DETECTION = _reduce_consecutive_layers(V3_SMALL, 9, 12) + + +__all__ = ['training_scope', 'mobilenet', 'V3_LARGE', 'V3_SMALL', 'large', + 'small', 'V3_LARGE_DETECTION', 'V3_SMALL_DETECTION'] diff --git a/nets/mobilenet/mobilenet_v3_test.py b/nets/mobilenet/mobilenet_v3_test.py new file mode 100644 index 0000000..86c3cf5 --- /dev/null +++ b/nets/mobilenet/mobilenet_v3_test.py @@ -0,0 +1,57 @@ +# Copyright 2019 The TensorFlow Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""Tests for google3.third_party.tensorflow_models.slim.nets.mobilenet.mobilenet_v3.""" + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +from absl.testing import absltest +import tensorflow as tf + +from nets.mobilenet import mobilenet_v3 + + +class MobilenetV3Test(absltest.TestCase): + + def setUp(self): + super(MobilenetV3Test, self).setUp() + tf.reset_default_graph() + + def testMobilenetV3Large(self): + logits, endpoints = mobilenet_v3.mobilenet( + tf.placeholder(tf.float32, (1, 224, 224, 3))) + self.assertEqual(endpoints['layer_19'].shape, [1, 1, 1, 1280]) + self.assertEqual(logits.shape, [1, 1001]) + + def testMobilenetV3Small(self): + _, endpoints = mobilenet_v3.mobilenet( + tf.placeholder(tf.float32, (1, 224, 224, 3)), + conv_defs=mobilenet_v3.V3_SMALL) + self.assertEqual(endpoints['layer_15'].shape, [1, 1, 1, 1024]) + + def testMobilenetV3BaseOnly(self): + result, endpoints = mobilenet_v3.mobilenet( + tf.placeholder(tf.float32, (1, 224, 224, 3)), + conv_defs=mobilenet_v3.V3_LARGE, + base_only=True, + final_endpoint='layer_17') + # Get the latest layer before average pool. + self.assertEqual(endpoints['layer_17'].shape, [1, 7, 7, 960]) + self.assertEqual(result, endpoints['layer_17']) + + +if __name__ == '__main__': + absltest.main() diff --git a/nets/pixel_link_symbol.py b/nets/pixel_link_symbol.py index 95be750..6dc8da4 100644 --- a/nets/pixel_link_symbol.py +++ b/nets/pixel_link_symbol.py @@ -1,8 +1,13 @@ import tensorflow as tf + +from .mobilenet import mobilenet_v2 + + slim = tf.contrib.slim MODEL_TYPE_vgg16 = 'vgg16' MODEL_TYPE_vgg16_no_dilation = 'vgg16_no_dilation' +MODEL_TYPE_mobilenetv2 = 'mobilenetv2' FUSE_TYPE_cascade_conv1x1_upsample_sum = 'cascade_conv1x1_upsample_sum' FUSE_TYPE_cascade_conv1x1_128_upsamle_sum_conv1x1_2 = \ @@ -10,6 +15,7 @@ FUSE_TYPE_cascade_conv1x1_128_upsamle_concat_conv1x1_2 = \ 'cascade_conv1x1_128_upsamle_concat_conv1x1_2' + class PixelLinkNet(object): def __init__(self, inputs, is_training): self.inputs = inputs @@ -20,7 +26,26 @@ def __init__(self, inputs, is_training): def _build_network(self): import config - if config.model_type == MODEL_TYPE_vgg16: + if config.model_type == MODEL_TYPE_mobilenetv2: + # keep this arg_scope code section such that self.arg_scope is set to + # the correct arg_scope, as it will be used later in the fusion layer + with slim.arg_scope([slim.conv2d], + activation_fn=tf.nn.relu, + weights_regularizer=slim.l2_regularizer(config.weight_decay), + weights_initializer= tf.contrib.layers.xavier_initializer(), + biases_initializer = tf.zeros_initializer()): + with slim.arg_scope([slim.conv2d, slim.max_pool2d], + padding='SAME') as sc: + self.arg_scope = sc + + with slim.arg_scope([slim.conv2d], + weights_regularizer=slim.l2_regularizer(config.weight_decay), + weights_initializer= tf.contrib.layers.xavier_initializer(), + biases_initializer = tf.zeros_initializer()): + # set num_classes to 0 will remove the last logit layer + self.net, self.end_points = mobilenet_v2.mobilenet(self.inputs, num_classes=0) + + elif config.model_type == MODEL_TYPE_vgg16: from nets import vgg with slim.arg_scope([slim.conv2d], activation_fn=tf.nn.relu, @@ -194,7 +219,7 @@ def _logits_to_scores(self): def build_loss(self, pixel_cls_labels, pixel_cls_weights, pixel_link_labels, pixel_link_weights, do_summary = True - ): + ): """ The loss consists of two parts: pixel_cls_loss + link_cls_loss, and link_cls_loss is calculated only on positive pixels @@ -231,7 +256,7 @@ def no_pos(): n_neg = tf.cast(n_neg, tf.int32) def has_neg(): neg_conf = tf.boolean_mask(scores, neg_mask) - vals, _ = tf.nn.top_k(-neg_conf, k=n_neg) + vals, _ = tf.nn.top_k(-neg_conf, k=n_neg) # victorx: hard negative means smaller neg_conf value, hence the - sign threshold = vals[-1]# a negtive value selected_neg_mask = tf.logical_and(neg_mask, scores <= -threshold) return selected_neg_mask diff --git a/pixel_link.py b/pixel_link.py index 367196e..37cac1e 100644 --- a/pixel_link.py +++ b/pixel_link.py @@ -133,13 +133,14 @@ def cal_gt_for_single_image(normed_xs, normed_ys, labels): bbox_points = zip(bbox_xs, bbox_ys) bbox_contours = util.img.points_to_contours(bbox_points) + # border_width = -1 means fill the interior util.img.draw_contours(bbox_mask, bbox_contours, idx = -1, color = 1, border_width = -1) bbox_masks.append(bbox_mask) if labels[bbox_idx] == text_label: - pos_mask += bbox_mask + pos_mask += bbox_mask # basically pos_mask contains all individual bounding boxes # treat overlapped in-bbox pixels as negative, # and non-overlapped ones as positive @@ -149,6 +150,7 @@ def cal_gt_for_single_image(normed_xs, normed_ys, labels): ## add all bbox_maskes, find non-overlapping pixels sum_mask = np.sum(bbox_masks, axis = 0) not_overlapped_mask = sum_mask == 1 + #TBD: isn't not_overlapped_mask the same as pos_mask? ## gt and weight calculation @@ -359,7 +361,8 @@ def mask_to_bboxes(mask, image_shape = None, min_area = None, continue cnt = cnts[0] rect, rect_area = min_area_rect(cnt) - + + # rect is [cx, cy, w, h, theta] w, h = rect[2:-1] if min(w, h) < min_height: continue diff --git a/scripts/eval_fscore.py b/scripts/eval_fscore.py new file mode 100644 index 0000000..7f3b855 --- /dev/null +++ b/scripts/eval_fscore.py @@ -0,0 +1,177 @@ +""" +This script can be used in two mode +1) one-off mode +You specify the exact checkpoint to evaluate, the script will print the result and quit. + +2) watching mode +You specify the checkpoint base dir, the script will poll the folder for new checkpoint. When +a new checkpoint is generated, the script will evaluate performance with this checkpoint and + append the result into fscore.csv in the checkpoint base dir +""" +import argparse +import glob +import re +import subprocess +import time +import os + +# this script itself only use tensorflow for creating tf.summary +# so no GPU is required. But we will require a GPU, +# which is to be specified in the cuda_visible_devices command option, to do the inference later. + +os.environ['CUDA_VISIBLE_DEVICES'] = '' +import tensorflow as tf +tf.enable_eager_execution() + +EVALUATION_INTERVAL_IN_MINUTES = 20 + + +def my_exec(cmd): + print(cmd) + os.system(cmd) + + +def exec_and_get_stdout(eval_fscore_cmd): + print(eval_fscore_cmd) + res = subprocess.check_output(eval_fscore_cmd, stderr=subprocess.STDOUT, shell=True, universal_newlines=True) + print(res) + return res + + +def extract_value(stdout_txt, metric_name): + pattern = r'.*"%s": ((0\.)?\d*),.*' % metric_name + return float(re.match(pattern, stdout_txt).group(1)) + + +def eval_with_checkpoint(checkpoint_base_name, args): + """ + + :param checkpoint_base_name: + :param global_step: + :param args: + :return: (recall, precision, fscore) + """ + checkpoint_path = os.path.join(args.checkpoint_folder, checkpoint_base_name) + + inference_cmd = '''CUDA_VISIBLE_DEVICES=%s PYTHONPATH=$PYTHONPATH:./pylib/src python test_pixel_link.py \ + --checkpoint_path=%s \ + --dataset_dir=%s \ + --gpu_memory_fraction=-1 +''' % (args.cuda_visible_devices, checkpoint_path, args.test_image_folder) + + my_exec(inference_cmd) + inference_res_zip_file = os.path.join( + args.checkpoint_folder, 'test', checkpoint_base_name, + '%s_det.zip' % checkpoint_base_name + ) + + eval_fscore_cmd = ''' + PYTHON_PATH=$PYTHONPATH:evaluation_script python ./evaluation_script/script.py -g=%s -s=%s + ''' % (args.test_ground_truth_zip_file, inference_res_zip_file) + + stdout_txt = exec_and_get_stdout(eval_fscore_cmd) + + recall = extract_value(stdout_txt, 'recall') + precision = extract_value(stdout_txt, 'precision') + fscore = extract_value(stdout_txt, 'hmean') + return recall, precision, fscore + + +def get_latest_checkpoint(checkpoint_folder): + """ + + :param checkpoint_folder: + :return: None if there is no any checkpoint in the folder yet, + otherwise a tuple of latest_checkpoint_base_name, latest_global_step + + + """ + step_number_regex = re.compile(r'.*model\.ckpt-(\d*)\.index') + step_numbers = [int(step_number_regex.match(f).group(1)) for f in glob.glob('%s/model.ckpt-*.index' % checkpoint_folder)] + + if len(step_numbers) == 0: + return None + + latest_global_step = sorted(step_numbers)[-1] + latest_checkpoint_base_name = 'model.ckpt-%s' % latest_global_step + return latest_checkpoint_base_name, latest_global_step + + +def main(args): + if args.checkpoint_path is not None: + one_off_evaluate(args) + else: + watching_and_evaluate(args) + + +def watching_and_evaluate(args): + tf_summary_writer = tf.contrib.summary.create_file_writer(args.checkpoint_folder) + + last_evaluated_global_step = None + while True: + res = get_latest_checkpoint(args.checkpoint_folder) + if res is not None: + latest_checkpoint_base_name, latest_global_step = res + # avoid evaluating on the very first checkpoint + if latest_global_step > 0 and last_evaluated_global_step != latest_global_step: + print('evaluate with %s' % latest_checkpoint_base_name) + recall, precision, fscore = eval_with_checkpoint(latest_checkpoint_base_name, args) + + # append result to fscrore.csv + log_result_to_file(recall, precision, fscore, latest_global_step, args) + log_result_for_tensorboard(recall, precision, fscore, latest_global_step, tf_summary_writer) + + last_evaluated_global_step = latest_global_step + + print('sleep ...') + time.sleep(EVALUATION_INTERVAL_IN_MINUTES * 60) + + +def one_off_evaluate(args): + args.checkpoint_folder, checkpoint_base_name = args.checkpoint_path.rsplit('/', 1) + recall, precision, fscore = eval_with_checkpoint(checkpoint_base_name, args) + print('recall=%.6f, prections=%.6f, fscore=%.6f' % (recall, precision, fscore)) + + +def log_result_to_file(recall, precision, fscore, latest_global_step, args): + fscore_csv_fpath = os.path.join(args.checkpoint_folder, 'fscore.csv') + row = '%d,%f,%f,%f' % (latest_global_step, recall, precision, fscore) + file(fscore_csv_fpath, 'a').write(row + '\n') + + +def log_result_for_tensorboard(recall, precision, fscore, step, tf_summary_writer): + with tf_summary_writer.as_default(), tf.contrib.summary.always_record_summaries(): + tf.contrib.summary.scalar("val/recall", recall, step=step) + tf.contrib.summary.scalar("val/precision", precision, step=step) + tf.contrib.summary.scalar("val/fscore", fscore, step=step) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument( + "-g", + "--cuda_visible_devices", + default='2', + help='Specify a gpu id which is not used by the training job', + ) + parser.add_argument( + "-c", + "--checkpoint_folder", + default='./checkpoint', + help='if this option is specified, the script will run in watching mode' + ) + parser.add_argument( + "-p", + "--checkpoint_path", + help='if this option is specified, the script will run in one-off mode ' + ) + parser.add_argument( + "-t", "--test_image_folder", + default='/home/victor/workspace/datasets/scene_text/street_number_recognition/val/jpg.clear', + ) + parser.add_argument( + "-z", "--test_ground_truth_zip_file", + default='/home/victor/workspace/datasets/scene_text/street_number_recognition/val/ic15_format_label.clear.zip', + ) + args = parser.parse_args() + main(args) \ No newline at end of file diff --git a/scripts/fscore_csv_to_tf_summary.py b/scripts/fscore_csv_to_tf_summary.py new file mode 100644 index 0000000..8a6bf05 --- /dev/null +++ b/scripts/fscore_csv_to_tf_summary.py @@ -0,0 +1,34 @@ +""" +Taken a fscore_csv file, create a tfsummary event file which can used with tensorboard +to visualize the fscore curve. + +The tf summary event file will be generated in the same folder as the fscore_csv file + +""" +import argparse +import os + +import pandas as pd +import tensorflow as tf +tf.enable_eager_execution() + + +def main(args): + output_dir = os.path.dirname(args.fscore_csv_fpath) + summary_writer = tf.contrib.summary.create_file_writer(output_dir) + + df = pd.read_csv(args.fscore_csv_fpath, header=None, names=['step', 'recall', 'precision', 'f1_score']) + with summary_writer.as_default(), tf.contrib.summary.always_record_summaries(): + for row in df.itertuples(): + tf.contrib.summary.scalar("recall", row.recall, step=row.step) + tf.contrib.summary.scalar("precision", row.precision, step=row.step) + tf.contrib.summary.scalar("f1_score", row.f1_score, step=row.step) + tf.contrib.summary.flush() + print('Done') + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument("-c", "--fscore_csv_fpath", required=True) + args = parser.parse_args() + main(args) \ No newline at end of file diff --git a/scripts/test_any.sh b/scripts/test_any.sh index bc2697e..a93e09f 100755 --- a/scripts/test_any.sh +++ b/scripts/test_any.sh @@ -6,6 +6,7 @@ export CUDA_VISIBLE_DEVICES=$1 python test_pixel_link_on_any_image.py \ --checkpoint_path=$2 \ --dataset_dir=$3 \ + --output_dir=$4 \ --eval_image_width=1280\ --eval_image_height=768\ --pixel_conf_threshold=0.5\ diff --git a/scripts/train.sh b/scripts/train.sh index 0ea30d5..ca2f80b 100755 --- a/scripts/train.sh +++ b/scripts/train.sh @@ -3,7 +3,8 @@ set -e export CUDA_VISIBLE_DEVICES=$1 IMG_PER_GPU=$2 -TRAIN_DIR=${HOME}/models/pixel_link +TRAIN_DIR=${PWD}/checkpoint +mkdir -p ${TRAIN_DIR} # get the number of gpus OLD_IFS="$IFS" @@ -18,28 +19,34 @@ BATCH_SIZE=`expr $NUM_GPUS \* $IMG_PER_GPU` #DATASET=synthtext #DATASET_PATH=SynthText -DATASET=icdar2015 -DATASET_DIR=$HOME/dataset/pixel_link/icdar2015 -python train_pixel_link.py \ - --train_dir=${TRAIN_DIR} \ - --num_gpus=${NUM_GPUS} \ - --learning_rate=1e-3\ - --gpu_memory_fraction=-1 \ - --train_image_width=512 \ - --train_image_height=512 \ - --batch_size=${BATCH_SIZE}\ - --dataset_dir=${DATASET_DIR} \ - --dataset_name=${DATASET} \ - --dataset_split_name=train \ - --max_number_of_steps=100\ - --checkpoint_path=${CKPT_PATH} \ - --using_moving_average=1 +#DATASET=synthesized_149k_and_street_number_train +#DATASET=synthesized_149k +#DATASET_DIR=$HOME/workspace/datasets/scene_text/synthesized_149k_and_street_number_train + +DATASET=street_number +DATASET_DIR=$HOME/workspace/datasets/scene_text/street_number_recognition/ + +#python train_pixel_link.py \ +# --train_dir=${TRAIN_DIR} \ +# --num_gpus=${NUM_GPUS} \ +# --learning_rate=1e-5\ +# --gpu_memory_fraction=-1 \ +# --train_image_width=512 \ +# --train_image_height=512 \ +# --batch_size=${BATCH_SIZE}\ +# --dataset_dir=${DATASET_DIR} \ +# --dataset_name=${DATASET} \ +# --dataset_split_name=train \ +# --max_number_of_steps=100\ +# --checkpoint_path=${CKPT_PATH} \ +# --using_moving_average=1 \ +# 2>&1 | tee -a ${TRAIN_DIR}/warmup.log python train_pixel_link.py \ --train_dir=${TRAIN_DIR} \ --num_gpus=${NUM_GPUS} \ - --learning_rate=1e-2\ + --learning_rate=1e-4\ --gpu_memory_fraction=-1 \ --train_image_width=512 \ --train_image_height=512 \ @@ -48,6 +55,9 @@ python train_pixel_link.py \ --dataset_name=${DATASET} \ --dataset_split_name=train \ --checkpoint_path=${CKPT_PATH} \ - --using_moving_average=1\ - 2>&1 | tee -a ${TRAIN_DIR}/log.log + --using_moving_average=1 \ + 2>&1 | tee -a ${TRAIN_DIR}/log.log + + +# python train_pixel_link.py --train_dir=/home/victor/workspace/pixel_link/checkpoint --num_gpus=2 --learning_rate=1e-3 --gpu_memory_fraction=-1 --train_image_width=512 --train_image_height=512 --batch_size=40 --dataset_dir=/home/victor/workspace/datasets/scene_text/street_number_recognition/ --dataset_name=street_number --dataset_split_name=train --checkpoint_path= --using_moving_average=1 diff --git a/set_env.sh b/set_env.sh new file mode 100644 index 0000000..adc7724 --- /dev/null +++ b/set_env.sh @@ -0,0 +1,2 @@ +export PYTHONPATH=$PYTHONPATH:/home/victor/workspace/pixel_link/pylib/src + diff --git a/test_pixel_link.py b/test_pixel_link.py index bab2ddd..f368402 100644 --- a/test_pixel_link.py +++ b/test_pixel_link.py @@ -1,4 +1,5 @@ -#encoding = utf-8 +# encoding = utf-8 +import os import numpy as np import math @@ -13,19 +14,18 @@ import pixel_link from nets import pixel_link_symbol - slim = tf.contrib.slim import config + # =========================================================================== # # Checkpoint and running Flags # =========================================================================== # -tf.app.flags.DEFINE_string('checkpoint_path', None, - 'the path of pretrained model to be used. If there are checkpoints\ - in train_dir, this config will be ignored.') - -tf.app.flags.DEFINE_float('gpu_memory_fraction', -1, - 'the gpu memory fraction to be used. If less than 0, allow_growth = True is used.') +tf.app.flags.DEFINE_string('checkpoint_path', None, + 'the path of pretrained model to be used. If there are checkpoints\ + in train_dir, this config will be ignored.') +tf.app.flags.DEFINE_float('gpu_memory_fraction', -1, + 'the gpu memory fraction to be used. If less than 0, allow_growth = True is used.') # =========================================================================== # # I/O and preprocessing Flags. @@ -34,143 +34,135 @@ 'num_readers', 1, 'The number of parallel readers that read data from the dataset.') tf.app.flags.DEFINE_integer( - 'num_preprocessing_threads', 4, + 'num_preprocessing_threads', 8, 'The number of threads used to create the batches.') -tf.app.flags.DEFINE_bool('preprocessing_use_rotation', False, - 'Whether to use rotation for data augmentation') +tf.app.flags.DEFINE_bool('preprocessing_use_rotation', False, + 'Whether to use rotation for data augmentation') # =========================================================================== # # Dataset Flags. # =========================================================================== # -tf.app.flags.DEFINE_string( - 'dataset_name', 'icdar2015', 'The name of the dataset to load.') -tf.app.flags.DEFINE_string( - 'dataset_split_name', 'test', 'The name of the train/test split.') -tf.app.flags.DEFINE_string('dataset_dir', - util.io.get_absolute_path('~/dataset/ICDAR2015/Challenge4/ch4_test_images'), - 'The directory where the dataset files are stored.') +tf.app.flags.DEFINE_string('dataset_dir', + util.io.get_absolute_path('~/dataset/ICDAR2015/Challenge4/ch4_test_images'), + 'The directory where the dataset files are stored.') tf.app.flags.DEFINE_integer('eval_image_width', 1280, 'Train image size') tf.app.flags.DEFINE_integer('eval_image_height', 768, 'Train image size') -tf.app.flags.DEFINE_bool('using_moving_average', True, +tf.app.flags.DEFINE_bool('using_moving_average', True, 'Whether to use ExponentionalMovingAverage') -tf.app.flags.DEFINE_float('moving_average_decay', 0.9999, +tf.app.flags.DEFINE_float('moving_average_decay', 0.9999, 'The decay rate of ExponentionalMovingAverage') - FLAGS = tf.app.flags.FLAGS + def config_initialization(): # image shape and feature layers shape inference image_shape = (FLAGS.eval_image_height, FLAGS.eval_image_width) - + if not FLAGS.dataset_dir: raise ValueError('You must supply the dataset directory with --dataset_dir') - + tf.logging.set_verbosity(tf.logging.DEBUG) config.load_config(FLAGS.checkpoint_path) - config.init_config(image_shape, - batch_size = 1, - pixel_conf_threshold = 0.8, - link_conf_threshold = 0.8, - num_gpus = 1, - ) - - util.proc.set_proc_name('test_pixel_link_on'+ '_' + FLAGS.dataset_name) - - - -def to_txt(txt_path, image_name, + config.init_config(image_shape, + batch_size=1, + pixel_conf_threshold=0.8, + link_conf_threshold=0.8, + num_gpus=1, + ) + + +def to_txt(txt_path, image_name, image_data, pixel_pos_scores, link_pos_scores): # write detection result as txt files def write_result_as_txt(image_name, bboxes, path): - filename = util.io.join_path(path, 'res_%s.txt'%(image_name)) + filename = util.io.join_path(path, 'res_%s.txt' % (image_name)) lines = [] for b_idx, bbox in enumerate(bboxes): - values = [int(v) for v in bbox] - line = "%d, %d, %d, %d, %d, %d, %d, %d\n"%tuple(values) - lines.append(line) + values = [int(v) for v in bbox] + line = "%d, %d, %d, %d, %d, %d, %d, %d\n" % tuple(values) + lines.append(line) util.io.write_lines(filename, lines) print 'result has been written to:', filename - + mask = pixel_link.decode_batch(pixel_pos_scores, link_pos_scores)[0, ...] bboxes = pixel_link.mask_to_bboxes(mask, image_data.shape) write_result_as_txt(image_name, bboxes, txt_path) + def test(): with tf.name_scope('test'): - image = tf.placeholder(dtype=tf.int32, shape = [None, None, 3]) - image_shape = tf.placeholder(dtype = tf.int32, shape = [3, ]) - processed_image, _, _, _, _ = ssd_vgg_preprocessing.preprocess_image(image, None, None, None, None, - out_shape = config.image_shape, - data_format = config.data_format, - is_training = False) - b_image = tf.expand_dims(processed_image, axis = 0) - net = pixel_link_symbol.PixelLinkNet(b_image, is_training = True) + image = tf.placeholder(dtype=tf.int32, shape=[None, None, 3]) + image_shape = tf.placeholder(dtype=tf.int32, shape=[3, ]) + processed_image, _, _, _, _ = ssd_vgg_preprocessing.preprocess_image(image, None, None, None, None, + out_shape=config.image_shape, + data_format=config.data_format, + is_training=False) + b_image = tf.expand_dims(processed_image, axis=0) + net = pixel_link_symbol.PixelLinkNet(b_image, is_training=True) global_step = slim.get_or_create_global_step() - - sess_config = tf.ConfigProto(log_device_placement = False, allow_soft_placement = True) + sess_config = tf.ConfigProto(log_device_placement=False, allow_soft_placement=True) if FLAGS.gpu_memory_fraction < 0: sess_config.gpu_options.allow_growth = True elif FLAGS.gpu_memory_fraction > 0: sess_config.gpu_options.per_process_gpu_memory_fraction = FLAGS.gpu_memory_fraction; - + checkpoint_dir = util.io.get_dir(FLAGS.checkpoint_path) - logdir = util.io.join_path(checkpoint_dir, 'test', FLAGS.dataset_name + '_' +FLAGS.dataset_split_name) + logdir = util.io.join_path(checkpoint_dir, 'test') # Variables to restore: moving avg. or normal weights. if FLAGS.using_moving_average: variable_averages = tf.train.ExponentialMovingAverage( - FLAGS.moving_average_decay) + FLAGS.moving_average_decay) variables_to_restore = variable_averages.variables_to_restore() variables_to_restore[global_step.op.name] = global_step else: variables_to_restore = slim.get_variables_to_restore() - - saver = tf.train.Saver(var_list = variables_to_restore) - - + + saver = tf.train.Saver(var_list=variables_to_restore) + image_names = util.io.ls(FLAGS.dataset_dir) image_names.sort() - + checkpoint = FLAGS.checkpoint_path checkpoint_name = util.io.get_filename(str(checkpoint)); dump_path = util.io.join_path(logdir, checkpoint_name) - txt_path = util.io.join_path(dump_path,'txt') + if os.path.exists(dump_path): + os.system('rm -rf %s' % dump_path) + txt_path = util.io.join_path(dump_path, 'txt') zip_path = util.io.join_path(dump_path, checkpoint_name + '_det.zip') - - with tf.Session(config = sess_config) as sess: + + with tf.Session(config=sess_config) as sess: saver.restore(sess, checkpoint) for iter, image_name in enumerate(image_names): image_data = util.img.imread( - util.io.join_path(FLAGS.dataset_dir, image_name), rgb = True) + util.io.join_path(FLAGS.dataset_dir, image_name), rgb=True) image_name = image_name.split('.')[0] pixel_pos_scores, link_pos_scores = sess.run( - [net.pixel_pos_scores, net.link_pos_scores], - feed_dict = { - image:image_data - }) - - print '%d/%d: %s'%(iter + 1, len(image_names), image_name) + [net.pixel_pos_scores, net.link_pos_scores], + feed_dict={ + image: image_data + }) + + print '%d/%d: %s' % (iter + 1, len(image_names), image_name) to_txt(txt_path, - image_name, image_data, - pixel_pos_scores, link_pos_scores) + image_name, image_data, + pixel_pos_scores, link_pos_scores) - # create zip file for icdar2015 - cmd = 'cd %s;zip -j %s %s/*'%(dump_path, zip_path, txt_path); + cmd = 'cd %s;zip -j %s %s/*' % (dump_path, zip_path, txt_path); print cmd util.cmd.cmd(cmd); print "zip file created: ", util.io.join_path(dump_path, zip_path) - def main(_): config_initialization() test() - - + + if __name__ == '__main__': tf.app.run() diff --git a/test_pixel_link_on_any_image.py b/test_pixel_link_on_any_image.py index 4e5f37a..29b212d 100644 --- a/test_pixel_link_on_any_image.py +++ b/test_pixel_link_on_any_image.py @@ -1,4 +1,5 @@ -#encoding = utf-8 +# encoding = utf-8 +import os import numpy as np import math @@ -13,139 +14,150 @@ import pixel_link from nets import pixel_link_symbol - slim = tf.contrib.slim import config + # =========================================================================== # # Checkpoint and running Flags # =========================================================================== # -tf.app.flags.DEFINE_string('checkpoint_path', None, - 'the path of pretrained model to be used. If there are checkpoints\ - in train_dir, this config will be ignored.') - -tf.app.flags.DEFINE_float('gpu_memory_fraction', -1, - 'the gpu memory fraction to be used. If less than 0, allow_growth = True is used.') +tf.app.flags.DEFINE_string('checkpoint_path', None, + 'the path of pretrained model to be used. If there are checkpoints\ + in train_dir, this config will be ignored.') +tf.app.flags.DEFINE_float('gpu_memory_fraction', -1, + 'the gpu memory fraction to be used. If less than 0, allow_growth = True is used.') # =========================================================================== # # Dataset Flags. # =========================================================================== # tf.app.flags.DEFINE_string( - 'dataset_dir', 'None', + 'dataset_dir', 'None', 'The directory where the dataset files are stored.') -tf.app.flags.DEFINE_integer('eval_image_width', None, 'resized image width for inference') -tf.app.flags.DEFINE_integer('eval_image_height', None, 'resized image height for inference') -tf.app.flags.DEFINE_float('pixel_conf_threshold', None, 'threshold on the pixel confidence') -tf.app.flags.DEFINE_float('link_conf_threshold', None, 'threshold on the link confidence') +tf.app.flags.DEFINE_string( + 'output_dir', 'None', + 'The directory where the output images should be saved.') +tf.app.flags.DEFINE_integer('eval_image_width', None, 'resized image width for inference') +tf.app.flags.DEFINE_integer('eval_image_height', None, 'resized image height for inference') +tf.app.flags.DEFINE_float('pixel_conf_threshold', None, 'threshold on the pixel confidence') +tf.app.flags.DEFINE_float('link_conf_threshold', None, 'threshold on the link confidence') -tf.app.flags.DEFINE_bool('using_moving_average', True, +tf.app.flags.DEFINE_bool('using_moving_average', True, 'Whether to use ExponentionalMovingAverage') -tf.app.flags.DEFINE_float('moving_average_decay', 0.9999, +tf.app.flags.DEFINE_float('moving_average_decay', 0.9999, 'The decay rate of ExponentionalMovingAverage') - FLAGS = tf.app.flags.FLAGS + def config_initialization(): # image shape and feature layers shape inference image_shape = (FLAGS.eval_image_height, FLAGS.eval_image_width) - + if not FLAGS.dataset_dir: raise ValueError('You must supply the dataset directory with --dataset_dir') - + tf.logging.set_verbosity(tf.logging.DEBUG) - - config.init_config(image_shape, - batch_size = 1, - pixel_conf_threshold = FLAGS.pixel_conf_threshold, - link_conf_threshold = FLAGS.link_conf_threshold, - num_gpus = 1, - ) + + config.init_config(image_shape, + batch_size=1, + pixel_conf_threshold=FLAGS.pixel_conf_threshold, + link_conf_threshold=FLAGS.link_conf_threshold, + num_gpus=1, + ) def test(): checkpoint_dir = util.io.get_dir(FLAGS.checkpoint_path) - + global_step = slim.get_or_create_global_step() - with tf.name_scope('evaluation_%dx%d'%(FLAGS.eval_image_height, FLAGS.eval_image_width)): - with tf.variable_scope(tf.get_variable_scope(), reuse = False): - image = tf.placeholder(dtype=tf.int32, shape = [None, None, 3]) - image_shape = tf.placeholder(dtype = tf.int32, shape = [3, ]) - processed_image, _, _, _, _ = ssd_vgg_preprocessing.preprocess_image(image, None, None, None, None, - out_shape = config.image_shape, - data_format = config.data_format, - is_training = False) - b_image = tf.expand_dims(processed_image, axis = 0) + with tf.name_scope('evaluation_%dx%d' % (FLAGS.eval_image_height, FLAGS.eval_image_width)): + with tf.variable_scope(tf.get_variable_scope(), reuse=False): + image = tf.placeholder(dtype=tf.int32, shape=[None, None, 3]) + image_shape = tf.placeholder(dtype=tf.int32, shape=[3, ]) + processed_image, _, _, _, _ = ssd_vgg_preprocessing.preprocess_image(image, None, None, None, None, + out_shape=config.image_shape, + data_format=config.data_format, + is_training=False) + b_image = tf.expand_dims(processed_image, axis=0) # build model and loss - net = pixel_link_symbol.PixelLinkNet(b_image, is_training = False) + net = pixel_link_symbol.PixelLinkNet(b_image, is_training=False) masks = pixel_link.tf_decode_score_map_to_mask_in_batch( net.pixel_pos_scores, net.link_pos_scores) - - sess_config = tf.ConfigProto(log_device_placement = False, allow_soft_placement = True) + + sess_config = tf.ConfigProto(log_device_placement=False, allow_soft_placement=True) if FLAGS.gpu_memory_fraction < 0: sess_config.gpu_options.allow_growth = True elif FLAGS.gpu_memory_fraction > 0: sess_config.gpu_options.per_process_gpu_memory_fraction = FLAGS.gpu_memory_fraction; - + # Variables to restore: moving avg. or normal weights. if FLAGS.using_moving_average: variable_averages = tf.train.ExponentialMovingAverage( - FLAGS.moving_average_decay) + FLAGS.moving_average_decay) variables_to_restore = variable_averages.variables_to_restore( - tf.trainable_variables()) + tf.trainable_variables()) variables_to_restore[global_step.op.name] = global_step else: variables_to_restore = slim.get_variables_to_restore() - - - saver = tf.train.Saver(var_list = variables_to_restore) + + saver = tf.train.Saver(var_list=variables_to_restore) with tf.Session() as sess: saver.restore(sess, util.tf.get_latest_ckpt(FLAGS.checkpoint_path)) - + + tf.logging.info('model restored') + files = util.io.ls(FLAGS.dataset_dir) - + tf.logging.info('image files listed') + for image_name in files: file_path = util.io.join_path(FLAGS.dataset_dir, image_name) - image_data = util.img.imread(file_path) + image_data = util.img.imread(file_path, rgb=True) link_scores, pixel_scores, mask_vals = sess.run( - [net.link_pos_scores, net.pixel_pos_scores, masks], - feed_dict = {image: image_data}) - h, w, _ =image_data.shape + [net.link_pos_scores, net.pixel_pos_scores, masks], + feed_dict={image: image_data}) + h, w, _ = image_data.shape + def resize(img): - return util.img.resize(img, size = (w, h), - interpolation = cv2.INTER_NEAREST) - + return util.img.resize(img, size=(w, h), + interpolation=cv2.INTER_NEAREST) + def get_bboxes(mask): return pixel_link.mask_to_bboxes(mask, image_data.shape) - + def draw_bboxes(img, bboxes, color): for bbox in bboxes: points = np.reshape(bbox, [4, 2]) cnts = util.img.points_to_contours(points) - util.img.draw_contours(img, contours = cnts, - idx = -1, color = color, border_width = 1) + util.img.draw_contours(img, contours=cnts, + idx=-1, color=color, border_width=1) + image_idx = 0 pixel_score = pixel_scores[image_idx, ...] mask = mask_vals[image_idx, ...] bboxes_det = get_bboxes(mask) - + mask = resize(mask) pixel_score = resize(pixel_score) draw_bboxes(image_data, bboxes_det, util.img.COLOR_RGB_RED) -# print util.sit(pixel_score) -# print util.sit(mask) - print util.sit(image_data) - - + # print util.sit(pixel_score) + # print util.sit(mask) + output_img_path = os.path.join(FLAGS.output_dir, image_name) + print util.sit(image_data, path=output_img_path) # save image to temp file and return the path + + def main(_): dataset = config_initialization() + + if not os.path.exists(FLAGS.output_dir): + os.makedirs(FLAGS.output_dir) + test() - - + + if __name__ == '__main__': tf.app.run() diff --git a/tf_extended/bboxes.py b/tf_extended/bboxes.py index bb32257..2ef4338 100644 --- a/tf_extended/bboxes.py +++ b/tf_extended/bboxes.py @@ -92,7 +92,7 @@ def bboxes_filter_by_shorter_side(labels, bboxes, xs, ys, min_height = 16, max_h """ Filtering bboxes by the length of shorter side """ - with tf.name_scope('bboxes_filter_by_shorter_side', [labels, bboxes]): + with tf.name_scope('bboxes_filter_by_shorter_side', values=[labels, bboxes]): bbox_rects = util.tf.min_area_rect(xs, ys) ws, hs = bbox_rects[:, 2], bbox_rects[:, 3] shorter_sides = tf.minimum(ws, hs) diff --git a/train_pixel_link.py b/train_pixel_link.py index 5b9e747..79401b4 100644 --- a/train_pixel_link.py +++ b/train_pixel_link.py @@ -1,4 +1,5 @@ #test code to make sure the ground truth calculation and data batch works well. +import os import numpy as np import tensorflow as tf # test @@ -15,16 +16,15 @@ # =========================================================================== # # Checkpoint and running Flags # =========================================================================== # -tf.app.flags.DEFINE_string('train_dir', None, +tf.app.flags.DEFINE_string('train_dir', './checkpoint', 'the path to store checkpoints and eventfiles for summaries') - -tf.app.flags.DEFINE_string('checkpoint_path', None, +tf.app.flags.DEFINE_string('checkpoint_path', '', 'the path of pretrained model to be used. If there are checkpoints in train_dir, this config will be ignored.') tf.app.flags.DEFINE_float('gpu_memory_fraction', -1, 'the gpu memory fraction to be used. If less than 0, allow_growth = True is used.') -tf.app.flags.DEFINE_integer('batch_size', None, 'The number of samples in each batch.') +tf.app.flags.DEFINE_integer('batch_size', 8, 'The number of samples in each batch.') tf.app.flags.DEFINE_integer('num_gpus', 1, 'The number of gpus can be used.') tf.app.flags.DEFINE_integer('max_number_of_steps', 1000000, 'The maximum number of training steps.') tf.app.flags.DEFINE_integer('log_every_n_steps', 1, 'log frequency') @@ -54,11 +54,11 @@ # Dataset Flags. # =========================================================================== # tf.app.flags.DEFINE_string( - 'dataset_name', None, 'The name of the dataset to load.') + 'dataset_name', 'street_number', 'The name of the dataset to load.') tf.app.flags.DEFINE_string( 'dataset_split_name', 'train', 'The name of the train/test split.') tf.app.flags.DEFINE_string( - 'dataset_dir', None, 'The directory where the dataset files are stored.') + 'dataset_dir', '/home/victor/workspace/datasets/scene_text/street_number_recognition/', 'The directory where the dataset files are stored.') tf.app.flags.DEFINE_integer('train_image_width', 512, 'Train image size') tf.app.flags.DEFINE_integer('train_image_height', 512, 'Train image size') @@ -76,7 +76,8 @@ def config_initialization(): log_file = 'log_train_pixel_link_%d_%d.log'%image_shape, log_path = FLAGS.train_dir, stdout = False, mode = 'a') - + # if there is a config.py in train_dir, use it to replace current one, otherwise copy current config.py to there + # this basically means we can use the same config.py if we need to resume training in future config.load_config(FLAGS.train_dir) config.init_config(image_shape, @@ -88,8 +89,8 @@ def config_initialization(): batch_size = config.batch_size batch_size_per_gpu = config.batch_size_per_gpu - tf.summary.scalar('batch_size', batch_size) - tf.summary.scalar('batch_size_per_gpu', batch_size_per_gpu) + tf.summary.scalar('config/batch_size', batch_size) + tf.summary.scalar('config/batch_size_per_gpu', batch_size_per_gpu) util.proc.set_proc_name('train_pixel_link_on'+ '_' + FLAGS.dataset_name) @@ -158,20 +159,24 @@ def create_dataset_batch_queue(dataset): capacity = 50) return batch_queue -def sum_gradients(clone_grads): + +def sum_gradients(clone_grads): + """ + basically return a new list of (grad, variable) tuples, where each grad is the sum + of the corresponding grad in each clone (i.e. GPU) + :param clone_grads: [grad_and_vars_of_clone0, grad_and_vars_of_clone1, grad_and_vars_of_clone2, ...] + :return: + """ averaged_grads = [] for grad_and_vars in zip(*clone_grads): + # grad_and_vars is (grad_and_var_tuple_k_of_gpu0, grad_and_var_tuple_k_of_gpu1, grad_and_var_tuple_k_of_gpu2,..) grads = [] var = grad_and_vars[0][1] - try: - for g, v in grad_and_vars: - assert v == var - grads.append(g) + for g, v in grad_and_vars: # iterat over the copy of g/v of each gpu + assert v == var + grads.append(g) grad = tf.add_n(grads, name = v.op.name + '_summed_gradients') - except: - import pdb - pdb.set_trace() - + averaged_grads.append((grad, v)) # tf.summary.histogram("variables_and_gradients_" + grad.op.name, grad) @@ -186,12 +191,16 @@ def create_clones(batch_queue): with tf.device('/cpu:0'): global_step = slim.create_global_step() learning_rate = tf.constant(FLAGS.learning_rate, name='learning_rate') - optimizer = tf.train.MomentumOptimizer(learning_rate, - momentum=FLAGS.momentum, name='Momentum') + tf.summary.scalar('config/learning_rate', learning_rate) + + if getattr(config, 'optimizer', 'Momentum') == 'Adam': + optimizer = tf.train.AdamOptimizer(learning_rate, name='Adam') + else: + optimizer = tf.train.MomentumOptimizer(learning_rate, momentum=FLAGS.momentum, name='Momentum') + print('Using %s optimizer' % optimizer.get_name()) - tf.summary.scalar('learning_rate', learning_rate) # place clones - pixel_link_loss = 0; # for summary only + pixel_link_loss = 0 # for summary only gradients = [] for clone_idx, gpu in enumerate(config.gpus): do_summary = clone_idx == 0 # only summary on the first clone @@ -225,8 +234,8 @@ def create_clones(batch_queue): clone_gradients = optimizer.compute_gradients(total_clone_loss) gradients.append(clone_gradients) - tf.summary.scalar('pixel_link_loss', pixel_link_loss) - tf.summary.scalar('regularization_loss', regularization_loss) + tf.summary.scalar('train/pixel_link_loss', pixel_link_loss) + tf.summary.scalar('train/regularization_loss', regularization_loss) # add all gradients together # note that the gradients do not need to be averaged, because the average operation has been done on loss. @@ -252,8 +261,7 @@ def create_clones(batch_queue): train_op = control_flow_ops.with_dependencies(train_ops, pixel_link_loss, name='train_op') return train_op - - + def train(train_op): summary_op = tf.summary.merge_all() sess_config = tf.ConfigProto(log_device_placement = False, allow_soft_placement = True) @@ -280,7 +288,7 @@ def train(train_op): def main(_): - # The choice of return dataset object via initialization method maybe confusing, + # The choice of return dataset object via initialization method maybe confusing, # but I need to print all configurations in this method, including dataset information. dataset = config_initialization() diff --git a/visualize_detection_result.py b/visualize_detection_result.py index 4ea3ad7..0a5a11d 100644 --- a/visualize_detection_result.py +++ b/visualize_detection_result.py @@ -30,13 +30,14 @@ def read_image_file(image_name): print '%d / %d: %s'%(image_idx + 1, len(image_names), image_name) image_data = read_image_file(image_name) # in BGR image_name = image_name.split('.')[0] - det_image = image_data.copy() - det_lines = read_det_file(image_name) - for line in det_lines: - draw_bbox(det_image, line, color = util.img.COLOR_GREEN) - output_path = util.io.join_path(output_root, '%s_pred.jpg'%(image_name)) - util.img.imwrite(output_path, det_image) - print "Detection result has been written to ", util.io.get_absolute_path(output_path) + if det_root is not None: + det_image = image_data.copy() + det_lines = read_det_file(image_name) + for line in det_lines: + draw_bbox(det_image, line, color = util.img.COLOR_GREEN) + output_path = util.io.join_path(output_root, '%s_pred.jpg'%(image_name)) + util.img.imwrite(output_path, det_image) + print "Detection result has been written to ", util.io.get_absolute_path(output_path) if gt_root is not None: gt_lines = read_gt_file(image_name) @@ -48,12 +49,17 @@ def read_image_file(image_name): import argparse parser = argparse.ArgumentParser(description='visualize detection result of pixel_link') parser.add_argument('--image', type=str, required = True,help='the directory of test image') - parser.add_argument('--gt', type=str, default=None,help='the directory of ground truth txt files') - parser.add_argument('--det', type=str, required = True, help='the directory of detection result') + parser.add_argument('--gt', type=str, default=None, help='the directory of ground truth txt files') + parser.add_argument('--det', type=str, default=None, help='the directory of detection result') parser.add_argument('--output', type=str, required = True, help='the directory to store images with bboxes') args = parser.parse_args() + print('**************Arguments*****************') print(args) print('****************************************') - visualize(image_root = args.image, gt_root = args.gt, det_root = args.det, output_root = args.output) + + if not args.det and not args.gt: + print("At least one of --gt or --det should be specified") + else: + visualize(image_root = args.image, gt_root = args.gt, det_root = args.det, output_root = args.output)