From b8972b3675edbd27a76b342c0fc2add256cd29fc Mon Sep 17 00:00:00 2001 From: LDOUBLEV Date: Wed, 26 May 2021 10:40:16 +0000 Subject: [PATCH 01/29] add python benchmark for ocr --- tools/infer/benchmark_utils.py | 232 +++++++++++++++++++++++++++++++++ tools/infer/predict_cls.py | 21 ++- tools/infer/predict_det.py | 81 ++++++++++-- tools/infer/predict_rec.py | 116 +++++++++++------ tools/infer/predict_system.py | 109 ++++++++++++---- tools/infer/utility.py | 120 ++++++++++++++++- 6 files changed, 595 insertions(+), 84 deletions(-) create mode 100644 tools/infer/benchmark_utils.py diff --git a/tools/infer/benchmark_utils.py b/tools/infer/benchmark_utils.py new file mode 100644 index 00000000..1a241d06 --- /dev/null +++ b/tools/infer/benchmark_utils.py @@ -0,0 +1,232 @@ +# Copyright (c) 2021 PaddlePaddle 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. + +import argparse +import os +import time +import logging + +import paddle +import paddle.inference as paddle_infer + +from pathlib import Path + +CUR_DIR = os.path.dirname(os.path.abspath(__file__)) + + +class PaddleInferBenchmark(object): + def __init__(self, + config, + model_info: dict={}, + data_info: dict={}, + perf_info: dict={}, + resource_info: dict={}, + save_log_path: str="", + **kwargs): + """ + Construct PaddleInferBenchmark Class to format logs. + args: + config(paddle.inference.Config): paddle inference config + model_info(dict): basic model info + {'model_name': 'resnet50' + 'precision': 'fp32'} + data_info(dict): input data info + {'batch_size': 1 + 'shape': '3,224,224' + 'data_num': 1000} + perf_info(dict): performance result + {'preprocess_time_s': 1.0 + 'inference_time_s': 2.0 + 'postprocess_time_s': 1.0 + 'total_time_s': 4.0} + resource_info(dict): + cpu and gpu resources + {'cpu_rss': 100 + 'gpu_rss': 100 + 'gpu_util': 60} + """ + # PaddleInferBenchmark Log Version + self.log_version = 1.0 + + # Paddle Version + self.paddle_version = paddle.__version__ + self.paddle_commit = paddle.__git_commit__ + paddle_infer_info = paddle_infer.get_version() + self.paddle_branch = paddle_infer_info.strip().split(': ')[-1] + + # model info + self.model_info = model_info + + # data info + self.data_info = data_info + + # perf info + self.perf_info = perf_info + + try: + self.model_name = model_info['model_name'] + self.precision = model_info['precision'] + + self.batch_size = data_info['batch_size'] + self.shape = data_info['shape'] + self.data_num = data_info['data_num'] + + self.preprocess_time_s = round(perf_info['preprocess_time_s'], 4) + self.inference_time_s = round(perf_info['inference_time_s'], 4) + self.postprocess_time_s = round(perf_info['postprocess_time_s'], 4) + self.total_time_s = round(perf_info['total_time_s'], 4) + except: + self.print_help() + raise ValueError( + "Set argument wrong, please check input argument and its type") + + # conf info + self.config_status = self.parse_config(config) + self.save_log_path = save_log_path + # mem info + if isinstance(resource_info, dict): + self.cpu_rss_mb = int(resource_info.get('cpu_rss_mb', 0)) + self.gpu_rss_mb = int(resource_info.get('gpu_rss_mb', 0)) + self.gpu_util = round(resource_info.get('gpu_util', 0), 2) + else: + self.cpu_rss_mb = 0 + self.gpu_rss_mb = 0 + self.gpu_util = 0 + + # init benchmark logger + self.benchmark_logger() + + def benchmark_logger(self): + """ + benchmark logger + """ + # Init logger + FORMAT = '%(asctime)s - %(name)s - %(levelname)s - %(message)s' + log_output = f"{self.save_log_path}/{self.model_name}.log" + Path(f"{self.save_log_path}").mkdir(parents=True, exist_ok=True) + logging.basicConfig( + level=logging.INFO, + format=FORMAT, + handlers=[ + logging.FileHandler( + filename=log_output, mode='w'), + logging.StreamHandler(), + ]) + self.logger = logging.getLogger(__name__) + self.logger.info( + f"Paddle Inference benchmark log will be saved to {log_output}") + + def parse_config(self, config) -> dict: + """ + parse paddle predictor config + args: + config(paddle.inference.Config): paddle inference config + return: + config_status(dict): dict style config info + """ + config_status = {} + config_status['runtime_device'] = "gpu" if config.use_gpu() else "cpu" + config_status['ir_optim'] = config.ir_optim() + config_status['enable_tensorrt'] = config.tensorrt_engine_enabled() + config_status['precision'] = self.precision + config_status['enable_mkldnn'] = config.mkldnn_enabled() + config_status[ + 'cpu_math_library_num_threads'] = config.cpu_math_library_num_threads( + ) + return config_status + + def report(self, identifier=None): + """ + print log report + args: + identifier(string): identify log + """ + if identifier: + identifier = f"[{identifier}]" + else: + identifier = "" + + self.logger.info("\n") + self.logger.info( + "---------------------- Paddle info ----------------------") + self.logger.info(f"{identifier} paddle_version: {self.paddle_version}") + self.logger.info(f"{identifier} paddle_commit: {self.paddle_commit}") + self.logger.info(f"{identifier} paddle_branch: {self.paddle_branch}") + self.logger.info(f"{identifier} log_api_version: {self.log_version}") + self.logger.info( + "----------------------- Conf info -----------------------") + self.logger.info( + f"{identifier} runtime_device: {self.config_status['runtime_device']}" + ) + self.logger.info( + f"{identifier} ir_optim: {self.config_status['ir_optim']}") + self.logger.info(f"{identifier} enable_memory_optim: {True}") + self.logger.info( + f"{identifier} enable_tensorrt: {self.config_status['enable_tensorrt']}" + ) + self.logger.info( + f"{identifier} enable_mkldnn: {self.config_status['enable_mkldnn']}") + self.logger.info( + f"{identifier} cpu_math_library_num_threads: {self.config_status['cpu_math_library_num_threads']}" + ) + self.logger.info( + "----------------------- Model info ----------------------") + self.logger.info(f"{identifier} model_name: {self.model_name}") + self.logger.info(f"{identifier} precision: {self.precision}") + self.logger.info( + "----------------------- Data info -----------------------") + self.logger.info(f"{identifier} batch_size: {self.batch_size}") + self.logger.info(f"{identifier} input_shape: {self.shape}") + self.logger.info(f"{identifier} data_num: {self.data_num}") + self.logger.info( + "----------------------- Perf info -----------------------") + self.logger.info( + f"{identifier} cpu_rss(MB): {self.cpu_rss_mb}, gpu_rss(MB): {self.gpu_rss_mb}, gpu_util: {self.gpu_util}%" + ) + self.logger.info( + f"{identifier} total time spent(s): {self.total_time_s}") + self.logger.info( + f"{identifier} preprocess_time(ms): {round(self.preprocess_time_s*1000, 1)}, inference_time(ms): {round(self.inference_time_s*1000, 1)}, postprocess_time(ms): {round(self.postprocess_time_s*1000, 1)}" + ) + + def print_help(self): + """ + print function help + """ + print("""Usage: + ==== Print inference benchmark logs. ==== + config = paddle.inference.Config() + model_info = {'model_name': 'resnet50' + 'precision': 'fp32'} + data_info = {'batch_size': 1 + 'shape': '3,224,224' + 'data_num': 1000} + perf_info = {'preprocess_time_s': 1.0 + 'inference_time_s': 2.0 + 'postprocess_time_s': 1.0 + 'total_time_s': 4.0} + resource_info = {'cpu_rss_mb': 100 + 'gpu_rss_mb': 100 + 'gpu_util': 60} + log = PaddleInferBenchmark(config, model_info, data_info, perf_info, resource_info) + log('Test') + """) + + def __call__(self, identifier=None): + """ + __call__ + args: + identifier(string): identify log + """ + self.report(identifier) diff --git a/tools/infer/predict_cls.py b/tools/infer/predict_cls.py index d2592c6c..0037b226 100755 --- a/tools/infer/predict_cls.py +++ b/tools/infer/predict_cls.py @@ -45,9 +45,11 @@ class TextClassifier(object): "label_list": args.label_list, } self.postprocess_op = build_post_process(postprocess_params) - self.predictor, self.input_tensor, self.output_tensors = \ + self.predictor, self.input_tensor, self.output_tensors, _ = \ utility.create_predictor(args, 'cls', logger) + self.cls_times = utility.Timer() + def resize_norm_img(self, img): imgC, imgH, imgW = self.cls_image_shape h = img.shape[0] @@ -83,7 +85,9 @@ class TextClassifier(object): cls_res = [['', 0.0]] * img_num batch_num = self.cls_batch_num elapse = 0 + self.cls_times.total_time.start() for beg_img_no in range(0, img_num, batch_num): + end_img_no = min(img_num, beg_img_no + batch_num) norm_img_batch = [] max_wh_ratio = 0 @@ -91,6 +95,7 @@ class TextClassifier(object): h, w = img_list[indices[ino]].shape[0:2] wh_ratio = w * 1.0 / h max_wh_ratio = max(max_wh_ratio, wh_ratio) + self.cls_times.preprocess_time.start() for ino in range(beg_img_no, end_img_no): norm_img = self.resize_norm_img(img_list[indices[ino]]) norm_img = norm_img[np.newaxis, :] @@ -98,11 +103,17 @@ class TextClassifier(object): norm_img_batch = np.concatenate(norm_img_batch) norm_img_batch = norm_img_batch.copy() starttime = time.time() + self.cls_times.preprocess_time.end() + self.cls_times.inference_time.start() + self.input_tensor.copy_from_cpu(norm_img_batch) self.predictor.run() prob_out = self.output_tensors[0].copy_to_cpu() + self.cls_times.inference_time.end() + self.cls_times.postprocess_time.start() self.predictor.try_shrink_memory() cls_result = self.postprocess_op(prob_out) + self.cls_times.postprocess_time.end() elapse += time.time() - starttime for rno in range(len(cls_result)): label, score = cls_result[rno] @@ -110,6 +121,9 @@ class TextClassifier(object): if '180' in label and score > self.cls_thresh: img_list[indices[beg_img_no + rno]] = cv2.rotate( img_list[indices[beg_img_no + rno]], 1) + self.cls_times.total_time.end() + self.cls_times.img_num += img_num + elapse = self.cls_times.total_time.value() return img_list, cls_res, elapse @@ -141,8 +155,9 @@ def main(args): for ino in range(len(img_list)): logger.info("Predicts of {}:{}".format(valid_image_file_list[ino], cls_res[ino])) - logger.info("Total predict time for {} images, cost: {:.3f}".format( - len(img_list), predict_time)) + logger.info( + "The predict time about text angle classify module is as follows: ") + text_classifier.cls_times.info(average=False) if __name__ == "__main__": diff --git a/tools/infer/predict_det.py b/tools/infer/predict_det.py index 59bb49f9..f5bade36 100755 --- a/tools/infer/predict_det.py +++ b/tools/infer/predict_det.py @@ -31,6 +31,8 @@ from ppocr.utils.utility import get_image_file_list, check_and_read_gif from ppocr.data import create_operators, transform from ppocr.postprocess import build_post_process +import tools.infer.benchmark_utils as benchmark_utils + logger = get_logger() @@ -95,9 +97,10 @@ class TextDetector(object): self.preprocess_op = create_operators(pre_process_list) self.postprocess_op = build_post_process(postprocess_params) - self.predictor, self.input_tensor, self.output_tensors = utility.create_predictor( - args, 'det', logger) # paddle.jit.load(args.det_model_dir) - # self.predictor.eval() + self.predictor, self.input_tensor, self.output_tensors, self.config = utility.create_predictor( + args, 'det', logger) + + self.det_times = utility.Timer() def order_points_clockwise(self, pts): """ @@ -155,6 +158,8 @@ class TextDetector(object): def __call__(self, img): ori_im = img.copy() data = {'image': img} + self.det_times.total_time.start() + self.det_times.preprocess_time.start() data = transform(data, self.preprocess_op) img, shape_list = data if img is None: @@ -162,7 +167,9 @@ class TextDetector(object): img = np.expand_dims(img, axis=0) shape_list = np.expand_dims(shape_list, axis=0) img = img.copy() - starttime = time.time() + + self.det_times.preprocess_time.end() + self.det_times.inference_time.start() self.input_tensor.copy_from_cpu(img) self.predictor.run() @@ -170,6 +177,7 @@ class TextDetector(object): for output_tensor in self.output_tensors: output = output_tensor.copy_to_cpu() outputs.append(output) + self.det_times.inference_time.end() preds = {} if self.det_algorithm == "EAST": @@ -184,6 +192,9 @@ class TextDetector(object): preds['maps'] = outputs[0] else: raise NotImplementedError + + self.det_times.postprocess_time.start() + self.predictor.try_shrink_memory() post_result = self.postprocess_op(preds, shape_list) dt_boxes = post_result[0]['points'] @@ -191,8 +202,11 @@ class TextDetector(object): dt_boxes = self.filter_tag_det_res_only_clip(dt_boxes, ori_im.shape) else: dt_boxes = self.filter_tag_det_res(dt_boxes, ori_im.shape) - elapse = time.time() - starttime - return dt_boxes, elapse + + self.det_times.postprocess_time.end() + self.det_times.total_time.end() + self.det_times.img_num += 1 + return dt_boxes, self.det_times.total_time.value() if __name__ == "__main__": @@ -202,6 +216,13 @@ if __name__ == "__main__": count = 0 total_time = 0 draw_img_save = "./inference_results" + cpu_mem, gpu_mem, gpu_util = 0, 0, 0 + + # warmup 10 times + fake_img = np.random.uniform(-1, 1, [640, 640, 3]).astype(np.float32) + for i in range(10): + dt_boxes, _ = text_detector(fake_img) + if not os.path.exists(draw_img_save): os.makedirs(draw_img_save) for image_file in image_file_list: @@ -211,16 +232,56 @@ if __name__ == "__main__": if img is None: logger.info("error in loading image:{}".format(image_file)) continue - dt_boxes, elapse = text_detector(img) + st = time.time() + dt_boxes, _ = text_detector(img) + elapse = time.time() - st if count > 0: total_time += elapse count += 1 + + if args.benchmark: + cm, gm, gu = utility.get_current_memory_mb(0) + cpu_mem += cm + gpu_mem += gm + gpu_util += gu + logger.info("Predict time of {}: {}".format(image_file, elapse)) src_im = utility.draw_text_det_res(dt_boxes, image_file) img_name_pure = os.path.split(image_file)[-1] img_path = os.path.join(draw_img_save, "det_res_{}".format(img_name_pure)) - cv2.imwrite(img_path, src_im) + logger.info("The visualized image saved in {}".format(img_path)) - if count > 1: - logger.info("Avg Time: {}".format(total_time / (count - 1))) + # print the information about memory and time-spent + if args.benchmark: + mems = { + 'cpu_rss_mb': cpu_mem / count, + 'gpu_rss_mb': gpu_mem / count, + 'gpu_util': gpu_util * 100 / count + } + else: + mems = None + logger.info("The predict time about detection module is as follows: ") + det_time_dict = text_detector.det_times.report(average=True) + det_model_name = args.det_model_dir + + if args.benchmark: + # construct log information + model_info = { + 'model_name': args.det_model_dir.split('/')[-1], + 'precision': args.precision + } + data_info = { + 'batch_size': 1, + 'shape': 'dynamic_shape', + 'data_num': det_time_dict['img_num'] + } + perf_info = { + 'preprocess_time_s': det_time_dict['preprocess_time'], + 'inference_time_s': det_time_dict['inference_time'], + 'postprocess_time_s': det_time_dict['postprocess_time'], + 'total_time_s': det_time_dict['total_time'] + } + benchmark_log = benchmark_utils.PaddleInferBenchmark( + text_detector.config, model_info, data_info, perf_info, mems) + benchmark_log("Det") diff --git a/tools/infer/predict_rec.py b/tools/infer/predict_rec.py index 24388026..2eeb39b2 100755 --- a/tools/infer/predict_rec.py +++ b/tools/infer/predict_rec.py @@ -28,6 +28,7 @@ import traceback import paddle import tools.infer.utility as utility +import tools.infer.benchmark_utils as benchmark_utils from ppocr.postprocess import build_post_process from ppocr.utils.logging import get_logger from ppocr.utils.utility import get_image_file_list, check_and_read_gif @@ -41,7 +42,6 @@ class TextRecognizer(object): self.character_type = args.rec_char_type self.rec_batch_num = args.rec_batch_num self.rec_algorithm = args.rec_algorithm - self.max_text_length = args.max_text_length postprocess_params = { 'name': 'CTCLabelDecode', "character_type": args.rec_char_type, @@ -63,9 +63,11 @@ class TextRecognizer(object): "use_space_char": args.use_space_char } self.postprocess_op = build_post_process(postprocess_params) - self.predictor, self.input_tensor, self.output_tensors = \ + self.predictor, self.input_tensor, self.output_tensors, self.config = \ utility.create_predictor(args, 'rec', logger) + self.rec_times = utility.Timer() + def resize_norm_img(self, img, max_wh_ratio): imgC, imgH, imgW = self.rec_image_shape assert imgC == img.shape[2] @@ -166,17 +168,15 @@ class TextRecognizer(object): width_list.append(img.shape[1] / float(img.shape[0])) # Sorting can speed up the recognition process indices = np.argsort(np.array(width_list)) - - # rec_res = [] + self.rec_times.total_time.start() rec_res = [['', 0.0]] * img_num batch_num = self.rec_batch_num - elapse = 0 for beg_img_no in range(0, img_num, batch_num): end_img_no = min(img_num, beg_img_no + batch_num) norm_img_batch = [] max_wh_ratio = 0 + self.rec_times.preprocess_time.start() for ino in range(beg_img_no, end_img_no): - # h, w = img_list[ino].shape[0:2] h, w = img_list[indices[ino]].shape[0:2] wh_ratio = w * 1.0 / h max_wh_ratio = max(max_wh_ratio, wh_ratio) @@ -187,9 +187,8 @@ class TextRecognizer(object): norm_img = norm_img[np.newaxis, :] norm_img_batch.append(norm_img) else: - norm_img = self.process_image_srn(img_list[indices[ino]], - self.rec_image_shape, 8, - self.max_text_length) + norm_img = self.process_image_srn( + img_list[indices[ino]], self.rec_image_shape, 8, 25) encoder_word_pos_list = [] gsrm_word_pos_list = [] gsrm_slf_attn_bias1_list = [] @@ -203,7 +202,6 @@ class TextRecognizer(object): norm_img_batch = norm_img_batch.copy() if self.rec_algorithm == "SRN": - starttime = time.time() encoder_word_pos_list = np.concatenate(encoder_word_pos_list) gsrm_word_pos_list = np.concatenate(gsrm_word_pos_list) gsrm_slf_attn_bias1_list = np.concatenate( @@ -218,19 +216,23 @@ class TextRecognizer(object): gsrm_slf_attn_bias1_list, gsrm_slf_attn_bias2_list, ] + self.rec_times.preprocess_time.end() + self.rec_times.inference_time.start() input_names = self.predictor.get_input_names() for i in range(len(input_names)): input_tensor = self.predictor.get_input_handle(input_names[ i]) input_tensor.copy_from_cpu(inputs[i]) self.predictor.run() + self.rec_times.inference_time.end() outputs = [] for output_tensor in self.output_tensors: output = output_tensor.copy_to_cpu() outputs.append(output) preds = {"predict": outputs[2]} else: - starttime = time.time() + self.rec_times.preprocess_time.end() + self.rec_times.inference_time.start() self.input_tensor.copy_from_cpu(norm_img_batch) self.predictor.run() @@ -239,22 +241,31 @@ class TextRecognizer(object): output = output_tensor.copy_to_cpu() outputs.append(output) preds = outputs[0] - self.predictor.try_shrink_memory() + self.rec_times.inference_time.end() + self.rec_times.postprocess_time.start() rec_result = self.postprocess_op(preds) for rno in range(len(rec_result)): rec_res[indices[beg_img_no + rno]] = rec_result[rno] - elapse += time.time() - starttime - return rec_res, elapse + self.rec_times.postprocess_time.end() + self.rec_times.img_num += int(norm_img_batch.shape[0]) + self.rec_times.total_time.end() + return rec_res, self.rec_times.total_time.value() def main(args): image_file_list = get_image_file_list(args.image_dir) text_recognizer = TextRecognizer(args) - total_run_time = 0.0 - total_images_num = 0 valid_image_file_list = [] img_list = [] - for idx, image_file in enumerate(image_file_list): + cpu_mem, gpu_mem, gpu_util = 0, 0, 0 + count = 0 + + # warmup 10 times + fake_img = np.random.uniform(-1, 1, [1, 32, 320, 3]).astype(np.float32) + for i in range(10): + dt_boxes, _ = text_recognizer(fake_img) + + for image_file in image_file_list: img, flag = check_and_read_gif(image_file) if not flag: img = cv2.imread(image_file) @@ -263,29 +274,54 @@ def main(args): continue valid_image_file_list.append(image_file) img_list.append(img) - if len(img_list) >= args.rec_batch_num or idx == len( - image_file_list) - 1: - try: - rec_res, predict_time = text_recognizer(img_list) - total_run_time += predict_time - except: - logger.info(traceback.format_exc()) - logger.info( - "ERROR!!!! \n" - "Please read the FAQ:https://github.com/PaddlePaddle/PaddleOCR#faq \n" - "If your model has tps module: " - "TPS does not support variable shape.\n" - "Please set --rec_image_shape='3,32,100' and --rec_char_type='en' " - ) - exit() - for ino in range(len(img_list)): - logger.info("Predicts of {}:{}".format(valid_image_file_list[ - ino], rec_res[ino])) - total_images_num += len(valid_image_file_list) - valid_image_file_list = [] - img_list = [] - logger.info("Total predict time for {} images, cost: {:.3f}".format( - total_images_num, total_run_time)) + try: + rec_res, _ = text_recognizer(img_list) + if args.benchmark: + cm, gm, gu = utility.get_current_memory_mb(0) + cpu_mem += cm + gpu_mem += gm + gpu_util += gu + count += 1 + + except Exception as E: + logger.info(traceback.format_exc()) + logger.info(E) + exit() + for ino in range(len(img_list)): + logger.info("Predicts of {}:{}".format(valid_image_file_list[ino], + rec_res[ino])) + if args.benchmark: + mems = { + 'cpu_rss_mb': cpu_mem / count, + 'gpu_rss_mb': gpu_mem / count, + 'gpu_util': gpu_util * 100 / count + } + else: + mems = None + logger.info("The predict time about recognizer module is as follows: ") + rec_time_dict = text_recognizer.rec_times.report(average=True) + rec_model_name = args.rec_model_dir + + if args.benchmark: + # construct log information + model_info = { + 'model_name': args.rec_model_dir.split('/')[-1], + 'precision': args.precision + } + data_info = { + 'batch_size': args.rec_batch_num, + 'shape': 'dynamic_shape', + 'data_num': rec_time_dict['img_num'] + } + perf_info = { + 'preprocess_time_s': rec_time_dict['preprocess_time'], + 'inference_time_s': rec_time_dict['inference_time'], + 'postprocess_time_s': rec_time_dict['postprocess_time'], + 'total_time_s': rec_time_dict['total_time'] + } + benchmark_log = benchmark_utils.PaddleInferBenchmark( + text_recognizer.config, model_info, data_info, perf_info, mems) + benchmark_log("Rec") if __name__ == "__main__": diff --git a/tools/infer/predict_system.py b/tools/infer/predict_system.py index ba81aff0..391779e6 100755 --- a/tools/infer/predict_system.py +++ b/tools/infer/predict_system.py @@ -13,7 +13,6 @@ # limitations under the License. import os import sys -import subprocess __dir__ = os.path.dirname(os.path.abspath(__file__)) sys.path.append(__dir__) @@ -32,8 +31,8 @@ import tools.infer.predict_det as predict_det import tools.infer.predict_cls as predict_cls from ppocr.utils.utility import get_image_file_list, check_and_read_gif from ppocr.utils.logging import get_logger -from tools.infer.utility import draw_ocr_box_txt - +from tools.infer.utility import draw_ocr_box_txt, get_current_memory_mb +import tools.infer.benchmark_utils as benchmark_utils logger = get_logger() @@ -88,8 +87,7 @@ class TextSystem(object): def __call__(self, img): ori_im = img.copy() dt_boxes, elapse = self.text_detector(img) - logger.info("dt_boxes num : {}, elapse : {}".format( - len(dt_boxes), elapse)) + if dt_boxes is None: return None, None img_crop_list = [] @@ -103,13 +101,9 @@ class TextSystem(object): if self.use_angle_cls: img_crop_list, angle_list, elapse = self.text_classifier( img_crop_list) - logger.info("cls num : {}, elapse : {}".format( - len(img_crop_list), elapse)) rec_res, elapse = self.text_recognizer(img_crop_list) - logger.info("rec_res num : {}, elapse : {}".format( - len(rec_res), elapse)) - # self.print_draw_crop_rec_res(img_crop_list, rec_res) + filter_boxes, filter_rec_res = [], [] for box, rec_reuslt in zip(dt_boxes, rec_res): text, score = rec_reuslt @@ -142,12 +136,15 @@ def sorted_boxes(dt_boxes): def main(args): image_file_list = get_image_file_list(args.image_dir) - image_file_list = image_file_list[args.process_id::args.total_process_num] text_sys = TextSystem(args) is_visualize = True font_path = args.vis_font_path drop_score = args.drop_score - for image_file in image_file_list: + total_time = 0 + cpu_mem, gpu_mem, gpu_util = 0, 0, 0 + _st = time.time() + count = 0 + for idx, image_file in enumerate(image_file_list): img, flag = check_and_read_gif(image_file) if not flag: img = cv2.imread(image_file) @@ -157,8 +154,16 @@ def main(args): starttime = time.time() dt_boxes, rec_res = text_sys(img) elapse = time.time() - starttime - logger.info("Predict time of %s: %.3fs" % (image_file, elapse)) + total_time += elapse + if args.benchmark and idx % 20 == 0: + cm, gm, gu = get_current_memory_mb(0) + cpu_mem += cm + gpu_mem += gm + gpu_util += gu + count += 1 + logger.info( + str(idx) + " Predict time of %s: %.3fs" % (image_file, elapse)) for text, score in rec_res: logger.info("{}, {:.3f}".format(text, score)) @@ -178,26 +183,74 @@ def main(args): draw_img_save = "./inference_results/" if not os.path.exists(draw_img_save): os.makedirs(draw_img_save) + if flag: + image_file = image_file[:-3] + "png" cv2.imwrite( os.path.join(draw_img_save, os.path.basename(image_file)), draw_img[:, :, ::-1]) logger.info("The visualized image saved in {}".format( os.path.join(draw_img_save, os.path.basename(image_file)))) + logger.info("The predict total time is {}".format(time.time() - _st)) + logger.info("\nThe predict total time is {}".format(total_time)) + + img_num = text_sys.text_detector.det_times.img_num + if args.benchmark: + mems = { + 'cpu_rss_mb': cpu_mem / count, + 'gpu_rss_mb': gpu_mem / count, + 'gpu_util': gpu_util * 100 / count + } + else: + mems = None + det_time_dict = text_sys.text_detector.det_times.report(average=True) + rec_time_dict = text_sys.text_recognizer.rec_times.report(average=True) + det_model_name = args.det_model_dir + rec_model_name = args.rec_model_dir + + # construct det log information + model_info = { + 'model_name': args.det_model_dir.split('/')[-1], + 'precision': args.precision + } + data_info = { + 'batch_size': 1, + 'shape': 'dynamic_shape', + 'data_num': det_time_dict['img_num'] + } + perf_info = { + 'preprocess_time_s': det_time_dict['preprocess_time'], + 'inference_time_s': det_time_dict['inference_time'], + 'postprocess_time_s': det_time_dict['postprocess_time'], + 'total_time_s': det_time_dict['total_time'] + } + + benchmark_log = benchmark_utils.PaddleInferBenchmark( + text_sys.text_detector.config, model_info, data_info, perf_info, mems, + args.save_log_path) + benchmark_log("Det") + + # construct rec log information + model_info = { + 'model_name': args.rec_model_dir.split('/')[-1], + 'precision': args.precision + } + data_info = { + 'batch_size': args.rec_batch_num, + 'shape': 'dynamic_shape', + 'data_num': rec_time_dict['img_num'] + } + perf_info = { + 'preprocess_time_s': rec_time_dict['preprocess_time'], + 'inference_time_s': rec_time_dict['inference_time'], + 'postprocess_time_s': rec_time_dict['postprocess_time'], + 'total_time_s': rec_time_dict['total_time'] + } + benchmark_log = benchmark_utils.PaddleInferBenchmark( + text_sys.text_recognizer.config, model_info, data_info, perf_info, mems, + args.save_log_path) + benchmark_log("Rec") + if __name__ == "__main__": - args = utility.parse_args() - if args.use_mp: - p_list = [] - total_process_num = args.total_process_num - for process_id in range(total_process_num): - cmd = [sys.executable, "-u"] + sys.argv + [ - "--process_id={}".format(process_id), - "--use_mp={}".format(False) - ] - p = subprocess.Popen(cmd, stdout=sys.stdout, stderr=sys.stdout) - p_list.append(p) - for p in p_list: - p.wait() - else: - main(args) + main(utility.parse_args()) diff --git a/tools/infer/utility.py b/tools/infer/utility.py index b5fe3ba9..5de4e69a 100755 --- a/tools/infer/utility.py +++ b/tools/infer/utility.py @@ -21,6 +21,9 @@ import json from PIL import Image, ImageDraw, ImageFont import math from paddle import inference +import time +from ppocr.utils.logging import get_logger +logger = get_logger() def parse_args(): @@ -32,7 +35,7 @@ def parse_args(): parser.add_argument("--use_gpu", type=str2bool, default=True) parser.add_argument("--ir_optim", type=str2bool, default=True) parser.add_argument("--use_tensorrt", type=str2bool, default=False) - parser.add_argument("--use_fp16", type=str2bool, default=False) + parser.add_argument("--precision", type=str, default="fp32") parser.add_argument("--gpu_mem", type=int, default=500) # params for text detector @@ -98,15 +101,88 @@ def parse_args(): parser.add_argument("--cls_thresh", type=float, default=0.9) parser.add_argument("--enable_mkldnn", type=str2bool, default=False) + parser.add_argument("--cpu_threads", type=int, default=10) parser.add_argument("--use_pdserving", type=str2bool, default=False) parser.add_argument("--use_mp", type=str2bool, default=False) parser.add_argument("--total_process_num", type=int, default=1) parser.add_argument("--process_id", type=int, default=0) + parser.add_argument("--benchmark", type=bool, default=False) + parser.add_argument("--save_log_path", type=str, default="./log_output/") return parser.parse_args() +class Times(object): + def __init__(self): + self.time = 0. + self.st = 0. + self.et = 0. + + def start(self): + self.st = time.time() + + def end(self, accumulative=True): + self.et = time.time() + if accumulative: + self.time += self.et - self.st + else: + self.time = self.et - self.st + + def reset(self): + self.time = 0. + self.st = 0. + self.et = 0. + + def value(self): + return round(self.time, 4) + + +class Timer(Times): + def __init__(self): + super(Timer, self).__init__() + self.total_time = Times() + self.preprocess_time = Times() + self.inference_time = Times() + self.postprocess_time = Times() + self.img_num = 0 + + def info(self, average=False): + logger.info("----------------------- Perf info -----------------------") + logger.info("total_time: {}, img_num: {}".format(self.total_time.value( + ), self.img_num)) + preprocess_time = round(self.preprocess_time.value() / self.img_num, + 4) if average else self.preprocess_time.value() + postprocess_time = round( + self.postprocess_time.value() / self.img_num, + 4) if average else self.postprocess_time.value() + inference_time = round(self.inference_time.value() / self.img_num, + 4) if average else self.inference_time.value() + + average_latency = self.total_time.value() / self.img_num + logger.info("average_latency(ms): {:.2f}, QPS: {:2f}".format( + average_latency * 1000, 1 / average_latency)) + logger.info( + "preprocess_latency(ms): {:.2f}, inference_latency(ms): {:.2f}, postprocess_latency(ms): {:.2f}". + format(preprocess_time * 1000, inference_time * 1000, + postprocess_time * 1000)) + + def report(self, average=False): + dic = {} + dic['preprocess_time'] = round( + self.preprocess_time.value() / self.img_num, + 4) if average else self.preprocess_time.value() + dic['postprocess_time'] = round( + self.postprocess_time.value() / self.img_num, + 4) if average else self.postprocess_time.value() + dic['inference_time'] = round( + self.inference_time.value() / self.img_num, + 4) if average else self.inference_time.value() + dic['img_num'] = self.img_num + dic['total_time'] = round(self.total_time.value(), 4) + return dic + + def create_predictor(args, mode, logger): if mode == "det": model_dir = args.det_model_dir @@ -131,6 +207,16 @@ def create_predictor(args, mode, logger): config = inference.Config(model_file_path, params_file_path) + if hasattr(args, 'precision'): + if args.precision == "fp16" and args.use_tensorrt: + precision = inference.PrecisionType.Half + elif args.precision == "int8": + precision = inference.PrecisionType.Int8 + else: + precision = inference.PrecisionType.Float32 + else: + precision = inference.PrecisionType.Float32 + if args.use_gpu: config.enable_use_gpu(args.gpu_mem, 0) if args.use_tensorrt: @@ -140,7 +226,10 @@ def create_predictor(args, mode, logger): max_batch_size=args.max_batch_size) else: config.disable_gpu() - config.set_cpu_math_library_num_threads(6) + if hasattr(args, "cpu_threads"): + config.set_cpu_math_library_num_threads(args.cpu_threads) + else: + config.set_cpu_math_library_num_threads(10) if args.enable_mkldnn: # cache 10 different shapes for mkldnn to avoid memory leak config.set_mkldnn_cache_capacity(10) @@ -166,7 +255,7 @@ def create_predictor(args, mode, logger): for output_name in output_names: output_tensor = predictor.get_output_handle(output_name) output_tensors.append(output_tensor) - return predictor, input_tensor, output_tensors + return predictor, input_tensor, output_tensors, config def draw_e2e_res(dt_boxes, strs, img_path): @@ -417,6 +506,31 @@ def draw_boxes(image, boxes, scores=None, drop_score=0.5): return image +def get_current_memory_mb(gpu_id=None): + """ + It is used to Obtain the memory usage of the CPU and GPU during the running of the program. + And this function Current program is time-consuming. + """ + import pynvml + import psutil + import GPUtil + pid = os.getpid() + p = psutil.Process(pid) + info = p.memory_full_info() + cpu_mem = info.uss / 1024. / 1024. + gpu_mem = 0 + gpu_percent = 0 + if gpu_id is not None: + GPUs = GPUtil.getGPUs() + gpu_load = GPUs[gpu_id].load + gpu_percent = gpu_load + pynvml.nvmlInit() + handle = pynvml.nvmlDeviceGetHandleByIndex(0) + meminfo = pynvml.nvmlDeviceGetMemoryInfo(handle) + gpu_mem = meminfo.used / 1024. / 1024. + return round(cpu_mem, 4), round(gpu_mem, 4), round(gpu_percent, 4) + + if __name__ == '__main__': test_img = "./doc/test_v2" predict_txt = "./doc/predict.txt" From 794362481e7d238bb08e719de1f2357db68faf1c Mon Sep 17 00:00:00 2001 From: WenmuZhou Date: Thu, 3 Jun 2021 16:39:31 +0800 Subject: [PATCH 02/29] add table eval and predict script --- ppocr/postprocess/rec_postprocess.py | 175 +- ppocr/utils/dict/table_dict.txt | 278 ++ ppocr/utils/dict/table_structure_dict.txt | 2759 ++++++++++++++++++++ ppocr/utils/table_utils/matcher.py | 214 ++ ppstructure/predict_system.py | 123 + ppstructure/table/__init__.py | 13 + ppstructure/table/eval_table.py | 67 + ppstructure/table/predict_structure.py | 141 + ppstructure/table/predict_table.py | 222 ++ ppstructure/table/table_metric/__init__.py | 16 + ppstructure/table/table_metric/parallel.py | 51 + tools/infer/utility.py | 5 +- 12 files changed, 4060 insertions(+), 4 deletions(-) create mode 100644 ppocr/utils/dict/table_dict.txt create mode 100644 ppocr/utils/dict/table_structure_dict.txt create mode 100755 ppocr/utils/table_utils/matcher.py create mode 100644 ppstructure/table/__init__.py create mode 100755 ppstructure/table/eval_table.py create mode 100755 ppstructure/table/predict_structure.py create mode 100644 ppstructure/table/predict_table.py create mode 100755 ppstructure/table/table_metric/__init__.py create mode 100755 ppstructure/table/table_metric/parallel.py diff --git a/ppocr/postprocess/rec_postprocess.py b/ppocr/postprocess/rec_postprocess.py index d353391c..594197a6 100644 --- a/ppocr/postprocess/rec_postprocess.py +++ b/ppocr/postprocess/rec_postprocess.py @@ -44,16 +44,16 @@ class BaseRecLabelDecode(object): self.character_str = string.printable[:-6] dict_character = list(self.character_str) elif character_type in support_character_type: - self.character_str = "" + self.character_str = [] assert character_dict_path is not None, "character_dict_path should not be None when character_type is {}".format( character_type) with open(character_dict_path, "rb") as fin: lines = fin.readlines() for line in lines: line = line.decode('utf-8').strip("\n").strip("\r\n") - self.character_str += line + self.character_str.append(line) if use_space_char: - self.character_str += " " + self.character_str.append(" ") dict_character = list(self.character_str) else: @@ -288,3 +288,172 @@ class SRNLabelDecode(BaseRecLabelDecode): assert False, "unsupport type %s in get_beg_end_flag_idx" \ % beg_or_end return idx + + +class TableLabelDecode(object): + """ """ + + def __init__(self, + max_text_length, + max_elem_length, + max_cell_num, + character_dict_path, + **kwargs): + self.max_text_length = max_text_length + self.max_elem_length = max_elem_length + self.max_cell_num = max_cell_num + list_character, list_elem = self.load_char_elem_dict(character_dict_path) + list_character = self.add_special_char(list_character) + list_elem = self.add_special_char(list_elem) + self.dict_character = {} + self.dict_idx_character = {} + for i, char in enumerate(list_character): + self.dict_idx_character[i] = char + self.dict_character[char] = i + self.dict_elem = {} + self.dict_idx_elem = {} + for i, elem in enumerate(list_elem): + self.dict_idx_elem[i] = elem + self.dict_elem[elem] = i + + def load_char_elem_dict(self, character_dict_path): + list_character = [] + list_elem = [] + with open(character_dict_path, "rb") as fin: + lines = fin.readlines() + substr = lines[0].decode('utf-8').strip("\n").split("\t") + character_num = int(substr[0]) + elem_num = int(substr[1]) + for cno in range(1, 1 + character_num): + character = lines[cno].decode('utf-8').strip("\n") + list_character.append(character) + for eno in range(1 + character_num, 1 + character_num + elem_num): + elem = lines[eno].decode('utf-8').strip("\n") + list_elem.append(elem) + return list_character, list_elem + + def add_special_char(self, list_character): + self.beg_str = "sos" + self.end_str = "eos" + list_character = [self.beg_str] + list_character + [self.end_str] + return list_character + + def get_sp_tokens(self): + char_beg_idx = self.get_beg_end_flag_idx('beg', 'char') + char_end_idx = self.get_beg_end_flag_idx('end', 'char') + elem_beg_idx = self.get_beg_end_flag_idx('beg', 'elem') + elem_end_idx = self.get_beg_end_flag_idx('end', 'elem') + elem_char_idx1 = self.dict_elem[''] + elem_char_idx2 = self.dict_elem['', '' or elem == ''\ + # # or 'rowspan' in elem or 'colspan' in elem: + # if elem == '' or elem == '': + # select_td_tokens.append(self.dict_elem[elem]) + # if 'rowspan' in elem or 'colspan' in elem: + # select_span_tokens.append(self.dict_elem[elem]) + result_list = [] + result_pos_list = [] + result_score_list = [] + result_elem_idx_list = [] + batch_size = len(text_index) + for batch_idx in range(batch_size): + char_list = [] + elem_pos_list = [] + elem_idx_list = [] + score_list = [] + for idx in range(len(text_index[batch_idx])): + tmp_elem_idx = int(text_index[batch_idx][idx]) + if idx > 0 and tmp_elem_idx == end_idx: + break + if tmp_elem_idx in ignored_tokens: + continue + # if tmp_elem_idx in select_td_tokens: + # total_td_score += structure_probs[batch_idx, idx] + # total_td_num += 1 + # if tmp_elem_idx in select_span_tokens: + # total_span_score += structure_probs[batch_idx, idx] + # total_span_num += 1 + char_list.append(current_dict[tmp_elem_idx]) + elem_pos_list.append(idx) + score_list.append(structure_probs[batch_idx, idx]) + elem_idx_list.append(tmp_elem_idx) + result_list.append(char_list) + result_pos_list.append(elem_pos_list) + result_score_list.append(score_list) + result_elem_idx_list.append(elem_idx_list) + return result_list, result_pos_list, result_score_list, result_elem_idx_list + + def get_ignored_tokens(self, char_or_elem): + beg_idx = self.get_beg_end_flag_idx("beg", char_or_elem) + end_idx = self.get_beg_end_flag_idx("end", char_or_elem) + return [beg_idx, end_idx] + + def get_beg_end_flag_idx(self, beg_or_end, char_or_elem): + if char_or_elem == "char": + if beg_or_end == "beg": + idx = self.dict_character[self.beg_str] + elif beg_or_end == "end": + idx = self.dict_character[self.end_str] + else: + assert False, "Unsupport type %s in get_beg_end_flag_idx of char" \ + % beg_or_end + elif char_or_elem == "elem": + if beg_or_end == "beg": + idx = self.dict_elem[self.beg_str] + elif beg_or_end == "end": + idx = self.dict_elem[self.end_str] + else: + assert False, "Unsupport type %s in get_beg_end_flag_idx of elem" \ + % beg_or_end + else: + assert False, "Unsupport type %s in char_or_elem" \ + % char_or_elem + return idx diff --git a/ppocr/utils/dict/table_dict.txt b/ppocr/utils/dict/table_dict.txt new file mode 100644 index 00000000..804f3e31 --- /dev/null +++ b/ppocr/utils/dict/table_dict.txt @@ -0,0 +1,278 @@ +← + +☆ +─ +α + + +⋅ +$ +ω +ψ +χ +( +υ +≥ +σ +, +ρ +ε +0 +■ +4 +8 +✗ +b +< +✓ +Ψ +Ω +€ +D +3 +Π +H +║ + +L +Φ +Χ +θ +P +κ +λ +μ +T +ξ +X +β +γ +δ +\ +ζ +η +` +d + +h +f +l +Θ +p +√ +t + +x +Β +Γ +Δ +| +ǂ +ɛ +j +̧ +➢ +⁡ +̌ +′ +« +△ +▲ +# + +' +Ι ++ +¶ +/ +▼ +⇑ +□ +· +7 +▪ +; +? +➔ +∩ +C +÷ +G +⇒ +K + +O +S +С +W +Α +[ +○ +_ +● +‡ +c +z +g + +o + +〈 +〉 +s +⩽ +w +φ +ʹ +{ +» +∣ +̆ +e +ˆ +∈ +τ +◆ +ι +∅ +∆ +∙ +∘ +Ø +ß +✔ +∞ +∑ +− +× +◊ +∗ +∖ +˃ +˂ +∫ +" +i +& +π +↔ +* +∥ +æ +∧ +. +⁄ +ø +Q +∼ +6 +⁎ +: +★ +> +a +B +≈ +F +J +̄ +N +♯ +R +V + +― +Z +♣ +^ +¤ +¥ +§ + +¢ +£ +≦ +­ +≤ +‖ +Λ +© +n +↓ +→ +↑ +r +° +± +v + +♂ +k +♀ +~ +ᅟ +̇ +@ +” +♦ +ł +® +⊕ +„ +! + +% +⇓ +) +- +1 +5 +9 += +А +A +‰ +⋆ +Σ +E +◦ +I +※ +M +m +̨ +⩾ +† + +• +U +Y +
 +] +̸ +2 +‐ +– +‒ +̂ +— +̀ +́ +’ +‘ +⋮ +⋯ +̊ +“ +̈ +≧ +q +u +ı +y + +​ +̃ +} +ν diff --git a/ppocr/utils/dict/table_structure_dict.txt b/ppocr/utils/dict/table_structure_dict.txt new file mode 100644 index 00000000..9c4531e5 --- /dev/null +++ b/ppocr/utils/dict/table_structure_dict.txt @@ -0,0 +1,2759 @@ +277 28 1267 1186 + +V +a +r +i +b +l +e + +H +z +d + +t +o +9 +5 +% +C +I + +p + +v +u +* +A +g +( +m +n +) +0 +. +7 +1 +6 +≤ +> +8 +3 +– +2 +G +4 +M +F +T +y +f +s +L +w +c +U +h +D +S +Q +R +x +P +- +E +O +/ +k +, ++ +N +K +q +′ +[ +] +< +≥ + +− + +μ +± +J +j +W +_ +Δ +B +“ +: +Y +α +λ +; + + +? +∼ += +° +# +̊ +̈ +̂ +’ +Z +X +∗ +— +β +' +† +~ +@ +" +γ +↓ +↑ +& +‡ +χ +” +σ +§ +| +¶ +‐ +× +$ +→ +√ +✓ +‘ +\ +∞ +π +• +® +^ +∆ +≧ + + +́ +♀ +♂ +‒ +⁎ +▲ +· +£ +φ +Ψ +ß +△ +☆ +▪ +η +€ +∧ +̃ +Φ +ρ +̄ +δ +‰ +̧ +Ω +♦ +{ +} +̀ +∑ +∫ +ø +κ +ε +¥ +※ +` +ω +Σ +➔ +‖ +Β +̸ +
 +─ +● +⩾ +Χ +Α +⋅ +◆ +★ +■ +ψ +ǂ +□ +ζ +! +Γ +↔ +θ +⁄ +〈 +〉 +― +υ +τ +⋆ +Ø +© +∥ +С +˂ +➢ +ɛ +⁡ +✗ +← +○ +¢ +⩽ +∖ +˃ +­ +≈ +Π +̌ +≦ +∅ +ᅟ + + +∣ +¤ +♯ +̆ +ξ +÷ +▼ + +ι +ν +║ + + +◦ +​ +◊ +∙ +« +» +ł +ı +Θ +∈ +„ +∘ +✔ +̇ +æ +ʹ +ˆ +♣ +⇓ +∩ +⊕ +⇒ +⇑ +̨ +Ι +Λ +⋯ +А +⋮ + + + + + + + + + + colspan="2" + colspan="3" + rowspan="2" + colspan="4" + colspan="6" + rowspan="3" + colspan="9" + colspan="10" + colspan="7" + rowspan="4" + rowspan="5" + rowspan="9" + colspan="8" + rowspan="8" + rowspan="6" + rowspan="7" + rowspan="10" +0 2924682 +1 3405345 +2 2363468 +3 2709165 +4 4078680 +5 3250792 +6 1923159 +7 1617890 +8 1450532 +9 1717624 +10 1477550 +11 1489223 +12 915528 +13 819193 +14 593660 +15 518924 +16 682065 +17 494584 +18 400591 +19 396421 +20 340994 +21 280688 +22 250328 +23 226786 +24 199927 +25 182707 +26 164629 +27 141613 +28 127554 +29 116286 +30 107682 +31 96367 +32 88002 +33 79234 +34 72186 +35 65921 +36 60374 +37 55976 +38 52166 +39 47414 +40 44932 +41 41279 +42 38232 +43 35463 +44 33703 +45 30557 +46 29639 +47 27000 +48 25447 +49 23186 +50 22093 +51 20412 +52 19844 +53 18261 +54 17561 +55 16499 +56 15597 +57 14558 +58 14372 +59 13445 +60 13514 +61 12058 +62 11145 +63 10767 +64 10370 +65 9630 +66 9337 +67 8881 +68 8727 +69 8060 +70 7994 +71 7740 +72 7189 +73 6729 +74 6749 +75 6548 +76 6321 +77 5957 +78 5740 +79 5407 +80 5370 +81 5035 +82 4921 +83 4656 +84 4600 +85 4519 +86 4277 +87 4023 +88 3939 +89 3910 +90 3861 +91 3560 +92 3483 +93 3406 +94 3346 +95 3229 +96 3122 +97 3086 +98 3001 +99 2884 +100 2822 +101 2677 +102 2670 +103 2610 +104 2452 +105 2446 +106 2400 +107 2300 +108 2316 +109 2196 +110 2089 +111 2083 +112 2041 +113 1881 +114 1838 +115 1896 +116 1795 +117 1786 +118 1743 +119 1765 +120 1750 +121 1683 +122 1563 +123 1499 +124 1513 +125 1462 +126 1388 +127 1441 +128 1417 +129 1392 +130 1306 +131 1321 +132 1274 +133 1294 +134 1240 +135 1126 +136 1157 +137 1130 +138 1084 +139 1130 +140 1083 +141 1040 +142 980 +143 1031 +144 974 +145 980 +146 932 +147 898 +148 960 +149 907 +150 852 +151 912 +152 859 +153 847 +154 876 +155 792 +156 791 +157 765 +158 788 +159 787 +160 744 +161 673 +162 683 +163 697 +164 666 +165 680 +166 632 +167 677 +168 657 +169 618 +170 587 +171 585 +172 567 +173 549 +174 562 +175 548 +176 542 +177 539 +178 542 +179 549 +180 547 +181 526 +182 525 +183 514 +184 512 +185 505 +186 515 +187 467 +188 475 +189 458 +190 435 +191 443 +192 427 +193 424 +194 404 +195 389 +196 429 +197 404 +198 386 +199 351 +200 388 +201 408 +202 361 +203 346 +204 324 +205 361 +206 363 +207 364 +208 323 +209 336 +210 342 +211 315 +212 325 +213 328 +214 314 +215 327 +216 320 +217 300 +218 295 +219 315 +220 310 +221 295 +222 275 +223 248 +224 274 +225 232 +226 293 +227 259 +228 286 +229 263 +230 242 +231 214 +232 261 +233 231 +234 211 +235 250 +236 233 +237 206 +238 224 +239 210 +240 233 +241 223 +242 216 +243 222 +244 207 +245 212 +246 196 +247 205 +248 201 +249 202 +250 211 +251 201 +252 215 +253 179 +254 163 +255 179 +256 191 +257 188 +258 196 +259 150 +260 154 +261 176 +262 211 +263 166 +264 171 +265 165 +266 149 +267 182 +268 159 +269 161 +270 164 +271 161 +272 141 +273 151 +274 127 +275 129 +276 142 +277 158 +278 148 +279 135 +280 127 +281 134 +282 138 +283 131 +284 126 +285 125 +286 130 +287 126 +288 135 +289 125 +290 135 +291 131 +292 95 +293 135 +294 106 +295 117 +296 136 +297 128 +298 128 +299 118 +300 109 +301 112 +302 117 +303 108 +304 120 +305 100 +306 95 +307 108 +308 112 +309 77 +310 120 +311 104 +312 109 +313 89 +314 98 +315 82 +316 98 +317 93 +318 77 +319 93 +320 77 +321 98 +322 93 +323 86 +324 89 +325 73 +326 70 +327 71 +328 77 +329 87 +330 77 +331 93 +332 100 +333 83 +334 72 +335 74 +336 69 +337 77 +338 68 +339 78 +340 90 +341 98 +342 75 +343 80 +344 63 +345 71 +346 83 +347 66 +348 71 +349 70 +350 62 +351 62 +352 59 +353 63 +354 62 +355 52 +356 64 +357 64 +358 56 +359 49 +360 57 +361 63 +362 60 +363 68 +364 62 +365 55 +366 54 +367 40 +368 75 +369 70 +370 53 +371 58 +372 57 +373 55 +374 69 +375 57 +376 53 +377 43 +378 45 +379 47 +380 56 +381 51 +382 59 +383 51 +384 43 +385 34 +386 57 +387 49 +388 39 +389 46 +390 48 +391 43 +392 40 +393 54 +394 50 +395 41 +396 43 +397 33 +398 27 +399 49 +400 44 +401 44 +402 38 +403 30 +404 32 +405 37 +406 39 +407 42 +408 53 +409 39 +410 34 +411 31 +412 32 +413 52 +414 27 +415 41 +416 34 +417 36 +418 50 +419 35 +420 32 +421 33 +422 45 +423 35 +424 40 +425 29 +426 41 +427 40 +428 39 +429 32 +430 31 +431 34 +432 29 +433 27 +434 26 +435 22 +436 34 +437 28 +438 30 +439 38 +440 35 +441 36 +442 36 +443 27 +444 24 +445 33 +446 31 +447 25 +448 33 +449 27 +450 32 +451 46 +452 31 +453 35 +454 35 +455 34 +456 26 +457 21 +458 25 +459 26 +460 24 +461 27 +462 33 +463 30 +464 35 +465 21 +466 32 +467 19 +468 27 +469 16 +470 28 +471 26 +472 27 +473 26 +474 25 +475 25 +476 27 +477 20 +478 28 +479 22 +480 23 +481 16 +482 25 +483 27 +484 19 +485 23 +486 19 +487 15 +488 15 +489 23 +490 24 +491 19 +492 20 +493 18 +494 17 +495 30 +496 28 +497 20 +498 29 +499 17 +500 19 +501 21 +502 15 +503 24 +504 15 +505 19 +506 25 +507 16 +508 23 +509 26 +510 21 +511 15 +512 12 +513 16 +514 18 +515 24 +516 26 +517 18 +518 8 +519 25 +520 14 +521 8 +522 24 +523 20 +524 18 +525 15 +526 13 +527 17 +528 18 +529 22 +530 21 +531 9 +532 16 +533 17 +534 13 +535 17 +536 15 +537 13 +538 20 +539 13 +540 19 +541 29 +542 10 +543 8 +544 18 +545 13 +546 9 +547 18 +548 10 +549 18 +550 18 +551 9 +552 9 +553 15 +554 13 +555 15 +556 14 +557 14 +558 18 +559 8 +560 13 +561 9 +562 7 +563 12 +564 6 +565 9 +566 9 +567 18 +568 9 +569 10 +570 13 +571 14 +572 13 +573 21 +574 8 +575 16 +576 12 +577 9 +578 16 +579 17 +580 22 +581 6 +582 14 +583 13 +584 15 +585 11 +586 13 +587 5 +588 12 +589 13 +590 15 +591 13 +592 15 +593 12 +594 7 +595 18 +596 12 +597 13 +598 13 +599 13 +600 12 +601 12 +602 10 +603 11 +604 6 +605 6 +606 2 +607 9 +608 8 +609 12 +610 9 +611 12 +612 13 +613 12 +614 14 +615 9 +616 8 +617 9 +618 14 +619 13 +620 12 +621 6 +622 8 +623 8 +624 8 +625 12 +626 8 +627 7 +628 5 +629 8 +630 12 +631 6 +632 10 +633 10 +634 7 +635 8 +636 9 +637 6 +638 9 +639 4 +640 12 +641 4 +642 3 +643 11 +644 10 +645 6 +646 12 +647 12 +648 4 +649 4 +650 9 +651 8 +652 6 +653 5 +654 14 +655 10 +656 11 +657 8 +658 5 +659 5 +660 9 +661 13 +662 4 +663 5 +664 9 +665 11 +666 12 +667 7 +668 13 +669 2 +670 1 +671 7 +672 7 +673 7 +674 10 +675 9 +676 6 +677 5 +678 7 +679 6 +680 3 +681 3 +682 4 +683 9 +684 8 +685 5 +686 3 +687 11 +688 9 +689 2 +690 6 +691 5 +692 9 +693 5 +694 6 +695 5 +696 9 +697 8 +698 3 +699 7 +700 5 +701 9 +702 8 +703 7 +704 2 +705 3 +706 7 +707 6 +708 6 +709 10 +710 2 +711 10 +712 6 +713 7 +714 5 +715 6 +716 4 +717 6 +718 8 +719 4 +720 6 +721 7 +722 5 +723 7 +724 3 +725 10 +726 10 +727 3 +728 7 +729 7 +730 5 +731 2 +732 1 +733 5 +734 1 +735 5 +736 6 +737 2 +738 2 +739 3 +740 7 +741 2 +742 7 +743 4 +744 5 +745 4 +746 5 +747 3 +748 1 +749 4 +750 4 +751 2 +752 4 +753 6 +754 6 +755 6 +756 3 +757 2 +758 5 +759 5 +760 3 +761 4 +762 2 +763 1 +764 8 +765 3 +766 4 +767 3 +768 1 +769 5 +770 3 +771 3 +772 4 +773 4 +774 1 +775 3 +776 2 +777 2 +778 3 +779 3 +780 1 +781 4 +782 3 +783 4 +784 6 +785 3 +786 5 +787 4 +788 2 +789 4 +790 5 +791 4 +792 6 +794 4 +795 1 +796 1 +797 4 +798 2 +799 3 +800 3 +801 1 +802 5 +803 5 +804 3 +805 3 +806 3 +807 4 +808 4 +809 2 +811 5 +812 4 +813 6 +814 3 +815 2 +816 2 +817 3 +818 5 +819 3 +820 1 +821 1 +822 4 +823 3 +824 4 +825 8 +826 3 +827 5 +828 5 +829 3 +830 6 +831 3 +832 4 +833 8 +834 5 +835 3 +836 3 +837 2 +838 4 +839 2 +840 1 +841 3 +842 2 +843 1 +844 3 +846 4 +847 4 +848 3 +849 3 +850 2 +851 3 +853 1 +854 4 +855 4 +856 2 +857 4 +858 1 +859 2 +860 5 +861 1 +862 1 +863 4 +864 2 +865 2 +867 5 +868 1 +869 4 +870 1 +871 1 +872 1 +873 2 +875 5 +876 3 +877 1 +878 3 +879 3 +880 3 +881 2 +882 1 +883 6 +884 2 +885 2 +886 1 +887 1 +888 3 +889 2 +890 2 +891 3 +892 1 +893 3 +894 1 +895 5 +896 1 +897 3 +899 2 +900 2 +902 1 +903 2 +904 4 +905 4 +906 3 +907 1 +908 1 +909 2 +910 5 +911 2 +912 3 +914 1 +915 1 +916 2 +918 2 +919 2 +920 4 +921 4 +922 1 +923 1 +924 4 +925 5 +926 1 +928 2 +929 1 +930 1 +931 1 +932 1 +933 1 +934 2 +935 1 +936 1 +937 1 +938 2 +939 1 +941 1 +942 4 +944 2 +945 2 +946 2 +947 1 +948 1 +950 1 +951 2 +953 1 +954 2 +955 1 +956 1 +957 2 +958 1 +960 3 +962 4 +963 1 +964 1 +965 3 +966 2 +967 2 +968 1 +969 3 +970 3 +972 1 +974 4 +975 3 +976 3 +977 2 +979 2 +980 1 +981 1 +983 5 +984 1 +985 3 +986 1 +987 2 +988 4 +989 2 +991 2 +992 2 +993 1 +994 1 +996 2 +997 2 +998 1 +999 3 +1000 2 +1001 1 +1002 3 +1003 3 +1004 2 +1005 3 +1006 1 +1007 2 +1009 1 +1011 1 +1013 3 +1014 1 +1016 2 +1017 1 +1018 1 +1019 1 +1020 4 +1021 1 +1022 2 +1025 1 +1026 1 +1027 2 +1028 1 +1030 1 +1031 2 +1032 4 +1034 3 +1035 2 +1036 1 +1038 1 +1039 1 +1040 1 +1041 1 +1042 2 +1043 1 +1044 2 +1045 4 +1048 1 +1050 1 +1051 1 +1052 2 +1054 1 +1055 3 +1056 2 +1057 1 +1059 1 +1061 2 +1063 1 +1064 1 +1065 1 +1066 1 +1067 1 +1068 1 +1069 2 +1074 1 +1075 1 +1077 1 +1078 1 +1079 1 +1082 1 +1085 1 +1088 1 +1090 1 +1091 1 +1092 2 +1094 2 +1097 2 +1098 1 +1099 2 +1101 2 +1102 1 +1104 1 +1105 1 +1107 1 +1109 1 +1111 2 +1112 1 +1114 2 +1115 2 +1116 2 +1117 1 +1118 1 +1119 1 +1120 1 +1122 1 +1123 1 +1127 1 +1128 3 +1132 2 +1138 3 +1142 1 +1145 4 +1150 1 +1153 2 +1154 1 +1158 1 +1159 1 +1163 1 +1165 1 +1169 2 +1174 1 +1176 1 +1177 1 +1178 2 +1179 1 +1180 2 +1181 1 +1182 1 +1183 2 +1185 1 +1187 1 +1191 2 +1193 1 +1195 3 +1196 1 +1201 3 +1203 1 +1206 1 +1210 1 +1213 1 +1214 1 +1215 2 +1218 1 +1220 1 +1221 1 +1225 1 +1226 1 +1233 2 +1241 1 +1243 1 +1249 1 +1250 2 +1251 1 +1254 1 +1255 2 +1260 1 +1268 1 +1270 1 +1273 1 +1274 1 +1277 1 +1284 1 +1287 1 +1291 1 +1292 2 +1294 1 +1295 2 +1297 1 +1298 1 +1301 1 +1307 1 +1308 3 +1311 2 +1313 1 +1316 1 +1321 1 +1324 1 +1325 1 +1330 1 +1333 1 +1334 1 +1338 2 +1340 1 +1341 1 +1342 1 +1343 1 +1345 1 +1355 1 +1357 1 +1360 2 +1375 1 +1376 1 +1380 1 +1383 1 +1387 1 +1389 1 +1393 1 +1394 1 +1396 1 +1398 1 +1410 1 +1414 1 +1419 1 +1425 1 +1434 1 +1435 1 +1438 1 +1439 1 +1447 1 +1455 2 +1460 1 +1461 1 +1463 1 +1466 1 +1470 1 +1473 1 +1478 1 +1480 1 +1483 1 +1484 1 +1485 2 +1492 2 +1499 1 +1509 1 +1512 1 +1513 1 +1523 1 +1524 1 +1525 2 +1529 1 +1539 1 +1544 1 +1568 1 +1584 1 +1591 1 +1598 1 +1600 1 +1604 1 +1614 1 +1617 1 +1621 1 +1622 1 +1626 1 +1638 1 +1648 1 +1658 1 +1661 1 +1679 1 +1682 1 +1693 1 +1700 1 +1705 1 +1707 1 +1722 1 +1728 1 +1758 1 +1762 1 +1763 1 +1775 1 +1776 1 +1801 1 +1810 1 +1812 1 +1827 1 +1834 1 +1846 1 +1847 1 +1848 1 +1851 1 +1862 1 +1866 1 +1877 2 +1884 1 +1888 1 +1903 1 +1912 1 +1925 1 +1938 1 +1955 1 +1998 1 +2054 1 +2058 1 +2065 1 +2069 1 +2076 1 +2089 1 +2104 1 +2111 1 +2133 1 +2138 1 +2156 1 +2204 1 +2212 1 +2237 1 +2246 2 +2298 1 +2304 1 +2360 1 +2400 1 +2481 1 +2544 1 +2586 1 +2622 1 +2666 1 +2682 1 +2725 1 +2920 1 +3997 1 +4019 1 +5211 1 +12 19 +14 1 +16 401 +18 2 +20 421 +22 557 +24 625 +26 50 +28 4481 +30 52 +32 550 +34 5840 +36 4644 +38 87 +40 5794 +41 33 +42 571 +44 11805 +46 4711 +47 7 +48 597 +49 12 +50 678 +51 2 +52 14715 +53 3 +54 7322 +55 3 +56 508 +57 39 +58 3486 +59 11 +60 8974 +61 45 +62 1276 +63 4 +64 15693 +65 15 +66 657 +67 13 +68 6409 +69 10 +70 3188 +71 25 +72 1889 +73 27 +74 10370 +75 9 +76 12432 +77 23 +78 520 +79 15 +80 1534 +81 29 +82 2944 +83 23 +84 12071 +85 36 +86 1502 +87 10 +88 10978 +89 11 +90 889 +91 16 +92 4571 +93 17 +94 7855 +95 21 +96 2271 +97 33 +98 1423 +99 15 +100 11096 +101 21 +102 4082 +103 13 +104 5442 +105 25 +106 2113 +107 26 +108 3779 +109 43 +110 1294 +111 29 +112 7860 +113 29 +114 4965 +115 22 +116 7898 +117 25 +118 1772 +119 28 +120 1149 +121 38 +122 1483 +123 32 +124 10572 +125 25 +126 1147 +127 31 +128 1699 +129 22 +130 5533 +131 22 +132 4669 +133 34 +134 3777 +135 10 +136 5412 +137 21 +138 855 +139 26 +140 2485 +141 46 +142 1970 +143 27 +144 6565 +145 40 +146 933 +147 15 +148 7923 +149 16 +150 735 +151 23 +152 1111 +153 33 +154 3714 +155 27 +156 2445 +157 30 +158 3367 +159 10 +160 4646 +161 27 +162 990 +163 23 +164 5679 +165 25 +166 2186 +167 17 +168 899 +169 32 +170 1034 +171 22 +172 6185 +173 32 +174 2685 +175 17 +176 1354 +177 38 +178 1460 +179 15 +180 3478 +181 20 +182 958 +183 20 +184 6055 +185 23 +186 2180 +187 15 +188 1416 +189 30 +190 1284 +191 22 +192 1341 +193 21 +194 2413 +195 18 +196 4984 +197 13 +198 830 +199 22 +200 1834 +201 19 +202 2238 +203 9 +204 3050 +205 22 +206 616 +207 17 +208 2892 +209 22 +210 711 +211 30 +212 2631 +213 19 +214 3341 +215 21 +216 987 +217 26 +218 823 +219 9 +220 3588 +221 20 +222 692 +223 7 +224 2925 +225 31 +226 1075 +227 16 +228 2909 +229 18 +230 673 +231 20 +232 2215 +233 14 +234 1584 +235 21 +236 1292 +237 29 +238 1647 +239 25 +240 1014 +241 30 +242 1648 +243 19 +244 4465 +245 10 +246 787 +247 11 +248 480 +249 25 +250 842 +251 15 +252 1219 +253 23 +254 1508 +255 8 +256 3525 +257 16 +258 490 +259 12 +260 1678 +261 14 +262 822 +263 16 +264 1729 +265 28 +266 604 +267 11 +268 2572 +269 7 +270 1242 +271 15 +272 725 +273 18 +274 1983 +275 13 +276 1662 +277 19 +278 491 +279 12 +280 1586 +281 14 +282 563 +283 10 +284 2363 +285 10 +286 656 +287 14 +288 725 +289 28 +290 871 +291 9 +292 2606 +293 12 +294 961 +295 9 +296 478 +297 13 +298 1252 +299 10 +300 736 +301 19 +302 466 +303 13 +304 2254 +305 12 +306 486 +307 14 +308 1145 +309 13 +310 955 +311 13 +312 1235 +313 13 +314 931 +315 14 +316 1768 +317 11 +318 330 +319 10 +320 539 +321 23 +322 570 +323 12 +324 1789 +325 13 +326 884 +327 5 +328 1422 +329 14 +330 317 +331 11 +332 509 +333 13 +334 1062 +335 12 +336 577 +337 27 +338 378 +339 10 +340 2313 +341 9 +342 391 +343 13 +344 894 +345 17 +346 664 +347 9 +348 453 +349 6 +350 363 +351 15 +352 1115 +353 13 +354 1054 +355 8 +356 1108 +357 12 +358 354 +359 7 +360 363 +361 16 +362 344 +363 11 +364 1734 +365 12 +366 265 +367 10 +368 969 +369 16 +370 316 +371 12 +372 757 +373 7 +374 563 +375 15 +376 857 +377 9 +378 469 +379 9 +380 385 +381 12 +382 921 +383 15 +384 764 +385 14 +386 246 +387 6 +388 1108 +389 14 +390 230 +391 8 +392 266 +393 11 +394 641 +395 8 +396 719 +397 9 +398 243 +399 4 +400 1108 +401 7 +402 229 +403 7 +404 903 +405 7 +406 257 +407 12 +408 244 +409 3 +410 541 +411 6 +412 744 +413 8 +414 419 +415 8 +416 388 +417 19 +418 470 +419 14 +420 612 +421 6 +422 342 +423 3 +424 1179 +425 3 +426 116 +427 14 +428 207 +429 6 +430 255 +431 4 +432 288 +433 12 +434 343 +435 6 +436 1015 +437 3 +438 538 +439 10 +440 194 +441 6 +442 188 +443 15 +444 524 +445 7 +446 214 +447 7 +448 574 +449 6 +450 214 +451 5 +452 635 +453 9 +454 464 +455 5 +456 205 +457 9 +458 163 +459 2 +460 558 +461 4 +462 171 +463 14 +464 444 +465 11 +466 543 +467 5 +468 388 +469 6 +470 141 +471 4 +472 647 +473 3 +474 210 +475 4 +476 193 +477 7 +478 195 +479 7 +480 443 +481 10 +482 198 +483 3 +484 816 +485 6 +486 128 +487 9 +488 215 +489 9 +490 328 +491 7 +492 158 +493 11 +494 335 +495 8 +496 435 +497 6 +498 174 +499 1 +500 373 +501 5 +502 140 +503 7 +504 330 +505 9 +506 149 +507 5 +508 642 +509 3 +510 179 +511 3 +512 159 +513 8 +514 204 +515 7 +516 306 +517 4 +518 110 +519 5 +520 326 +521 6 +522 305 +523 6 +524 294 +525 7 +526 268 +527 5 +528 149 +529 4 +530 133 +531 2 +532 513 +533 10 +534 116 +535 5 +536 258 +537 4 +538 113 +539 4 +540 138 +541 6 +542 116 +544 485 +545 4 +546 93 +547 9 +548 299 +549 3 +550 256 +551 6 +552 92 +553 3 +554 175 +555 6 +556 253 +557 7 +558 95 +559 2 +560 128 +561 4 +562 206 +563 2 +564 465 +565 3 +566 69 +567 3 +568 157 +569 7 +570 97 +571 8 +572 118 +573 5 +574 130 +575 4 +576 301 +577 6 +578 177 +579 2 +580 397 +581 3 +582 80 +583 1 +584 128 +585 5 +586 52 +587 2 +588 72 +589 1 +590 84 +591 6 +592 323 +593 11 +594 77 +595 5 +596 205 +597 1 +598 244 +599 4 +600 69 +601 3 +602 89 +603 5 +604 254 +605 6 +606 147 +607 3 +608 83 +609 3 +610 77 +611 3 +612 194 +613 1 +614 98 +615 3 +616 243 +617 3 +618 50 +619 8 +620 188 +621 4 +622 67 +623 4 +624 123 +625 2 +626 50 +627 1 +628 239 +629 2 +630 51 +631 4 +632 65 +633 5 +634 188 +636 81 +637 3 +638 46 +639 3 +640 103 +641 1 +642 136 +643 3 +644 188 +645 3 +646 58 +648 122 +649 4 +650 47 +651 2 +652 155 +653 4 +654 71 +655 1 +656 71 +657 3 +658 50 +659 2 +660 177 +661 5 +662 66 +663 2 +664 183 +665 3 +666 50 +667 2 +668 53 +669 2 +670 115 +672 66 +673 2 +674 47 +675 1 +676 197 +677 2 +678 46 +679 3 +680 95 +681 3 +682 46 +683 3 +684 107 +685 1 +686 86 +687 2 +688 158 +689 4 +690 51 +691 1 +692 80 +694 56 +695 4 +696 40 +698 43 +699 3 +700 95 +701 2 +702 51 +703 2 +704 133 +705 1 +706 100 +707 2 +708 121 +709 2 +710 15 +711 3 +712 35 +713 2 +714 20 +715 3 +716 37 +717 2 +718 78 +720 55 +721 1 +722 42 +723 2 +724 218 +725 3 +726 23 +727 2 +728 26 +729 1 +730 64 +731 2 +732 65 +734 24 +735 2 +736 53 +737 1 +738 32 +739 1 +740 60 +742 81 +743 1 +744 77 +745 1 +746 47 +747 1 +748 62 +749 1 +750 19 +751 1 +752 86 +753 3 +754 40 +756 55 +757 2 +758 38 +759 1 +760 101 +761 1 +762 22 +764 67 +765 2 +766 35 +767 1 +768 38 +769 1 +770 22 +771 1 +772 82 +773 1 +774 73 +776 29 +777 1 +778 55 +780 23 +781 1 +782 16 +784 84 +785 3 +786 28 +788 59 +789 1 +790 33 +791 3 +792 24 +794 13 +795 1 +796 110 +797 2 +798 15 +800 22 +801 3 +802 29 +803 1 +804 87 +806 21 +808 29 +810 48 +812 28 +813 1 +814 58 +815 1 +816 48 +817 1 +818 31 +819 1 +820 66 +822 17 +823 2 +824 58 +826 10 +827 2 +828 25 +829 1 +830 29 +831 1 +832 63 +833 1 +834 26 +835 3 +836 52 +837 1 +838 18 +840 27 +841 2 +842 12 +843 1 +844 83 +845 1 +846 7 +847 1 +848 10 +850 26 +852 25 +853 1 +854 15 +856 27 +858 32 +859 1 +860 15 +862 43 +864 32 +865 1 +866 6 +868 39 +870 11 +872 25 +873 1 +874 10 +875 1 +876 20 +877 2 +878 19 +879 1 +880 30 +882 11 +884 53 +886 25 +887 1 +888 28 +890 6 +892 36 +894 10 +896 13 +898 14 +900 31 +902 14 +903 2 +904 43 +906 25 +908 9 +910 11 +911 1 +912 16 +913 1 +914 24 +916 27 +918 6 +920 15 +922 27 +923 1 +924 23 +926 13 +928 42 +929 1 +930 3 +932 27 +934 17 +936 8 +937 1 +938 11 +940 33 +942 4 +943 1 +944 18 +946 15 +948 13 +950 18 +952 12 +954 11 +956 21 +958 10 +960 13 +962 5 +964 32 +966 13 +968 8 +970 8 +971 1 +972 23 +973 2 +974 12 +975 1 +976 22 +978 7 +979 1 +980 14 +982 8 +984 22 +985 1 +986 6 +988 17 +989 1 +990 6 +992 13 +994 19 +996 11 +998 4 +1000 9 +1002 2 +1004 14 +1006 5 +1008 3 +1010 9 +1012 29 +1014 6 +1016 22 +1017 1 +1018 8 +1019 1 +1020 7 +1022 6 +1023 1 +1024 10 +1026 2 +1028 8 +1030 11 +1031 2 +1032 8 +1034 9 +1036 13 +1038 12 +1040 12 +1042 3 +1044 12 +1046 3 +1048 11 +1050 2 +1051 1 +1052 2 +1054 11 +1056 6 +1058 8 +1059 1 +1060 23 +1062 6 +1063 1 +1064 8 +1066 3 +1068 6 +1070 8 +1071 1 +1072 5 +1074 3 +1076 5 +1078 3 +1080 11 +1081 1 +1082 7 +1084 18 +1086 4 +1087 1 +1088 3 +1090 3 +1092 7 +1094 3 +1096 12 +1098 6 +1099 1 +1100 2 +1102 6 +1104 14 +1106 3 +1108 6 +1110 5 +1112 2 +1114 8 +1116 3 +1118 3 +1120 7 +1122 10 +1124 6 +1126 8 +1128 1 +1130 4 +1132 3 +1134 2 +1136 5 +1138 5 +1140 8 +1142 3 +1144 7 +1146 3 +1148 11 +1150 1 +1152 5 +1154 1 +1156 5 +1158 1 +1160 5 +1162 3 +1164 6 +1165 1 +1166 1 +1168 4 +1169 1 +1170 3 +1171 1 +1172 2 +1174 5 +1176 3 +1177 1 +1180 8 +1182 2 +1184 4 +1186 2 +1188 3 +1190 2 +1192 5 +1194 6 +1196 1 +1198 2 +1200 2 +1204 10 +1206 2 +1208 9 +1210 1 +1214 6 +1216 3 +1218 4 +1220 9 +1221 2 +1222 1 +1224 5 +1226 4 +1228 8 +1230 1 +1232 1 +1234 3 +1236 5 +1240 3 +1242 1 +1244 3 +1245 1 +1246 4 +1248 6 +1250 2 +1252 7 +1256 3 +1258 2 +1260 2 +1262 3 +1264 4 +1265 1 +1266 1 +1270 1 +1271 1 +1272 2 +1274 3 +1276 3 +1278 1 +1280 3 +1284 1 +1286 1 +1290 1 +1292 3 +1294 1 +1296 7 +1300 2 +1302 4 +1304 3 +1306 2 +1308 2 +1312 1 +1314 1 +1316 3 +1318 2 +1320 1 +1324 8 +1326 1 +1330 1 +1331 1 +1336 2 +1338 1 +1340 3 +1341 1 +1344 1 +1346 2 +1347 1 +1348 3 +1352 1 +1354 2 +1356 1 +1358 1 +1360 3 +1362 1 +1364 4 +1366 1 +1370 1 +1372 3 +1380 2 +1384 2 +1388 2 +1390 2 +1392 2 +1394 1 +1396 1 +1398 1 +1400 2 +1402 1 +1404 1 +1406 1 +1410 1 +1412 5 +1418 1 +1420 1 +1424 1 +1432 2 +1434 2 +1442 3 +1444 5 +1448 1 +1454 1 +1456 1 +1460 3 +1462 4 +1468 1 +1474 1 +1476 1 +1478 2 +1480 1 +1486 2 +1488 1 +1492 1 +1496 1 +1500 3 +1503 1 +1506 1 +1512 2 +1516 1 +1522 1 +1524 2 +1534 4 +1536 1 +1538 1 +1540 2 +1544 2 +1548 1 +1556 1 +1560 1 +1562 1 +1564 2 +1566 1 +1568 1 +1570 1 +1572 1 +1576 1 +1590 1 +1594 1 +1604 1 +1608 1 +1614 1 +1622 1 +1624 2 +1628 1 +1629 1 +1636 1 +1642 1 +1654 2 +1660 1 +1664 1 +1670 1 +1684 4 +1698 1 +1732 3 +1742 1 +1752 1 +1760 1 +1764 1 +1772 2 +1798 1 +1808 1 +1820 1 +1852 1 +1856 1 +1874 1 +1902 1 +1908 1 +1952 1 +2004 1 +2018 1 +2020 1 +2028 1 +2174 1 +2233 1 +2244 1 +2280 1 +2290 1 +2352 1 +2604 1 +4190 1 diff --git a/ppocr/utils/table_utils/matcher.py b/ppocr/utils/table_utils/matcher.py new file mode 100755 index 00000000..711806aa --- /dev/null +++ b/ppocr/utils/table_utils/matcher.py @@ -0,0 +1,214 @@ +import json +def distance(box_1, box_2): + x1, y1, x2, y2 = box_1 + x3, y3, x4, y4 = box_2 + # min_x = (x1 + x2) / 2 + # min_y = (y1 + y2) / 2 + # max_x = (x3 + x4) / 2 + # max_y = (y3 + y4) / 2 + dis = abs(x3 - x1) + abs(y3 - y1) + abs(x4- x2) + abs(y4 - y2) + dis_2 = abs(x3 - x1) + abs(y3 - y1) + dis_3 = abs(x4- x2) + abs(y4 - y2) + #dis = pow(min_x - max_x, 2) + pow(min_y - max_y, 2) + pow(x3 - x1, 2) + pow(y3 - y1, 2) + pow(x4- x2, 2) + pow(y4 - y2, 2) + abs(x3 - x1) + abs(y3 - y1) + abs(x4- x2) + abs(y4 - y2) + return dis + min(dis_2, dis_3) + +def compute_iou(rec1, rec2): + """ + computing IoU + :param rec1: (y0, x0, y1, x1), which reflects + (top, left, bottom, right) + :param rec2: (y0, x0, y1, x1) + :return: scala value of IoU + """ + # computing area of each rectangles + rec1, rec2 = rec1 * 1000, rec2 * 1000 + S_rec1 = (rec1[2] - rec1[0]) * (rec1[3] - rec1[1]) + S_rec2 = (rec2[2] - rec2[0]) * (rec2[3] - rec2[1]) + + # computing the sum_area + sum_area = S_rec1 + S_rec2 + + # find the each edge of intersect rectangle + left_line = max(rec1[1], rec2[1]) + right_line = min(rec1[3], rec2[3]) + top_line = max(rec1[0], rec2[0]) + bottom_line = min(rec1[2], rec2[2]) + + # judge if there is an intersect + if left_line >= right_line or top_line >= bottom_line: + return 0 + else: + intersect = (right_line - left_line) * (bottom_line - top_line) + return (intersect / (sum_area - intersect))*1.0 + + + +def matcher_merge(ocr_bboxes, pred_bboxes): # ocr_bboxes: OCR pred_bboxes:端到端 + all_dis = [] + ious = [] + matched = {} + for i, gt_box in enumerate(ocr_bboxes): + distances = [] + for j, pred_box in enumerate(pred_bboxes): + distances.append((distance(gt_box, pred_box), 1. - compute_iou(gt_box, pred_box))) #获取两两cell之间的L1距离和 1- IOU + sorted_distances = distances.copy() + # 根据距离和IOU挑选最"近"的cell + sorted_distances = sorted(sorted_distances, key = lambda item: (item[1], item[0])) + if distances.index(sorted_distances[0]) not in matched.keys(): + matched[distances.index(sorted_distances[0])] = [i] + else: + matched[distances.index(sorted_distances[0])].append(i) + return matched#, sum(ious) / len(ious) +def complex_num(pred_bboxes): + complex_nums = [] + for bbox in pred_bboxes: + distances = [] + temp_ious = [] + for pred_bbox in pred_bboxes: + if bbox != pred_bbox: + distances.append(distance(bbox, pred_bbox)) + temp_ious.append(compute_iou(bbox, pred_bbox)) + complex_nums.append(temp_ious[distances.index(min(distances))]) + return sum(complex_nums) / len(complex_nums) + +def get_rows(pred_bboxes): + pre_bbox = pred_bboxes[0] + res = [] + step = 0 + for i in range(len(pred_bboxes)): + bbox = pred_bboxes[i] + if bbox[1] - pre_bbox[1] > 2 or bbox[0] - pre_bbox[0] < 0: + break + else: + res.append(bbox) + step += 1 + for i in range(step): + pred_bboxes.pop(0) + return res, pred_bboxes +def refine_rows(pred_bboxes): # 微调整行的框,使在一条水平线上 + ys_1 = [] + ys_2 = [] + for box in pred_bboxes: + ys_1.append(box[1]) + ys_2.append(box[3]) + min_y_1 = sum(ys_1) / len(ys_1) + min_y_2 = sum(ys_2) / len(ys_2) + re_boxes = [] + for box in pred_bboxes: + box[1] = min_y_1 + box[3] = min_y_2 + re_boxes.append(box) + return re_boxes + +def matcher_refine_row(gt_bboxes, pred_bboxes): + before_refine_pred_bboxes = pred_bboxes.copy() + pred_bboxes = [] + while(len(before_refine_pred_bboxes) != 0): + row_bboxes, before_refine_pred_bboxes = get_rows(before_refine_pred_bboxes) + print(row_bboxes) + pred_bboxes.extend(refine_rows(row_bboxes)) + all_dis = [] + ious = [] + matched = {} + for i, gt_box in enumerate(gt_bboxes): + distances = [] + #temp_ious = [] + for j, pred_box in enumerate(pred_bboxes): + distances.append(distance(gt_box, pred_box)) + #temp_ious.append(compute_iou(gt_box, pred_box)) + #all_dis.append(min(distances)) + #ious.append(temp_ious[distances.index(min(distances))]) + if distances.index(min(distances)) not in matched.keys(): + matched[distances.index(min(distances))] = [i] + else: + matched[distances.index(min(distances))].append(i) + return matched#, sum(ious) / len(ious) + + + +#先挑选出一行,再进行匹配 +def matcher_structure_1(gt_bboxes, pred_bboxes_rows, pred_bboxes): + gt_box_index = 0 + delete_gt_bboxes = gt_bboxes.copy() + match_bboxes_ready = [] + matched = {} + while(len(delete_gt_bboxes) != 0): + row_bboxes, delete_gt_bboxes = get_rows(delete_gt_bboxes) + row_bboxes = sorted(row_bboxes, key = lambda key: key[0]) + if len(pred_bboxes_rows) > 0: + match_bboxes_ready.extend(pred_bboxes_rows.pop(0)) + print(row_bboxes) + for i, gt_box in enumerate(row_bboxes): + #print(gt_box) + pred_distances = [] + distances = [] + for pred_bbox in pred_bboxes: + pred_distances.append(distance(gt_box, pred_bbox)) + for j, pred_box in enumerate(match_bboxes_ready): + distances.append(distance(gt_box, pred_box)) + index = pred_distances.index(min(distances)) + #print('index', index) + if index not in matched.keys(): + matched[index] = [gt_box_index] + else: + matched[index].append(gt_box_index) + gt_box_index += 1 + return matched + +def matcher_structure(gt_bboxes, pred_bboxes_rows, pred_bboxes): + ''' + gt_bboxes: 排序后 + pred_bboxes: + ''' + pre_bbox = gt_bboxes[0] + matched = {} + match_bboxes_ready = [] + match_bboxes_ready.extend(pred_bboxes_rows.pop(0)) + for i, gt_box in enumerate(gt_bboxes): + + pred_distances = [] + for pred_bbox in pred_bboxes: + pred_distances.append(distance(gt_box, pred_bbox)) + distances = [] + gap_pre = gt_box[1] - pre_bbox[1] + gap_pre_1 = gt_box[0] - pre_bbox[2] + #print(gap_pre, len(pred_bboxes_rows)) + if (gap_pre_1 < 0 and len(pred_bboxes_rows) > 0): + match_bboxes_ready.extend(pred_bboxes_rows.pop(0)) + if len(pred_bboxes_rows) == 1: + match_bboxes_ready.extend(pred_bboxes_rows.pop(0)) + if len(match_bboxes_ready) == 0 and len(pred_bboxes_rows) > 0: + match_bboxes_ready.extend(pred_bboxes_rows.pop(0)) + if len(match_bboxes_ready) == 0 and len(pred_bboxes_rows) == 0: + break + #print(match_bboxes_ready) + for j, pred_box in enumerate(match_bboxes_ready): + distances.append(distance(gt_box, pred_box)) + index = pred_distances.index(min(distances)) + #print(gt_box, index) + #match_bboxes_ready.pop(distances.index(min(distances))) + print(gt_box, match_bboxes_ready[distances.index(min(distances))]) + if index not in matched.keys(): + matched[index] = [i] + else: + matched[index].append(i) + pre_bbox = gt_box + return matched + + +def main(): + detect_bboxes = json.load(open('./f_detecion_bbox.json')) + gt_bboxes = json.load(open('./f_gt_bbox.json')) + all_node = 0 + matched_right = 0 + key = 'PMC4796501_003_00.png' + print(key) + gt_bbox = gt_bboxes[key] + pred_bbox = detect_bboxes[key] + matched = matcher(gt_bbox, pred_bbox) + print(matched) + + +if __name__ == "__main__": + main() + diff --git a/ppstructure/predict_system.py b/ppstructure/predict_system.py index e69de29b..cd2ff0fb 100644 --- a/ppstructure/predict_system.py +++ b/ppstructure/predict_system.py @@ -0,0 +1,123 @@ +# Copyright (c) 2020 PaddlePaddle 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. + +import os +import sys +import subprocess + +__dir__ = os.path.dirname(os.path.abspath(__file__)) +sys.path.append(__dir__) +sys.path.append(os.path.abspath(os.path.join(__dir__, '../..'))) + +os.environ["FLAGS_allocator_strategy"] = 'auto_growth' +import cv2 +import copy +import numpy as np +import time +import tools.infer.utility as utility +from tools.infer.predict_system import TextSystem +from ppstructure.table.predict_table import TableSystem, to_excel +from ppstructure.layout.predict_layout import LayoutDetector +from ppocr.utils.utility import get_image_file_list, check_and_read_gif +from ppocr.utils.logging import get_logger + +logger = get_logger() + + +def parse_args(): + parser = utility.init_args() + + # params for table structure + parser.add_argument("--table_max_len", type=int, default=488) + parser.add_argument("--table_max_text_length", type=int, default=100) + parser.add_argument("--table_max_elem_length", type=int, default=800) + parser.add_argument("--table_max_cell_num", type=int, default=500) + parser.add_argument("--table_model_dir", type=str) + parser.add_argument("--table_char_type", type=str, default='en') + parser.add_argument("--table_char_dict_path", type=str, default="./ppocr/utils/dict/table_structure_dict.txt") + + # params for layout detector + parser.add_argument("--layout_model_dir", type=str) + return parser.parse_args() + + +class OCRSystem(): + def __init__(self, args): + self.text_system = TextSystem(args) + self.table_system = TableSystem(args) + self.table_layout = LayoutDetector(args) + self.use_angle_cls = args.use_angle_cls + self.drop_score = args.drop_score + + def __call__(self, img): + ori_im = img.copy() + layout_res = self.table_layout(copy.deepcopy(img)) + for region in layout_res: + x1, y1, x2, y2 = region['bbox'] + roi_img = ori_im[y1:y2, x1:x2,:] + if region['label'] == 'table': + res = self.table_system(roi_img) + else: + res = self.text_system(roi_img) + region['res'] = res + return layout_res + + +def main(args): + image_file_list = get_image_file_list(args.image_dir) + image_file_list = image_file_list[args.process_id::args.total_process_num] + excel_save_folder = 'output/table' + os.makedirs(excel_save_folder, exist_ok=True) + + text_sys = OCRSystem(args) + img_num = len(image_file_list) + for i, image_file in enumerate(image_file_list): + logger.info("[{}/{}] {}".format(i, img_num, image_file)) + img, flag = check_and_read_gif(image_file) + imgname = os.path.basename(image_file).split('.')[0] + # excel_path = os.path.join(excel_save_folder, + '.xlsx') + if not flag: + img = cv2.imread(image_file) + if img is None: + logger.info("error in loading image:{}".format(image_file)) + continue + starttime = time.time() + res = text_sys(img) + + for region in res: + if region['label'] == 'table': + # x1, y1, x2, y2 = region['bbox'] + excel_path = os.path.join(excel_save_folder, '{}_{}.xlsx'.format(imgname,region['bbox'])) + to_excel(region['res'],excel_path) + logger.info(res) + elapse = time.time() - starttime + logger.info("Predict time : {:.3f}s".format(elapse)) + + +if __name__ == "__main__": + args = parse_args() + if args.use_mp: + p_list = [] + total_process_num = args.total_process_num + for process_id in range(total_process_num): + cmd = [sys.executable, "-u"] + sys.argv + [ + "--process_id={}".format(process_id), + "--use_mp={}".format(False) + ] + p = subprocess.Popen(cmd, stdout=sys.stdout, stderr=sys.stdout) + p_list.append(p) + for p in p_list: + p.wait() + else: + main(args) diff --git a/ppstructure/table/__init__.py b/ppstructure/table/__init__.py new file mode 100644 index 00000000..1d11e265 --- /dev/null +++ b/ppstructure/table/__init__.py @@ -0,0 +1,13 @@ +# copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +# 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. diff --git a/ppstructure/table/eval_table.py b/ppstructure/table/eval_table.py new file mode 100755 index 00000000..baa70177 --- /dev/null +++ b/ppstructure/table/eval_table.py @@ -0,0 +1,67 @@ +# Copyright (c) 2020 PaddlePaddle 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. +import os +import sys +__dir__ = os.path.dirname(os.path.abspath(__file__)) +sys.path.append(__dir__) +sys.path.append(os.path.abspath(os.path.join(__dir__, '..'))) + +import cv2 +import json +from tqdm import tqdm +from ppstructure.table.table_metric import TEDS +from ppstructure.table.predict_table import TableSystem, utility + + +def main(gt_path, img_root, args): + teds = TEDS(n_jobs=16) + + text_sys = TableSystem(args) + jsons_gt = json.load(open(gt_path)) # gt + pred_htmls = [] + gt_htmls = [] + for img_name in tqdm(jsons_gt): + if img_name != 'PMC1064865_002_00.png': + continue + # 读取信息 + img = cv2.imread(os.path.join(img_root,img_name)) + pred_html = text_sys(img) + pred_htmls.append(pred_html) + + gt_structures, gt_bboxes, gt_contents, contents_with_block = jsons_gt[img_name] + gt_html, gt = get_gt_html(gt_structures, contents_with_block) # 获取HTMLgt + gt_htmls.append(gt_html) + scores = teds.batch_evaluate_html(gt_htmls, pred_htmls) # 计算teds + print('teds:', sum(scores) / len(scores)) + + +def get_gt_html(gt_structures, contents_with_block): + end_html = [] + td_index = 0 + for tag in gt_structures: + if '' in tag: + if contents_with_block[td_index] != []: + end_html.extend(contents_with_block[td_index]) + end_html.append(tag) + td_index += 1 + else: + end_html.append(tag) + return ''.join(end_html), end_html + + +if __name__ == '__main__': + args = utility.parse_args() + gt_path = 'table/match_code/f_gt_bbox.json' + img_root = 'table/imgs' + main(gt_path,img_root, args) diff --git a/ppstructure/table/predict_structure.py b/ppstructure/table/predict_structure.py new file mode 100755 index 00000000..fd00dfd1 --- /dev/null +++ b/ppstructure/table/predict_structure.py @@ -0,0 +1,141 @@ +# Copyright (c) 2020 PaddlePaddle 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. +import os +import sys + +__dir__ = os.path.dirname(os.path.abspath(__file__)) +sys.path.append(__dir__) +sys.path.append(os.path.abspath(os.path.join(__dir__, '../..'))) + +os.environ["FLAGS_allocator_strategy"] = 'auto_growth' + +import cv2 +import numpy as np +import math +import time +import traceback +import paddle + +import tools.infer.utility as utility +from ppocr.data import create_operators, transform +from ppocr.postprocess import build_post_process +from ppocr.utils.logging import get_logger +from ppocr.utils.utility import get_image_file_list, check_and_read_gif + +logger = get_logger() + + +class TableStructurer(object): + def __init__(self, args): + pre_process_list = [{ + 'ResizeTableImage': { + 'max_len': args.table_max_len + } + }, { + 'NormalizeImage': { + 'std': [0.229, 0.224, 0.225], + 'mean': [0.485, 0.456, 0.406], + 'scale': '1./255.', + 'order': 'hwc' + } + }, { + 'PaddingTableImage': None + }, { + 'ToCHWImage': None + }, { + 'KeepKeys': { + 'keep_keys': ['image'] + } + }] + postprocess_params = { + 'name': 'TableLabelDecode', + "character_type": args.table_char_type, + "character_dict_path": args.table_char_dict_path, + "max_text_length": args.table_max_text_length, + "max_elem_length": args.table_max_elem_length, + "max_cell_num": args.table_max_cell_num + } + + self.preprocess_op = create_operators(pre_process_list) + self.postprocess_op = build_post_process(postprocess_params) + self.predictor, self.input_tensor, self.output_tensors = \ + utility.create_predictor(args, 'table', logger) + + def __call__(self, img): + ori_im = img.copy() + data = {'image': img} + data = transform(data, self.preprocess_op) + img = data[0] + if img is None: + return None, 0 + img = np.expand_dims(img, axis=0) + img = img.copy() + starttime = time.time() + + self.input_tensor.copy_from_cpu(img) + self.predictor.run() + outputs = [] + for output_tensor in self.output_tensors: + output = output_tensor.copy_to_cpu() + outputs.append(output) + + preds = {} + preds['structure_probs'] = outputs[1] + preds['loc_preds'] = outputs[0] + + post_result = self.postprocess_op(preds) + + structure_str_list = post_result['structure_str_list'] + res_loc = post_result['res_loc'] + imgh, imgw = ori_im.shape[0:2] + res_loc_final = [] + for rno in range(len(res_loc[0])): + x0, y0, x1, y1 = res_loc[0][rno] + left = max(int(imgw * x0), 0) + top = max(int(imgh * y0), 0) + right = min(int(imgw * x1), imgw - 1) + bottom = min(int(imgh * y1), imgh - 1) + res_loc_final.append([left, top, right, bottom]) + + structure_str_list = structure_str_list[0][:-1] + structure_str_list = ['', '', ''] + structure_str_list + ['
', '', ''] + + elapse = time.time() - starttime + return (structure_str_list, res_loc_final), elapse + + +def main(args): + image_file_list = get_image_file_list(args.image_dir) + table_structurer = TableStructurer(args) + count = 0 + total_time = 0 + for image_file in image_file_list: + img, flag = check_and_read_gif(image_file) + if not flag: + img = cv2.imread(image_file) + if img is None: + logger.info("error in loading image:{}".format(image_file)) + continue + structure_res, elapse = table_structurer(img) + + logger.info("result: {}".format(structure_res)) + + if count > 0: + total_time += elapse + count += 1 + logger.info("Predict time of {}: {}".format(image_file, elapse)) + + +if __name__ == "__main__": + main(utility.parse_args()) diff --git a/ppstructure/table/predict_table.py b/ppstructure/table/predict_table.py new file mode 100644 index 00000000..36cf4939 --- /dev/null +++ b/ppstructure/table/predict_table.py @@ -0,0 +1,222 @@ +# Copyright (c) 2020 PaddlePaddle 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. + +import os +import sys +import subprocess + +__dir__ = os.path.dirname(os.path.abspath(__file__)) +sys.path.append(__dir__) +sys.path.append(os.path.abspath(os.path.join(__dir__, '../..'))) + +os.environ["FLAGS_allocator_strategy"] = 'auto_growth' +import cv2 +import copy +import numpy as np +import time +import tools.infer.utility as utility +import tools.infer.predict_rec as predict_rec +import tools.infer.predict_det as predict_det +import ppstructure.table.predict_structure as predict_strture +from ppocr.utils.utility import get_image_file_list, check_and_read_gif +from ppocr.utils.logging import get_logger +from ppocr.utils.table_utils.matcher import distance, compute_iou + +logger = get_logger() + + +def expand(pix, det_box, shape): + x0, y0, x1, y1 = det_box + # print(shape) + h, w, c = shape + tmp_x0 = x0 - pix + tmp_x1 = x1 + pix + tmp_y0 = y0 - pix + tmp_y1 = y1 + pix + x0_ = tmp_x0 if tmp_x0 >= 0 else 0 + x1_ = tmp_x1 if tmp_x1 <= w else w + y0_ = tmp_y0 if tmp_y0 >= 0 else 0 + y1_ = tmp_y1 if tmp_y1 <= h else h + return x0_, y0_, x1_, y1_ + + +class TableSystem(object): + def __init__(self, args): + self.text_detector = predict_det.TextDetector(args) + self.text_recognizer = predict_rec.TextRecognizer(args) + self.table_structurer = predict_strture.TableStructurer(args) + self.use_angle_cls = args.use_angle_cls + self.drop_score = args.drop_score + + def __call__(self, img): + ori_im = img.copy() + structure_res, elapse = self.table_structurer(copy.deepcopy(img)) + dt_boxes, elapse = self.text_detector(copy.deepcopy(img)) + dt_boxes = sorted_boxes(dt_boxes) + + r_boxes = [] + for box in dt_boxes: + x_min = box[:, 0].min() - 1 + x_max = box[:, 0].max() + 1 + y_min = box[:, 1].min() - 1 + y_max = box[:, 1].max() + 1 + box = [x_min, y_min, x_max, y_max] + r_boxes.append(box) + dt_boxes = np.array(r_boxes) + + # logger.info("dt_boxes num : {}, elapse : {}".format( + # len(dt_boxes), elapse)) + if dt_boxes is None: + return None, None + img_crop_list = [] + + for i in range(len(dt_boxes)): + det_box = dt_boxes[i] + x0, y0, x1, y1 = expand(2, det_box, ori_im.shape) + text_rect = ori_im[int(y0):int(y1), int(x0):int(x1), :] + img_crop_list.append(text_rect) + rec_res, elapse = self.text_recognizer(img_crop_list) + # logger.info("rec_res num : {}, elapse : {}".format( + # len(rec_res), elapse)) + + pred_html, pred = self.rebuild_table(structure_res, dt_boxes, rec_res) + return pred_html + + def rebuild_table(self, structure_res, dt_boxes, rec_res): + pred_structures, pred_bboxes = structure_res + matched_index = self.match_result(dt_boxes, pred_bboxes) + pred_html, pred = self.get_pred_html(pred_structures, matched_index, rec_res) + return pred_html, pred + + def match_result(self, dt_boxes, pred_bboxes): + matched = {} + for i, gt_box in enumerate(dt_boxes): + # gt_box = [np.min(gt_box[:, 0]), np.min(gt_box[:, 1]), np.max(gt_box[:, 0]), np.max(gt_box[:, 1])] + distances = [] + for j, pred_box in enumerate(pred_bboxes): + distances.append( + (distance(gt_box, pred_box), 1. - compute_iou(gt_box, pred_box))) # 获取两两cell之间的L1距离和 1- IOU + sorted_distances = distances.copy() + # 根据距离和IOU挑选最"近"的cell + sorted_distances = sorted(sorted_distances, key=lambda item: (item[1], item[0])) + if distances.index(sorted_distances[0]) not in matched.keys(): + matched[distances.index(sorted_distances[0])] = [i] + else: + matched[distances.index(sorted_distances[0])].append(i) + return matched + + def get_pred_html(self, pred_structures, matched_index, ocr_contents): + end_html = [] + td_index = 0 + for tag in pred_structures: + if '' in tag: + if td_index in matched_index.keys(): + b_with = False + if '' in ocr_contents[matched_index[td_index][0]] and len(matched_index[td_index]) > 1: + b_with = True + end_html.extend('') + for i, td_index_index in enumerate(matched_index[td_index]): + content = ocr_contents[td_index_index][0] + if len(matched_index[td_index]) > 1: + if len(content) == 0: + continue + if content[0] == ' ': + content = content[1:] + if '' in content: + content = content[3:] + if '' in content: + content = content[:-4] + if len(content) == 0: + continue + if i != len(matched_index[td_index]) - 1 and ' ' != content[-1]: + content += ' ' + end_html.extend(content) + if b_with: + end_html.extend('') + + end_html.append(tag) + td_index += 1 + else: + end_html.append(tag) + return ''.join(end_html), end_html + + +def sorted_boxes(dt_boxes): + """ + Sort text boxes in order from top to bottom, left to right + args: + dt_boxes(array):detected text boxes with shape [4, 2] + return: + sorted boxes(array) with shape [4, 2] + """ + num_boxes = dt_boxes.shape[0] + sorted_boxes = sorted(dt_boxes, key=lambda x: (x[0][1], x[0][0])) + _boxes = list(sorted_boxes) + + for i in range(num_boxes - 1): + if abs(_boxes[i + 1][0][1] - _boxes[i][0][1]) < 10 and \ + (_boxes[i + 1][0][0] < _boxes[i][0][0]): + tmp = _boxes[i] + _boxes[i] = _boxes[i + 1] + _boxes[i + 1] = tmp + return _boxes + +def to_excel(html_table, excel_path): + from tablepyxl import tablepyxl + tablepyxl.document_to_xl(html_table, excel_path) + + +def main(args): + image_file_list = get_image_file_list(args.image_dir) + image_file_list = image_file_list[args.process_id::args.total_process_num] + excel_save_folder = 'output/table' + os.makedirs(excel_save_folder, exist_ok=True) + + text_sys = TableSystem(args) + img_num = len(image_file_list) + for i, image_file in enumerate(image_file_list): + logger.info("[{}/{}] {}".format(i, img_num, image_file)) + img, flag = check_and_read_gif(image_file) + excel_path = os.path.join(excel_save_folder, os.path.basename(image_file).split('.')[0] + '.xlsx') + if not flag: + img = cv2.imread(image_file) + if img is None: + logger.info("error in loading image:{}".format(image_file)) + continue + starttime = time.time() + pred_html = text_sys(img) + + to_excel(pred_html, excel_path) + logger.info('excel saved to {}'.format(excel_path)) + logger.info(pred_html) + elapse = time.time() - starttime + logger.info("Predict time : {:.3f}s".format(elapse)) + + +if __name__ == "__main__": + args = utility.parse_args() + if args.use_mp: + p_list = [] + total_process_num = args.total_process_num + for process_id in range(total_process_num): + cmd = [sys.executable, "-u"] + sys.argv + [ + "--process_id={}".format(process_id), + "--use_mp={}".format(False) + ] + p = subprocess.Popen(cmd, stdout=sys.stdout, stderr=sys.stdout) + p_list.append(p) + for p in p_list: + p.wait() + else: + main(args) diff --git a/ppstructure/table/table_metric/__init__.py b/ppstructure/table/table_metric/__init__.py new file mode 100755 index 00000000..de2d3074 --- /dev/null +++ b/ppstructure/table/table_metric/__init__.py @@ -0,0 +1,16 @@ +# copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +# 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. + +__all__ = ['TEDS'] +from .table_metric import TEDS \ No newline at end of file diff --git a/ppstructure/table/table_metric/parallel.py b/ppstructure/table/table_metric/parallel.py new file mode 100755 index 00000000..f7326a1f --- /dev/null +++ b/ppstructure/table/table_metric/parallel.py @@ -0,0 +1,51 @@ +from tqdm import tqdm +from concurrent.futures import ProcessPoolExecutor, as_completed + + +def parallel_process(array, function, n_jobs=16, use_kwargs=False, front_num=0): + """ + A parallel version of the map function with a progress bar. + Args: + array (array-like): An array to iterate over. + function (function): A python function to apply to the elements of array + n_jobs (int, default=16): The number of cores to use + use_kwargs (boolean, default=False): Whether to consider the elements of array as dictionaries of + keyword arguments to function + front_num (int, default=3): The number of iterations to run serially before kicking off the parallel job. + Useful for catching bugs + Returns: + [function(array[0]), function(array[1]), ...] + """ + # We run the first few iterations serially to catch bugs + if front_num > 0: + front = [function(**a) if use_kwargs else function(a) + for a in array[:front_num]] + else: + front = [] + # If we set n_jobs to 1, just run a list comprehension. This is useful for benchmarking and debugging. + if n_jobs == 1: + return front + [function(**a) if use_kwargs else function(a) for a in tqdm(array[front_num:])] + # Assemble the workers + with ProcessPoolExecutor(max_workers=n_jobs) as pool: + # Pass the elements of array into function + if use_kwargs: + futures = [pool.submit(function, **a) for a in array[front_num:]] + else: + futures = [pool.submit(function, a) for a in array[front_num:]] + kwargs = { + 'total': len(futures), + 'unit': 'it', + 'unit_scale': True, + 'leave': True + } + # Print out the progress as tasks complete + for f in tqdm(as_completed(futures), **kwargs): + pass + out = [] + # Get the results from the futures. + for i, future in tqdm(enumerate(futures)): + try: + out.append(future.result()) + except Exception as e: + out.append(e) + return front + out diff --git a/tools/infer/utility.py b/tools/infer/utility.py index 3f0ff2ff..956df5ca 100755 --- a/tools/infer/utility.py +++ b/tools/infer/utility.py @@ -125,6 +125,8 @@ def create_predictor(args, mode, logger): model_dir = args.cls_model_dir elif mode == 'rec': model_dir = args.rec_model_dir + elif mode == 'table': + model_dir = args.table_model_dir else: model_dir = args.e2e_model_dir @@ -244,7 +246,8 @@ def create_predictor(args, mode, logger): config.delete_pass("conv_transpose_eltwiseadd_bn_fuse_pass") config.switch_use_feed_fetch_ops(False) - + if mode == 'table': + config.switch_ir_optim(False) # create predictor predictor = inference.create_predictor(config) input_names = predictor.get_input_names() From eb7ce442a3adbd8899b2f357583847d1b237d88b Mon Sep 17 00:00:00 2001 From: WenmuZhou Date: Thu, 3 Jun 2021 16:43:29 +0800 Subject: [PATCH 03/29] add table eval and predict script --- .../table}/matcher.py | 0 .../table/table_metric/table_metric.py | 247 ++++++++++++++++++ 2 files changed, 247 insertions(+) rename {ppocr/utils/table_utils => ppstructure/table}/matcher.py (100%) create mode 100755 ppstructure/table/table_metric/table_metric.py diff --git a/ppocr/utils/table_utils/matcher.py b/ppstructure/table/matcher.py similarity index 100% rename from ppocr/utils/table_utils/matcher.py rename to ppstructure/table/matcher.py diff --git a/ppstructure/table/table_metric/table_metric.py b/ppstructure/table/table_metric/table_metric.py new file mode 100755 index 00000000..9aca98ad --- /dev/null +++ b/ppstructure/table/table_metric/table_metric.py @@ -0,0 +1,247 @@ +# Copyright 2020 IBM +# Author: peter.zhong@au1.ibm.com +# +# This is free software; you can redistribute it and/or modify +# it under the terms of the Apache 2.0 License. +# +# This software is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# Apache 2.0 License for more details. + +import distance +from apted import APTED, Config +from apted.helpers import Tree +from lxml import etree, html +from collections import deque +from .parallel import parallel_process +from tqdm import tqdm + + +class TableTree(Tree): + def __init__(self, tag, colspan=None, rowspan=None, content=None, *children): + self.tag = tag + self.colspan = colspan + self.rowspan = rowspan + self.content = content + self.children = list(children) + + def bracket(self): + """Show tree using brackets notation""" + if self.tag == 'td': + result = '"tag": %s, "colspan": %d, "rowspan": %d, "text": %s' % \ + (self.tag, self.colspan, self.rowspan, self.content) + else: + result = '"tag": %s' % self.tag + for child in self.children: + result += child.bracket() + return "{{{}}}".format(result) + + +class CustomConfig(Config): + @staticmethod + def maximum(*sequences): + """Get maximum possible value + """ + return max(map(len, sequences)) + + def normalized_distance(self, *sequences): + """Get distance from 0 to 1 + """ + return float(distance.levenshtein(*sequences)) / self.maximum(*sequences) + + def rename(self, node1, node2): + """Compares attributes of trees""" + #print(node1.tag) + if (node1.tag != node2.tag) or (node1.colspan != node2.colspan) or (node1.rowspan != node2.rowspan): + return 1. + if node1.tag == 'td': + if node1.content or node2.content: + #print(node1.content, ) + return self.normalized_distance(node1.content, node2.content) + return 0. + + + +class CustomConfig_del_short(Config): + @staticmethod + def maximum(*sequences): + """Get maximum possible value + """ + return max(map(len, sequences)) + + def normalized_distance(self, *sequences): + """Get distance from 0 to 1 + """ + return float(distance.levenshtein(*sequences)) / self.maximum(*sequences) + + def rename(self, node1, node2): + """Compares attributes of trees""" + if (node1.tag != node2.tag) or (node1.colspan != node2.colspan) or (node1.rowspan != node2.rowspan): + return 1. + if node1.tag == 'td': + if node1.content or node2.content: + #print('before') + #print(node1.content, node2.content) + #print('after') + node1_content = node1.content + node2_content = node2.content + if len(node1_content) < 3: + node1_content = ['####'] + if len(node2_content) < 3: + node2_content = ['####'] + return self.normalized_distance(node1_content, node2_content) + return 0. + +class CustomConfig_del_block(Config): + @staticmethod + def maximum(*sequences): + """Get maximum possible value + """ + return max(map(len, sequences)) + + def normalized_distance(self, *sequences): + """Get distance from 0 to 1 + """ + return float(distance.levenshtein(*sequences)) / self.maximum(*sequences) + + def rename(self, node1, node2): + """Compares attributes of trees""" + if (node1.tag != node2.tag) or (node1.colspan != node2.colspan) or (node1.rowspan != node2.rowspan): + return 1. + if node1.tag == 'td': + if node1.content or node2.content: + + node1_content = node1.content + node2_content = node2.content + while ' ' in node1_content: + print(node1_content.index(' ')) + node1_content.pop(node1_content.index(' ')) + while ' ' in node2_content: + print(node2_content.index(' ')) + node2_content.pop(node2_content.index(' ')) + return self.normalized_distance(node1_content, node2_content) + return 0. + +class TEDS(object): + ''' Tree Edit Distance basead Similarity + ''' + + def __init__(self, structure_only=False, n_jobs=1, ignore_nodes=None): + assert isinstance(n_jobs, int) and ( + n_jobs >= 1), 'n_jobs must be an integer greather than 1' + self.structure_only = structure_only + self.n_jobs = n_jobs + self.ignore_nodes = ignore_nodes + self.__tokens__ = [] + + def tokenize(self, node): + ''' Tokenizes table cells + ''' + self.__tokens__.append('<%s>' % node.tag) + if node.text is not None: + self.__tokens__ += list(node.text) + for n in node.getchildren(): + self.tokenize(n) + if node.tag != 'unk': + self.__tokens__.append('' % node.tag) + if node.tag != 'td' and node.tail is not None: + self.__tokens__ += list(node.tail) + + def load_html_tree(self, node, parent=None): + ''' Converts HTML tree to the format required by apted + ''' + global __tokens__ + if node.tag == 'td': + if self.structure_only: + cell = [] + else: + self.__tokens__ = [] + self.tokenize(node) + cell = self.__tokens__[1:-1].copy() + new_node = TableTree(node.tag, + int(node.attrib.get('colspan', '1')), + int(node.attrib.get('rowspan', '1')), + cell, *deque()) + else: + new_node = TableTree(node.tag, None, None, None, *deque()) + if parent is not None: + parent.children.append(new_node) + if node.tag != 'td': + for n in node.getchildren(): + self.load_html_tree(n, new_node) + if parent is None: + return new_node + + def evaluate(self, pred, true): + ''' Computes TEDS score between the prediction and the ground truth of a + given sample + ''' + if (not pred) or (not true): + return 0.0 + parser = html.HTMLParser(remove_comments=True, encoding='utf-8') + pred = html.fromstring(pred, parser=parser) + true = html.fromstring(true, parser=parser) + if pred.xpath('body/table') and true.xpath('body/table'): + pred = pred.xpath('body/table')[0] + true = true.xpath('body/table')[0] + if self.ignore_nodes: + etree.strip_tags(pred, *self.ignore_nodes) + etree.strip_tags(true, *self.ignore_nodes) + n_nodes_pred = len(pred.xpath(".//*")) + n_nodes_true = len(true.xpath(".//*")) + n_nodes = max(n_nodes_pred, n_nodes_true) + tree_pred = self.load_html_tree(pred) + tree_true = self.load_html_tree(true) + distance = APTED(tree_pred, tree_true, + CustomConfig()).compute_edit_distance() + return 1.0 - (float(distance) / n_nodes) + else: + return 0.0 + + def batch_evaluate(self, pred_json, true_json): + ''' Computes TEDS score between the prediction and the ground truth of + a batch of samples + @params pred_json: {'FILENAME': 'HTML CODE', ...} + @params true_json: {'FILENAME': {'html': 'HTML CODE'}, ...} + @output: {'FILENAME': 'TEDS SCORE', ...} + ''' + samples = true_json.keys() + if self.n_jobs == 1: + scores = [self.evaluate(pred_json.get( + filename, ''), true_json[filename]['html']) for filename in tqdm(samples)] + else: + inputs = [{'pred': pred_json.get( + filename, ''), 'true': true_json[filename]['html']} for filename in samples] + scores = parallel_process( + inputs, self.evaluate, use_kwargs=True, n_jobs=self.n_jobs, front_num=1) + scores = dict(zip(samples, scores)) + return scores + + def batch_evaluate_html(self, pred_htmls, true_htmls): + ''' Computes TEDS score between the prediction and the ground truth of + a batch of samples + ''' + if self.n_jobs == 1: + scores = [self.evaluate(pred_html, true_html) for ( + pred_html, true_html) in zip(pred_htmls, true_htmls)] + else: + inputs = [{"pred": pred_html, "true": true_html} for( + pred_html, true_html) in zip(pred_htmls, true_htmls)] + + scores = parallel_process( + inputs, self.evaluate, use_kwargs=True, n_jobs=self.n_jobs, front_num=1) + return scores + + +if __name__ == '__main__': + import json + import pprint + with open('sample_pred.json') as fp: + pred_json = json.load(fp) + with open('sample_gt.json') as fp: + true_json = json.load(fp) + teds = TEDS(n_jobs=4) + scores = teds.batch_evaluate(pred_json, true_json) + pp = pprint.PrettyPrinter() + pp.pprint(scores) From cd0522fbfda20b81c540f2883bc5fe033e8cf941 Mon Sep 17 00:00:00 2001 From: WenmuZhou Date: Thu, 3 Jun 2021 16:44:04 +0800 Subject: [PATCH 04/29] add table eval and predict script --- ppstructure/table/predict_table.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ppstructure/table/predict_table.py b/ppstructure/table/predict_table.py index 36cf4939..30e503e5 100644 --- a/ppstructure/table/predict_table.py +++ b/ppstructure/table/predict_table.py @@ -31,7 +31,7 @@ import tools.infer.predict_det as predict_det import ppstructure.table.predict_structure as predict_strture from ppocr.utils.utility import get_image_file_list, check_and_read_gif from ppocr.utils.logging import get_logger -from ppocr.utils.table_utils.matcher import distance, compute_iou +from ppstructure.table.matcher import distance, compute_iou logger = get_logger() From 0bf30fea675388add8a8dc0ddc5c3e1bf713a090 Mon Sep 17 00:00:00 2001 From: WenmuZhou Date: Thu, 3 Jun 2021 16:47:38 +0800 Subject: [PATCH 05/29] fix eval parse_args import error --- ppstructure/table/eval_table.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/ppstructure/table/eval_table.py b/ppstructure/table/eval_table.py index baa70177..46df68df 100755 --- a/ppstructure/table/eval_table.py +++ b/ppstructure/table/eval_table.py @@ -21,7 +21,8 @@ import cv2 import json from tqdm import tqdm from ppstructure.table.table_metric import TEDS -from ppstructure.table.predict_table import TableSystem, utility +from ppstructure.table.predict_table import TableSystem +from ppstructure.predict_system import parse_args def main(gt_path, img_root, args): @@ -32,8 +33,6 @@ def main(gt_path, img_root, args): pred_htmls = [] gt_htmls = [] for img_name in tqdm(jsons_gt): - if img_name != 'PMC1064865_002_00.png': - continue # 读取信息 img = cv2.imread(os.path.join(img_root,img_name)) pred_html = text_sys(img) @@ -61,7 +60,7 @@ def get_gt_html(gt_structures, contents_with_block): if __name__ == '__main__': - args = utility.parse_args() + args = parse_args() gt_path = 'table/match_code/f_gt_bbox.json' img_root = 'table/imgs' main(gt_path,img_root, args) From 20466055b2dc463b715dcb5a5a3f7c4bcdcfdcb8 Mon Sep 17 00:00:00 2001 From: WenmuZhou Date: Thu, 3 Jun 2021 20:09:05 +0800 Subject: [PATCH 06/29] add save_dir to args --- ppstructure/predict_system.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/ppstructure/predict_system.py b/ppstructure/predict_system.py index cd2ff0fb..f6852ab7 100644 --- a/ppstructure/predict_system.py +++ b/ppstructure/predict_system.py @@ -38,6 +38,8 @@ logger = get_logger() def parse_args(): parser = utility.init_args() + # params for output + parser.add_argument("--table_output", type=str, default='output/table') # params for table structure parser.add_argument("--table_max_len", type=int, default=488) parser.add_argument("--table_max_text_length", type=int, default=100) @@ -65,9 +67,9 @@ class OCRSystem(): layout_res = self.table_layout(copy.deepcopy(img)) for region in layout_res: x1, y1, x2, y2 = region['bbox'] - roi_img = ori_im[y1:y2, x1:x2,:] + roi_img = ori_im[y1:y2, x1:x2, :] if region['label'] == 'table': - res = self.table_system(roi_img) + res = self.text_system(roi_img) else: res = self.text_system(roi_img) region['res'] = res @@ -77,15 +79,15 @@ class OCRSystem(): def main(args): image_file_list = get_image_file_list(args.image_dir) image_file_list = image_file_list[args.process_id::args.total_process_num] - excel_save_folder = 'output/table' - os.makedirs(excel_save_folder, exist_ok=True) + save_folder = args.table_output + os.makedirs(save_folder, exist_ok=True) text_sys = OCRSystem(args) img_num = len(image_file_list) for i, image_file in enumerate(image_file_list): logger.info("[{}/{}] {}".format(i, img_num, image_file)) img, flag = check_and_read_gif(image_file) - imgname = os.path.basename(image_file).split('.')[0] + img_name = os.path.basename(image_file).split('.')[0] # excel_path = os.path.join(excel_save_folder, + '.xlsx') if not flag: img = cv2.imread(image_file) @@ -95,11 +97,17 @@ def main(args): starttime = time.time() res = text_sys(img) + excel_save_folder = os.path.join(save_folder, img_name) + os.makedirs(excel_save_folder, exist_ok=True) + # save res for region in res: if region['label'] == 'table': - # x1, y1, x2, y2 = region['bbox'] - excel_path = os.path.join(excel_save_folder, '{}_{}.xlsx'.format(imgname,region['bbox'])) - to_excel(region['res'],excel_path) + excel_path = os.path.join(excel_save_folder, '{}.xlsx'.format(region['bbox'])) + to_excel(region['res'], excel_path) + else: + with open(os.path.join(excel_save_folder, 'res.txt'),'a',encoding='utf8') as f: + for box, rec_res in zip(*region['res']): + f.write('{}\t{}\n'.format(np.array(box).reshape(-1).tolist(), rec_res)) logger.info(res) elapse = time.time() - starttime logger.info("Predict time : {:.3f}s".format(elapse)) From a5f7511505662d047a3dd2db10512ed90c1e82bf Mon Sep 17 00:00:00 2001 From: WenmuZhou Date: Sat, 5 Jun 2021 17:33:50 +0800 Subject: [PATCH 07/29] mv download func to ppocr/utils/network.py --- paddleocr.py | 126 +++++++++++++---------------------------- ppocr/utils/network.py | 66 +++++++++++++++++++++ 2 files changed, 106 insertions(+), 86 deletions(-) create mode 100644 ppocr/utils/network.py diff --git a/paddleocr.py b/paddleocr.py index 1e4d94ff..708f20b1 100644 --- a/paddleocr.py +++ b/paddleocr.py @@ -21,15 +21,13 @@ sys.path.append(os.path.join(__dir__, '')) import cv2 import numpy as np from pathlib import Path -import tarfile -import requests -from tqdm import tqdm from tools.infer import predict_system from ppocr.utils.logging import get_logger logger = get_logger() from ppocr.utils.utility import check_and_read_gif, get_image_file_list +from ppocr.utils.network import maybe_download, download_with_progressbar from tools.infer.utility import draw_ocr, init_args, str2bool __all__ = ['PaddleOCR'] @@ -37,84 +35,84 @@ __all__ = ['PaddleOCR'] model_urls = { 'det': { 'ch': - 'https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_det_infer.tar', + 'https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_det_infer.tar', 'en': - 'https://paddleocr.bj.bcebos.com/dygraph_v2.0/multilingual/en_ppocr_mobile_v2.0_det_infer.tar' + 'https://paddleocr.bj.bcebos.com/dygraph_v2.0/multilingual/en_ppocr_mobile_v2.0_det_infer.tar' }, 'rec': { 'ch': { 'url': - 'https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_rec_infer.tar', + 'https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_rec_infer.tar', 'dict_path': './ppocr/utils/ppocr_keys_v1.txt' }, 'en': { 'url': - 'https://paddleocr.bj.bcebos.com/dygraph_v2.0/multilingual/en_number_mobile_v2.0_rec_infer.tar', + 'https://paddleocr.bj.bcebos.com/dygraph_v2.0/multilingual/en_number_mobile_v2.0_rec_infer.tar', 'dict_path': './ppocr/utils/en_dict.txt' }, 'french': { 'url': - 'https://paddleocr.bj.bcebos.com/dygraph_v2.0/multilingual/french_mobile_v2.0_rec_infer.tar', + 'https://paddleocr.bj.bcebos.com/dygraph_v2.0/multilingual/french_mobile_v2.0_rec_infer.tar', 'dict_path': './ppocr/utils/dict/french_dict.txt' }, 'german': { 'url': - 'https://paddleocr.bj.bcebos.com/dygraph_v2.0/multilingual/german_mobile_v2.0_rec_infer.tar', + 'https://paddleocr.bj.bcebos.com/dygraph_v2.0/multilingual/german_mobile_v2.0_rec_infer.tar', 'dict_path': './ppocr/utils/dict/german_dict.txt' }, 'korean': { 'url': - 'https://paddleocr.bj.bcebos.com/dygraph_v2.0/multilingual/korean_mobile_v2.0_rec_infer.tar', + 'https://paddleocr.bj.bcebos.com/dygraph_v2.0/multilingual/korean_mobile_v2.0_rec_infer.tar', 'dict_path': './ppocr/utils/dict/korean_dict.txt' }, 'japan': { 'url': - 'https://paddleocr.bj.bcebos.com/dygraph_v2.0/multilingual/japan_mobile_v2.0_rec_infer.tar', + 'https://paddleocr.bj.bcebos.com/dygraph_v2.0/multilingual/japan_mobile_v2.0_rec_infer.tar', 'dict_path': './ppocr/utils/dict/japan_dict.txt' }, 'chinese_cht': { 'url': - 'https://paddleocr.bj.bcebos.com/dygraph_v2.0/multilingual/chinese_cht_mobile_v2.0_rec_infer.tar', + 'https://paddleocr.bj.bcebos.com/dygraph_v2.0/multilingual/chinese_cht_mobile_v2.0_rec_infer.tar', 'dict_path': './ppocr/utils/dict/chinese_cht_dict.txt' }, 'ta': { 'url': - 'https://paddleocr.bj.bcebos.com/dygraph_v2.0/multilingual/ta_mobile_v2.0_rec_infer.tar', + 'https://paddleocr.bj.bcebos.com/dygraph_v2.0/multilingual/ta_mobile_v2.0_rec_infer.tar', 'dict_path': './ppocr/utils/dict/ta_dict.txt' }, 'te': { 'url': - 'https://paddleocr.bj.bcebos.com/dygraph_v2.0/multilingual/te_mobile_v2.0_rec_infer.tar', + 'https://paddleocr.bj.bcebos.com/dygraph_v2.0/multilingual/te_mobile_v2.0_rec_infer.tar', 'dict_path': './ppocr/utils/dict/te_dict.txt' }, 'ka': { 'url': - 'https://paddleocr.bj.bcebos.com/dygraph_v2.0/multilingual/ka_mobile_v2.0_rec_infer.tar', + 'https://paddleocr.bj.bcebos.com/dygraph_v2.0/multilingual/ka_mobile_v2.0_rec_infer.tar', 'dict_path': './ppocr/utils/dict/ka_dict.txt' }, 'latin': { 'url': - 'https://paddleocr.bj.bcebos.com/dygraph_v2.0/multilingual/latin_ppocr_mobile_v2.0_rec_infer.tar', + 'https://paddleocr.bj.bcebos.com/dygraph_v2.0/multilingual/latin_ppocr_mobile_v2.0_rec_infer.tar', 'dict_path': './ppocr/utils/dict/latin_dict.txt' }, 'arabic': { 'url': - 'https://paddleocr.bj.bcebos.com/dygraph_v2.0/multilingual/arabic_ppocr_mobile_v2.0_rec_infer.tar', + 'https://paddleocr.bj.bcebos.com/dygraph_v2.0/multilingual/arabic_ppocr_mobile_v2.0_rec_infer.tar', 'dict_path': './ppocr/utils/dict/arabic_dict.txt' }, 'cyrillic': { 'url': - 'https://paddleocr.bj.bcebos.com/dygraph_v2.0/multilingual/cyrillic_ppocr_mobile_v2.0_rec_infer.tar', + 'https://paddleocr.bj.bcebos.com/dygraph_v2.0/multilingual/cyrillic_ppocr_mobile_v2.0_rec_infer.tar', 'dict_path': './ppocr/utils/dict/cyrillic_dict.txt' }, 'devanagari': { 'url': - 'https://paddleocr.bj.bcebos.com/dygraph_v2.0/multilingual/devanagari_ppocr_mobile_v2.0_rec_infer.tar', + 'https://paddleocr.bj.bcebos.com/dygraph_v2.0/multilingual/devanagari_ppocr_mobile_v2.0_rec_infer.tar', 'dict_path': './ppocr/utils/dict/devanagari_dict.txt' } }, 'cls': - 'https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_cls_infer.tar' + 'https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_cls_infer.tar' } SUPPORT_DET_MODEL = ['DB'] @@ -123,50 +121,6 @@ SUPPORT_REC_MODEL = ['CRNN'] BASE_DIR = os.path.expanduser("~/.paddleocr/") -def download_with_progressbar(url, save_path): - response = requests.get(url, stream=True) - total_size_in_bytes = int(response.headers.get('content-length', 0)) - block_size = 1024 # 1 Kibibyte - progress_bar = tqdm(total=total_size_in_bytes, unit='iB', unit_scale=True) - with open(save_path, 'wb') as file: - for data in response.iter_content(block_size): - progress_bar.update(len(data)) - file.write(data) - progress_bar.close() - if total_size_in_bytes == 0 or progress_bar.n != total_size_in_bytes: - logger.error("Something went wrong while downloading models") - sys.exit(0) - - -def maybe_download(model_storage_directory, url): - # using custom model - tar_file_name_list = [ - 'inference.pdiparams', 'inference.pdiparams.info', 'inference.pdmodel' - ] - if not os.path.exists( - os.path.join(model_storage_directory, 'inference.pdiparams') - ) or not os.path.exists( - os.path.join(model_storage_directory, 'inference.pdmodel')): - tmp_path = os.path.join(model_storage_directory, url.split('/')[-1]) - print('download {} to {}'.format(url, tmp_path)) - os.makedirs(model_storage_directory, exist_ok=True) - download_with_progressbar(url, tmp_path) - with tarfile.open(tmp_path, 'r') as tarObj: - for member in tarObj.getmembers(): - filename = None - for tar_file_name in tar_file_name_list: - if tar_file_name in member.name: - filename = tar_file_name - if filename is None: - continue - file = tarObj.extractfile(member) - with open( - os.path.join(model_storage_directory, filename), - 'wb') as f: - f.write(file.read()) - os.remove(tmp_path) - - def parse_args(mMain=True): import argparse parser = init_args() @@ -194,10 +148,10 @@ class PaddleOCR(predict_system.TextSystem): args: **kwargs: other params show in paddleocr --help """ - postprocess_params = parse_args(mMain=False) - postprocess_params.__dict__.update(**kwargs) - self.use_angle_cls = postprocess_params.use_angle_cls - lang = postprocess_params.lang + params = parse_args(mMain=False) + params.__dict__.update(**kwargs) + self.use_angle_cls = params.use_angle_cls + lang = params.lang latin_lang = [ 'af', 'az', 'bs', 'cs', 'cy', 'da', 'de', 'es', 'et', 'fr', 'ga', 'hr', 'hu', 'id', 'is', 'it', 'ku', 'la', 'lt', 'lv', 'mi', 'ms', @@ -223,46 +177,46 @@ class PaddleOCR(predict_system.TextSystem): lang = "devanagari" assert lang in model_urls[ 'rec'], 'param lang must in {}, but got {}'.format( - model_urls['rec'].keys(), lang) + model_urls['rec'].keys(), lang) if lang == "ch": det_lang = "ch" else: det_lang = "en" use_inner_dict = False - if postprocess_params.rec_char_dict_path is None: + if params.rec_char_dict_path is None: use_inner_dict = True - postprocess_params.rec_char_dict_path = model_urls['rec'][lang][ + params.rec_char_dict_path = model_urls['rec'][lang][ 'dict_path'] # init model dir - if postprocess_params.det_model_dir is None: - postprocess_params.det_model_dir = os.path.join(BASE_DIR, VERSION, + if params.det_model_dir is None: + params.det_model_dir = os.path.join(BASE_DIR, VERSION, 'det', det_lang) - if postprocess_params.rec_model_dir is None: - postprocess_params.rec_model_dir = os.path.join(BASE_DIR, VERSION, + if params.rec_model_dir is None: + params.rec_model_dir = os.path.join(BASE_DIR, VERSION, 'rec', lang) - if postprocess_params.cls_model_dir is None: - postprocess_params.cls_model_dir = os.path.join(BASE_DIR, 'cls') - print(postprocess_params) + if params.cls_model_dir is None: + params.cls_model_dir = os.path.join(BASE_DIR, 'cls') # download model - maybe_download(postprocess_params.det_model_dir, + maybe_download(params.det_model_dir, model_urls['det'][det_lang]) - maybe_download(postprocess_params.rec_model_dir, + maybe_download(params.rec_model_dir, model_urls['rec'][lang]['url']) - maybe_download(postprocess_params.cls_model_dir, model_urls['cls']) + maybe_download(params.cls_model_dir, model_urls['cls']) - if postprocess_params.det_algorithm not in SUPPORT_DET_MODEL: + if params.det_algorithm not in SUPPORT_DET_MODEL: logger.error('det_algorithm must in {}'.format(SUPPORT_DET_MODEL)) sys.exit(0) - if postprocess_params.rec_algorithm not in SUPPORT_REC_MODEL: + if params.rec_algorithm not in SUPPORT_REC_MODEL: logger.error('rec_algorithm must in {}'.format(SUPPORT_REC_MODEL)) sys.exit(0) if use_inner_dict: - postprocess_params.rec_char_dict_path = str( - Path(__file__).parent / postprocess_params.rec_char_dict_path) + params.rec_char_dict_path = str( + Path(__file__).parent / params.rec_char_dict_path) + print(params) # init det_model and rec_model - super().__init__(postprocess_params) + super().__init__(params) def ocr(self, img, det=True, rec=True, cls=True): """ diff --git a/ppocr/utils/network.py b/ppocr/utils/network.py new file mode 100644 index 00000000..7d98f5c3 --- /dev/null +++ b/ppocr/utils/network.py @@ -0,0 +1,66 @@ +# copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +# 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. + +import os +import sys +import tarfile +import requests +from tqdm import tqdm + +from ppocr.utils.logging import get_logger + +def download_with_progressbar(url, save_path): + logger = get_logger() + response = requests.get(url, stream=True) + total_size_in_bytes = int(response.headers.get('content-length', 0)) + block_size = 1024 # 1 Kibibyte + progress_bar = tqdm(total=total_size_in_bytes, unit='iB', unit_scale=True) + with open(save_path, 'wb') as file: + for data in response.iter_content(block_size): + progress_bar.update(len(data)) + file.write(data) + progress_bar.close() + if total_size_in_bytes == 0 or progress_bar.n != total_size_in_bytes: + logger.error("Something went wrong while downloading models") + sys.exit(0) + + +def maybe_download(model_storage_directory, url): + # using custom model + tar_file_name_list = [ + 'inference.pdiparams', 'inference.pdiparams.info', 'inference.pdmodel' + ] + if not os.path.exists( + os.path.join(model_storage_directory, 'inference.pdiparams') + ) or not os.path.exists( + os.path.join(model_storage_directory, 'inference.pdmodel')): + tmp_path = os.path.join(model_storage_directory, url.split('/')[-1]) + print('download {} to {}'.format(url, tmp_path)) + os.makedirs(model_storage_directory, exist_ok=True) + download_with_progressbar(url, tmp_path) + with tarfile.open(tmp_path, 'r') as tarObj: + for member in tarObj.getmembers(): + filename = None + for tar_file_name in tar_file_name_list: + if tar_file_name in member.name: + filename = tar_file_name + if filename is None: + continue + file = tarObj.extractfile(member) + with open( + os.path.join(model_storage_directory, filename), + 'wb') as f: + f.write(file.read()) + os.remove(tmp_path) + From bc0d766425e891e001dbf904079658f55f9ab60f Mon Sep 17 00:00:00 2001 From: WenmuZhou Date: Sat, 5 Jun 2021 22:00:17 +0800 Subject: [PATCH 08/29] init commit for paddlestructure --- MANIFEST.in | 2 +- paddleocr.py | 3 + ppocr/utils/dict/table_dict.txt | 3 +- ppstructure/MANIFEST.in | 9 ++ ppstructure/layout/README.md | 0 ppstructure/layout/README_ch.md | 0 ppstructure/paddlestructure.py | 161 +++++++++++++++++++++++++ ppstructure/predict_system.py | 102 ++++++++-------- ppstructure/setup.py | 65 ++++++++++ ppstructure/table/README_ch.md | 15 +++ ppstructure/table/eval_table.py | 15 ++- ppstructure/table/matcher.py | 18 --- ppstructure/table/predict_structure.py | 14 +-- ppstructure/table/predict_table.py | 31 +++-- ppstructure/utility.py | 40 ++++++ tools/infer/predict_system.py | 8 +- tools/infer/utility.py | 9 +- 17 files changed, 385 insertions(+), 110 deletions(-) create mode 100644 ppstructure/MANIFEST.in delete mode 100644 ppstructure/layout/README.md delete mode 100644 ppstructure/layout/README_ch.md create mode 100644 ppstructure/paddlestructure.py create mode 100644 ppstructure/setup.py create mode 100644 ppstructure/utility.py diff --git a/MANIFEST.in b/MANIFEST.in index e16f157d..cd34d574 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,7 +1,7 @@ include LICENSE.txt include README.md -recursive-include ppocr/utils *.txt utility.py logging.py +recursive-include ppocr/utils *.txt utility.py logging.py network.py recursive-include ppocr/data/ *.py recursive-include ppocr/postprocess *.py recursive-include tools/infer *.py diff --git a/paddleocr.py b/paddleocr.py index 708f20b1..48c8c9c6 100644 --- a/paddleocr.py +++ b/paddleocr.py @@ -19,6 +19,7 @@ __dir__ = os.path.dirname(__file__) sys.path.append(os.path.join(__dir__, '')) import cv2 +import logging import numpy as np from pathlib import Path @@ -150,6 +151,8 @@ class PaddleOCR(predict_system.TextSystem): """ params = parse_args(mMain=False) params.__dict__.update(**kwargs) + if params.show_log: + logger.setLevel(logging.DEBUG) self.use_angle_cls = params.use_angle_cls lang = params.lang latin_lang = [ diff --git a/ppocr/utils/dict/table_dict.txt b/ppocr/utils/dict/table_dict.txt index 804f3e31..2ef028c7 100644 --- a/ppocr/utils/dict/table_dict.txt +++ b/ppocr/utils/dict/table_dict.txt @@ -33,8 +33,7 @@ D Π H ║ - +
L Φ Χ diff --git a/ppstructure/MANIFEST.in b/ppstructure/MANIFEST.in new file mode 100644 index 00000000..f9bd0fe9 --- /dev/null +++ b/ppstructure/MANIFEST.in @@ -0,0 +1,9 @@ +include LICENSE.txt +include README.md + +recursive-include ppocr/utils *.txt utility.py logging.py network.py +recursive-include ppocr/data/ *.py +recursive-include ppocr/postprocess *.py +recursive-include tools/infer *.py +recursive-include table *.py +recursive-include ppstructure *.py \ No newline at end of file diff --git a/ppstructure/layout/README.md b/ppstructure/layout/README.md deleted file mode 100644 index e69de29b..00000000 diff --git a/ppstructure/layout/README_ch.md b/ppstructure/layout/README_ch.md deleted file mode 100644 index e69de29b..00000000 diff --git a/ppstructure/paddlestructure.py b/ppstructure/paddlestructure.py new file mode 100644 index 00000000..c2db42c1 --- /dev/null +++ b/ppstructure/paddlestructure.py @@ -0,0 +1,161 @@ +# Copyright (c) 2020 PaddlePaddle 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. +import logging +import os +import sys + +__dir__ = os.path.dirname(__file__) +sys.path.append(os.path.join(__dir__, '')) + + +import cv2 +import numpy as np +from pathlib import Path + +from ppocr.utils.logging import get_logger +from predict_system import OCRSystem, save_res +from utility import init_args + +logger = get_logger() +from ppocr.utils.utility import check_and_read_gif, get_image_file_list +from ppocr.utils.network import maybe_download, download_with_progressbar + +__all__ = ['PaddleStructure'] + +VERSION = '2.1' +BASE_DIR = os.path.expanduser("~/.paddlestructure/") + +model_urls = { + 'det': { + 'https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_det_infer.tar', + }, + 'rec': { + 'https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_det_infer.tar', + }, + 'structure': { + 'https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_det_infer.tar', + }, +} + + +def parse_args(mMain=True): + import argparse + parser = init_args() + parser.add_help = mMain + + for action in parser._actions: + if action.dest in ['rec_char_dict_path', 'structure_char_dict_path']: + action.default = None + if mMain: + return parser.parse_args() + else: + inference_args_dict = {} + for action in parser._actions: + inference_args_dict[action.dest] = action.default + return argparse.Namespace(**inference_args_dict) + + +class PaddleStructure(OCRSystem): + def __init__(self, **kwargs): + params = parse_args(mMain=False) + params.__dict__.update(**kwargs) + if params.show_log: + logger.setLevel(logging.DEBUG) + params.use_angle_cls = False + # init model dir + if params.det_model_dir is None: + params.det_model_dir = os.path.join(BASE_DIR, VERSION, 'det') + if params.rec_model_dir is None: + params.rec_model_dir = os.path.join(BASE_DIR, VERSION, 'rec') + if params.structure_model_dir is None: + params.structure_model_dir = os.path.join(BASE_DIR, VERSION, 'structure') + # download model + maybe_download(params.det_model_dir, model_urls['det']) + maybe_download(params.det_model_dir, model_urls['rec']) + maybe_download(params.det_model_dir, model_urls['structure']) + + if params.rec_char_dict_path is None: + params.rec_char_type = 'EN' + if os.path.exists(str(Path(__file__).parent / 'ppocr/utils/dict/table_dict.txt')): + params.rec_char_dict_path = str(Path(__file__).parent / 'ppocr/utils/dict/table_dict.txt') + else: + params.rec_char_dict_path = str(Path(__file__).parent.parent / 'ppocr/utils/dict/table_dict.txt') + if params.structure_char_dict_path is None: + if os.path.exists(str(Path(__file__).parent / 'ppocr/utils/dict/table_structure_dict.txt')): + params.structure_char_dict_path = str(Path(__file__).parent / 'ppocr/utils/dict/table_structure_dict.txt') + else: + params.structure_char_dict_path = str(Path(__file__).parent.parent / 'ppocr/utils/dict/table_structure_dict.txt') + + print(params) + super().__init__(params) + + def __call__(self, img): + if isinstance(img, str): + # download net image + if img.startswith('http'): + download_with_progressbar(img, 'tmp.jpg') + img = 'tmp.jpg' + image_file = img + img, flag = check_and_read_gif(image_file) + if not flag: + with open(image_file, 'rb') as f: + np_arr = np.frombuffer(f.read(), dtype=np.uint8) + img = cv2.imdecode(np_arr, cv2.IMREAD_COLOR) + if img is None: + logger.error("error in loading image:{}".format(image_file)) + return None + if isinstance(img, np.ndarray) and len(img.shape) == 2: + img = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR) + + res = super().__call__(img) + return res + + +def main(): + # for cmd + args = parse_args(mMain=True) + image_dir = args.image_dir + save_folder = args.output + if image_dir.startswith('http'): + download_with_progressbar(image_dir, 'tmp.jpg') + image_file_list = ['tmp.jpg'] + else: + image_file_list = get_image_file_list(args.image_dir) + if len(image_file_list) == 0: + logger.error('no images find in {}'.format(args.image_dir)) + return + + structure_engine = PaddleStructure(**(args.__dict__)) + for img_path in image_file_list: + img_name = os.path.basename(img_path).split('.')[0] + logger.info('{}{}{}'.format('*' * 10, img_path, '*' * 10)) + result = structure_engine(img_path) + save_res(result, args.output, os.path.basename(img_path).split('.')[0]) + for item in result: + logger.info(item['res']) + save_res(result, save_folder, img_name) + logger.info('result save to {}'.format(os.path.join(save_folder, img_name))) + + + +if __name__ == '__main__': + table_engine = PaddleStructure(det_model_dir='../inference/table/ch_ppocr_mobile_v2.0_table_det_infer', + rec_model_dir='../inference/table/ch_ppocr_mobile_v2.0_table_rec_infer', + structure_model_dir='../inference/table/ch_ppocr_mobile_v2.0_table_structure_infer', + output='/Users/zhoujun20/Desktop/工作相关/table/table_pr/PaddleOCR/output/table', + show_log=True) + img = cv2.imread('/Users/zhoujun20/Desktop/工作相关/table/table_pr/PaddleOCR/ppstructure/test_imgs/table_1.png') + result = table_engine(img) + for line in result: + print(line) diff --git a/ppstructure/predict_system.py b/ppstructure/predict_system.py index f6852ab7..e40aa8a8 100644 --- a/ppstructure/predict_system.py +++ b/ppstructure/predict_system.py @@ -18,97 +18,93 @@ import subprocess __dir__ = os.path.dirname(os.path.abspath(__file__)) sys.path.append(__dir__) -sys.path.append(os.path.abspath(os.path.join(__dir__, '../..'))) +sys.path.append(os.path.abspath(os.path.join(__dir__, '..'))) os.environ["FLAGS_allocator_strategy"] = 'auto_growth' import cv2 -import copy import numpy as np import time -import tools.infer.utility as utility -from tools.infer.predict_system import TextSystem -from ppstructure.table.predict_table import TableSystem, to_excel -from ppstructure.layout.predict_layout import LayoutDetector + +import layoutparser as lp + from ppocr.utils.utility import get_image_file_list, check_and_read_gif from ppocr.utils.logging import get_logger +from tools.infer.predict_system import TextSystem +from ppstructure.table.predict_table import TableSystem, to_excel +from ppstructure.utility import parse_args logger = get_logger() -def parse_args(): - parser = utility.init_args() - - # params for output - parser.add_argument("--table_output", type=str, default='output/table') - # params for table structure - parser.add_argument("--table_max_len", type=int, default=488) - parser.add_argument("--table_max_text_length", type=int, default=100) - parser.add_argument("--table_max_elem_length", type=int, default=800) - parser.add_argument("--table_max_cell_num", type=int, default=500) - parser.add_argument("--table_model_dir", type=str) - parser.add_argument("--table_char_type", type=str, default='en') - parser.add_argument("--table_char_dict_path", type=str, default="./ppocr/utils/dict/table_structure_dict.txt") - - # params for layout detector - parser.add_argument("--layout_model_dir", type=str) - return parser.parse_args() - - -class OCRSystem(): +class OCRSystem(object): def __init__(self, args): self.text_system = TextSystem(args) - self.table_system = TableSystem(args) - self.table_layout = LayoutDetector(args) + self.table_system = TableSystem(args, self.text_system.text_detector, self.text_system.text_recognizer) + self.table_layout = lp.PaddleDetectionLayoutModel("lp://PubLayNet/ppyolov2_r50vd_dcn_365e_publaynet/config", + threshold=0.5, enable_mkldnn=args.enable_mkldnn, + enforce_cpu=not args.use_gpu) self.use_angle_cls = args.use_angle_cls self.drop_score = args.drop_score def __call__(self, img): ori_im = img.copy() - layout_res = self.table_layout(copy.deepcopy(img)) + layout_res = self.table_layout.detect(img[..., ::-1]) + res_list = [] for region in layout_res: - x1, y1, x2, y2 = region['bbox'] + x1, y1, x2, y2 = region.coordinates + x1, y1, x2, y2 = int(x1), int(y1), int(x2), int(y2) roi_img = ori_im[y1:y2, x1:x2, :] - if region['label'] == 'table': - res = self.text_system(roi_img) + if region.type == 'Table': + res = self.table_system(roi_img) + elif region.type == 'Figure': + continue else: - res = self.text_system(roi_img) - region['res'] = res - return layout_res + filter_boxes, filter_rec_res = self.text_system(roi_img) + filter_boxes = [x.reshape(-1).tolist() for x in filter_boxes] + res = (filter_boxes, filter_rec_res) + res_list.append({'type': region.type, 'bbox': [x1, y1, x2, y2], 'res': res}) + return res_list + + +def save_res(res, save_folder, img_name): + excel_save_folder = os.path.join(save_folder, img_name) + os.makedirs(excel_save_folder, exist_ok=True) + # save res + for region in res: + if region['type'] == 'Table': + excel_path = os.path.join(excel_save_folder, '{}.xlsx'.format(region['bbox'])) + to_excel(region['res'], excel_path) + elif region['type'] == 'Figure': + pass + else: + with open(os.path.join(excel_save_folder, 'res.txt'), 'a', encoding='utf8') as f: + for box, rec_res in zip(*region['res']): + f.write('{}\t{}\n'.format(np.array(box).reshape(-1).tolist(), rec_res)) def main(args): image_file_list = get_image_file_list(args.image_dir) + image_file_list = image_file_list image_file_list = image_file_list[args.process_id::args.total_process_num] - save_folder = args.table_output + save_folder = args.output os.makedirs(save_folder, exist_ok=True) - text_sys = OCRSystem(args) + structure_sys = OCRSystem(args) img_num = len(image_file_list) for i, image_file in enumerate(image_file_list): logger.info("[{}/{}] {}".format(i, img_num, image_file)) img, flag = check_and_read_gif(image_file) img_name = os.path.basename(image_file).split('.')[0] - # excel_path = os.path.join(excel_save_folder, + '.xlsx') + if not flag: img = cv2.imread(image_file) if img is None: - logger.info("error in loading image:{}".format(image_file)) + logger.error("error in loading image:{}".format(image_file)) continue starttime = time.time() - res = text_sys(img) - - excel_save_folder = os.path.join(save_folder, img_name) - os.makedirs(excel_save_folder, exist_ok=True) - # save res - for region in res: - if region['label'] == 'table': - excel_path = os.path.join(excel_save_folder, '{}.xlsx'.format(region['bbox'])) - to_excel(region['res'], excel_path) - else: - with open(os.path.join(excel_save_folder, 'res.txt'),'a',encoding='utf8') as f: - for box, rec_res in zip(*region['res']): - f.write('{}\t{}\n'.format(np.array(box).reshape(-1).tolist(), rec_res)) - logger.info(res) + res = structure_sys(img) + save_res(res, save_folder, img_name) + logger.info('result save to {}'.format(os.path.join(save_folder, img_name))) elapse = time.time() - starttime logger.info("Predict time : {:.3f}s".format(elapse)) diff --git a/ppstructure/setup.py b/ppstructure/setup.py new file mode 100644 index 00000000..493599b7 --- /dev/null +++ b/ppstructure/setup.py @@ -0,0 +1,65 @@ +# Copyright (c) 2020 PaddlePaddle 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. + +from setuptools import setup +from io import open +import shutil + +with open('../requirements.txt', encoding="utf-8-sig") as f: + requirements = f.readlines() + requirements.append('tqdm') + requirements.append('layoutparser') + + +def readme(): + with open('README_ch.md', encoding="utf-8-sig") as f: + README = f.read() + return README + +shutil.copytree('../ppocr','./ppocr') +shutil.copytree('../tools','./tools') +shutil.copytree('../ppstructure','./ppstructure') + +setup( + name='paddlestructure', + packages=['paddlestructure'], + package_dir={'paddlestructure': ''}, + include_package_data=True, + entry_points={"console_scripts": ["paddlestructure= paddlestructure.paddlestructure:main"]}, + version='2.0.6', + install_requires=requirements, + license='Apache License 2.0', + description='Awesome OCR toolkits based on PaddlePaddle (8.6M ultra-lightweight pre-trained model, support training and deployment among server, mobile, embeded and IoT devices', + long_description=readme(), + long_description_content_type='text/markdown', + url='https://github.com/PaddlePaddle/PaddleOCR', + download_url='https://github.com/PaddlePaddle/PaddleOCR.git', + keywords=[ + 'ocr textdetection textrecognition paddleocr crnn east star-net rosetta ocrlite db chineseocr chinesetextdetection chinesetextrecognition' + ], + classifiers=[ + 'Intended Audience :: Developers', 'Operating System :: OS Independent', + 'Natural Language :: Chinese (Simplified)', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.2', + 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', 'Topic :: Utilities' + ], ) + +shutil.rmtree('ppocr') +shutil.rmtree('tools') +shutil.rmtree('ppstructure') \ No newline at end of file diff --git a/ppstructure/table/README_ch.md b/ppstructure/table/README_ch.md index e69de29b..effd1cf2 100644 --- a/ppstructure/table/README_ch.md +++ b/ppstructure/table/README_ch.md @@ -0,0 +1,15 @@ +# 表格结构和内容预测 + +先cd到PaddleOCR/ppstructure目录下 + +预测 +```python +python3 table/predict_table.py --det_model_dir=../inference/db --rec_model_dir=../inference/rec_mv3_large1.0/infer --table_model_dir=../inference/explite3/infer --image_dir=../table/imgs/PMC3006023_004_00.png --rec_char_dict_path=../ppocr/utils/dict/table_dict.txt --table_char_dict_path=../ppocr/utils/dict/table_structure_dict.txt --rec_char_type=EN --det_limit_side_len=736 --det_limit_type=min --table_output ../output/table +``` +运行完成后,每张图片的excel表格会保存到table_output字段指定的目录下 + +eval + +```python +python3 table/eval_table.py --det_model_dir=../inference/db --rec_model_dir=../inference/rec_mv3_large1.0/infer --table_model_dir=../inference/explite3/infer --image_dir=../table/imgs --rec_char_dict_path=../ppocr/utils/dict/table_dict.txt --table_char_dict_path=../ppocr/utils/dict/table_structure_dict.txt --rec_char_type=EN --det_limit_side_len=736 --det_limit_type=min --gt_path=path/to/gt.json +``` diff --git a/ppstructure/table/eval_table.py b/ppstructure/table/eval_table.py index 46df68df..0ba7acbc 100755 --- a/ppstructure/table/eval_table.py +++ b/ppstructure/table/eval_table.py @@ -15,16 +15,21 @@ import os import sys __dir__ = os.path.dirname(os.path.abspath(__file__)) sys.path.append(__dir__) -sys.path.append(os.path.abspath(os.path.join(__dir__, '..'))) +sys.path.append(os.path.abspath(os.path.join(__dir__, '../..'))) import cv2 import json from tqdm import tqdm from ppstructure.table.table_metric import TEDS from ppstructure.table.predict_table import TableSystem -from ppstructure.predict_system import parse_args +from ppstructure.utility import init_args +def parse_args(): + parser = init_args() + parser.add_argument("--gt_path", type=str) + return parser.parse_args() + def main(gt_path, img_root, args): teds = TEDS(n_jobs=16) @@ -33,6 +38,8 @@ def main(gt_path, img_root, args): pred_htmls = [] gt_htmls = [] for img_name in tqdm(jsons_gt): + if img_name != 'PMC1064865_002_00.png': + continue # 读取信息 img = cv2.imread(os.path.join(img_root,img_name)) pred_html = text_sys(img) @@ -61,6 +68,4 @@ def get_gt_html(gt_structures, contents_with_block): if __name__ == '__main__': args = parse_args() - gt_path = 'table/match_code/f_gt_bbox.json' - img_root = 'table/imgs' - main(gt_path,img_root, args) + main(args.gt_path,args.image_dir, args) diff --git a/ppstructure/table/matcher.py b/ppstructure/table/matcher.py index 711806aa..b3c70430 100755 --- a/ppstructure/table/matcher.py +++ b/ppstructure/table/matcher.py @@ -194,21 +194,3 @@ def matcher_structure(gt_bboxes, pred_bboxes_rows, pred_bboxes): matched[index].append(i) pre_bbox = gt_box return matched - - -def main(): - detect_bboxes = json.load(open('./f_detecion_bbox.json')) - gt_bboxes = json.load(open('./f_gt_bbox.json')) - all_node = 0 - matched_right = 0 - key = 'PMC4796501_003_00.png' - print(key) - gt_bbox = gt_bboxes[key] - pred_bbox = detect_bboxes[key] - matched = matcher(gt_bbox, pred_bbox) - print(matched) - - -if __name__ == "__main__": - main() - diff --git a/ppstructure/table/predict_structure.py b/ppstructure/table/predict_structure.py index fd00dfd1..6e680b35 100755 --- a/ppstructure/table/predict_structure.py +++ b/ppstructure/table/predict_structure.py @@ -40,7 +40,7 @@ class TableStructurer(object): def __init__(self, args): pre_process_list = [{ 'ResizeTableImage': { - 'max_len': args.table_max_len + 'max_len': args.structure_max_len } }, { 'NormalizeImage': { @@ -60,17 +60,17 @@ class TableStructurer(object): }] postprocess_params = { 'name': 'TableLabelDecode', - "character_type": args.table_char_type, - "character_dict_path": args.table_char_dict_path, - "max_text_length": args.table_max_text_length, - "max_elem_length": args.table_max_elem_length, - "max_cell_num": args.table_max_cell_num + "character_type": args.structure_char_type, + "character_dict_path": args.structure_char_dict_path, + "max_text_length": args.structure_max_text_length, + "max_elem_length": args.structure_max_elem_length, + "max_cell_num": args.structure_max_cell_num } self.preprocess_op = create_operators(pre_process_list) self.postprocess_op = build_post_process(postprocess_params) self.predictor, self.input_tensor, self.output_tensors = \ - utility.create_predictor(args, 'table', logger) + utility.create_predictor(args, 'structure', logger) def __call__(self, img): ori_im = img.copy() diff --git a/ppstructure/table/predict_table.py b/ppstructure/table/predict_table.py index 30e503e5..4a247e40 100644 --- a/ppstructure/table/predict_table.py +++ b/ppstructure/table/predict_table.py @@ -18,6 +18,7 @@ import subprocess __dir__ = os.path.dirname(os.path.abspath(__file__)) sys.path.append(__dir__) +sys.path.append(os.path.abspath(os.path.join(__dir__, '..'))) sys.path.append(os.path.abspath(os.path.join(__dir__, '../..'))) os.environ["FLAGS_allocator_strategy"] = 'auto_growth' @@ -25,13 +26,13 @@ import cv2 import copy import numpy as np import time -import tools.infer.utility as utility import tools.infer.predict_rec as predict_rec import tools.infer.predict_det as predict_det import ppstructure.table.predict_structure as predict_strture from ppocr.utils.utility import get_image_file_list, check_and_read_gif from ppocr.utils.logging import get_logger -from ppstructure.table.matcher import distance, compute_iou +from matcher import distance, compute_iou +from ppstructure.utility import parse_args logger = get_logger() @@ -52,12 +53,10 @@ def expand(pix, det_box, shape): class TableSystem(object): - def __init__(self, args): - self.text_detector = predict_det.TextDetector(args) - self.text_recognizer = predict_rec.TextRecognizer(args) + def __init__(self, args, text_detector=None, text_recognizer=None): + self.text_detector = predict_det.TextDetector(args) if text_detector is None else text_detector + self.text_recognizer = predict_rec.TextRecognizer(args) if text_recognizer is None else text_recognizer self.table_structurer = predict_strture.TableStructurer(args) - self.use_angle_cls = args.use_angle_cls - self.drop_score = args.drop_score def __call__(self, img): ori_im = img.copy() @@ -75,8 +74,8 @@ class TableSystem(object): r_boxes.append(box) dt_boxes = np.array(r_boxes) - # logger.info("dt_boxes num : {}, elapse : {}".format( - # len(dt_boxes), elapse)) + logger.debug("dt_boxes num : {}, elapse : {}".format( + len(dt_boxes), elapse)) if dt_boxes is None: return None, None img_crop_list = [] @@ -87,8 +86,8 @@ class TableSystem(object): text_rect = ori_im[int(y0):int(y1), int(x0):int(x1), :] img_crop_list.append(text_rect) rec_res, elapse = self.text_recognizer(img_crop_list) - # logger.info("rec_res num : {}, elapse : {}".format( - # len(rec_res), elapse)) + logger.debug("rec_res num : {}, elapse : {}".format( + len(rec_res), elapse)) pred_html, pred = self.rebuild_table(structure_res, dt_boxes, rec_res) return pred_html @@ -172,6 +171,7 @@ def sorted_boxes(dt_boxes): _boxes[i + 1] = tmp return _boxes + def to_excel(html_table, excel_path): from tablepyxl import tablepyxl tablepyxl.document_to_xl(html_table, excel_path) @@ -180,19 +180,18 @@ def to_excel(html_table, excel_path): def main(args): image_file_list = get_image_file_list(args.image_dir) image_file_list = image_file_list[args.process_id::args.total_process_num] - excel_save_folder = 'output/table' - os.makedirs(excel_save_folder, exist_ok=True) + os.makedirs(args.output, exist_ok=True) text_sys = TableSystem(args) img_num = len(image_file_list) for i, image_file in enumerate(image_file_list): logger.info("[{}/{}] {}".format(i, img_num, image_file)) img, flag = check_and_read_gif(image_file) - excel_path = os.path.join(excel_save_folder, os.path.basename(image_file).split('.')[0] + '.xlsx') + excel_path = os.path.join(args.table_output, os.path.basename(image_file).split('.')[0] + '.xlsx') if not flag: img = cv2.imread(image_file) if img is None: - logger.info("error in loading image:{}".format(image_file)) + logger.error("error in loading image:{}".format(image_file)) continue starttime = time.time() pred_html = text_sys(img) @@ -205,7 +204,7 @@ def main(args): if __name__ == "__main__": - args = utility.parse_args() + args = parse_args() if args.use_mp: p_list = [] total_process_num = args.total_process_num diff --git a/ppstructure/utility.py b/ppstructure/utility.py new file mode 100644 index 00000000..57659920 --- /dev/null +++ b/ppstructure/utility.py @@ -0,0 +1,40 @@ +# copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +# 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. +import logging + +from tools.infer.utility import str2bool, init_args as infer_args + + +def init_args(): + parser = infer_args() + + # params for output + parser.add_argument("--output", type=str, default='./output/table') + # params for table structure + parser.add_argument("--structure_max_len", type=int, default=488) + parser.add_argument("--structure_max_text_length", type=int, default=100) + parser.add_argument("--structure_max_elem_length", type=int, default=800) + parser.add_argument("--structure_max_cell_num", type=int, default=500) + parser.add_argument("--structure_model_dir", type=str) + parser.add_argument("--structure_char_type", type=str, default='en') + parser.add_argument("--structure_char_dict_path", type=str, default="../ppocr/utils/dict/table_structure_dict.txt") + + # params for layout detector + parser.add_argument("--layout_model_dir", type=str) + return parser + + +def parse_args(): + parser = init_args() + return parser.parse_args() diff --git a/tools/infer/predict_system.py b/tools/infer/predict_system.py index 78f5a472..235a075b 100755 --- a/tools/infer/predict_system.py +++ b/tools/infer/predict_system.py @@ -88,7 +88,7 @@ class TextSystem(object): def __call__(self, img, cls=True): ori_im = img.copy() dt_boxes, elapse = self.text_detector(img) - logger.info("dt_boxes num : {}, elapse : {}".format( + logger.debug("dt_boxes num : {}, elapse : {}".format( len(dt_boxes), elapse)) if dt_boxes is None: return None, None @@ -103,11 +103,11 @@ class TextSystem(object): if self.use_angle_cls and cls: img_crop_list, angle_list, elapse = self.text_classifier( img_crop_list) - logger.info("cls num : {}, elapse : {}".format( + logger.debug("cls num : {}, elapse : {}".format( len(img_crop_list), elapse)) rec_res, elapse = self.text_recognizer(img_crop_list) - logger.info("rec_res num : {}, elapse : {}".format( + logger.debug("rec_res num : {}, elapse : {}".format( len(rec_res), elapse)) # self.print_draw_crop_rec_res(img_crop_list, rec_res) filter_boxes, filter_rec_res = [], [] @@ -152,7 +152,7 @@ def main(args): if not flag: img = cv2.imread(image_file) if img is None: - logger.info("error in loading image:{}".format(image_file)) + logger.error("error in loading image:{}".format(image_file)) continue starttime = time.time() dt_boxes, rec_res = text_sys(img) diff --git a/tools/infer/utility.py b/tools/infer/utility.py index 956df5ca..a558f490 100755 --- a/tools/infer/utility.py +++ b/tools/infer/utility.py @@ -109,7 +109,7 @@ def init_args(): parser.add_argument("--use_mp", type=str2bool, default=False) parser.add_argument("--total_process_num", type=int, default=1) parser.add_argument("--process_id", type=int, default=0) - + parser.add_argument("--show_log", type=str2bool, default=True) return parser @@ -125,8 +125,8 @@ def create_predictor(args, mode, logger): model_dir = args.cls_model_dir elif mode == 'rec': model_dir = args.rec_model_dir - elif mode == 'table': - model_dir = args.table_model_dir + elif mode == 'structure': + model_dir = args.structure_model_dir else: model_dir = args.e2e_model_dir @@ -246,7 +246,8 @@ def create_predictor(args, mode, logger): config.delete_pass("conv_transpose_eltwiseadd_bn_fuse_pass") config.switch_use_feed_fetch_ops(False) - if mode == 'table': + config.switch_ir_optim(True) + if mode == 'structure': config.switch_ir_optim(False) # create predictor predictor = inference.create_predictor(config) From 85ae1eb8c1dd9ddbae59c9679803d66350dc66bf Mon Sep 17 00:00:00 2001 From: WenmuZhou Date: Sat, 5 Jun 2021 22:01:54 +0800 Subject: [PATCH 09/29] =?UTF-8?q?=E5=88=A0=E9=99=A4main?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ppstructure/paddlestructure.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/ppstructure/paddlestructure.py b/ppstructure/paddlestructure.py index c2db42c1..7c25f9c0 100644 --- a/ppstructure/paddlestructure.py +++ b/ppstructure/paddlestructure.py @@ -146,16 +146,3 @@ def main(): logger.info(item['res']) save_res(result, save_folder, img_name) logger.info('result save to {}'.format(os.path.join(save_folder, img_name))) - - - -if __name__ == '__main__': - table_engine = PaddleStructure(det_model_dir='../inference/table/ch_ppocr_mobile_v2.0_table_det_infer', - rec_model_dir='../inference/table/ch_ppocr_mobile_v2.0_table_rec_infer', - structure_model_dir='../inference/table/ch_ppocr_mobile_v2.0_table_structure_infer', - output='/Users/zhoujun20/Desktop/工作相关/table/table_pr/PaddleOCR/output/table', - show_log=True) - img = cv2.imread('/Users/zhoujun20/Desktop/工作相关/table/table_pr/PaddleOCR/ppstructure/test_imgs/table_1.png') - result = table_engine(img) - for line in result: - print(line) From 8f50ceb0ed2565306ed2ea292e4dcd6248afdec0 Mon Sep 17 00:00:00 2001 From: WenmuZhou Date: Sat, 5 Jun 2021 22:02:15 +0800 Subject: [PATCH 10/29] =?UTF-8?q?=E6=B7=BB=E5=8A=A0TableStructurer=20init?= =?UTF-8?q?=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ppstructure/__init__.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 ppstructure/__init__.py diff --git a/ppstructure/__init__.py b/ppstructure/__init__.py new file mode 100644 index 00000000..e4fc8196 --- /dev/null +++ b/ppstructure/__init__.py @@ -0,0 +1,17 @@ +# copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +# 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. + +from .paddlestructure import PaddleStructure + +__all__ = ['PaddleStructure'] \ No newline at end of file From 864af3db34ad271670d46e4cdb2ddbebe429a6a6 Mon Sep 17 00:00:00 2001 From: WenmuZhou Date: Sat, 5 Jun 2021 23:46:30 +0800 Subject: [PATCH 11/29] =?UTF-8?q?=E4=BF=AE=E5=A4=8Dwhl=E5=8C=85bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- MANIFEST.in | 2 +- ppstructure/MANIFEST.in | 6 +++--- ppstructure/README_ch.md | 1 + ppstructure/paddlestructure.py | 25 ++++++++++++++++++++----- ppstructure/setup.py | 15 +++++++++++---- ppstructure/table/README_ch.md | 2 +- ppstructure/table/predict_table.py | 4 ++-- 7 files changed, 39 insertions(+), 16 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index cd34d574..cd1c9636 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,4 @@ -include LICENSE.txt +include LICENSE include README.md recursive-include ppocr/utils *.txt utility.py logging.py network.py diff --git a/ppstructure/MANIFEST.in b/ppstructure/MANIFEST.in index f9bd0fe9..2961e722 100644 --- a/ppstructure/MANIFEST.in +++ b/ppstructure/MANIFEST.in @@ -1,9 +1,9 @@ -include LICENSE.txt +include LICENSE include README.md recursive-include ppocr/utils *.txt utility.py logging.py network.py recursive-include ppocr/data/ *.py recursive-include ppocr/postprocess *.py recursive-include tools/infer *.py -recursive-include table *.py -recursive-include ppstructure *.py \ No newline at end of file +recursive-include ppstructure *.py + diff --git a/ppstructure/README_ch.md b/ppstructure/README_ch.md index e69de29b..7ad154f8 100644 --- a/ppstructure/README_ch.md +++ b/ppstructure/README_ch.md @@ -0,0 +1 @@ +# TableStructurer \ No newline at end of file diff --git a/ppstructure/paddlestructure.py b/ppstructure/paddlestructure.py index 7c25f9c0..cf49fd99 100644 --- a/ppstructure/paddlestructure.py +++ b/ppstructure/paddlestructure.py @@ -16,15 +16,15 @@ import os import sys __dir__ = os.path.dirname(__file__) -sys.path.append(os.path.join(__dir__, '')) - +sys.path.append(__dir__) +sys.path.append(os.path.join(__dir__, '..')) import cv2 import numpy as np from pathlib import Path from ppocr.utils.logging import get_logger -from predict_system import OCRSystem, save_res +from ppstructure.predict_system import OCRSystem, save_res from utility import init_args logger = get_logger() @@ -93,9 +93,11 @@ class PaddleStructure(OCRSystem): params.rec_char_dict_path = str(Path(__file__).parent.parent / 'ppocr/utils/dict/table_dict.txt') if params.structure_char_dict_path is None: if os.path.exists(str(Path(__file__).parent / 'ppocr/utils/dict/table_structure_dict.txt')): - params.structure_char_dict_path = str(Path(__file__).parent / 'ppocr/utils/dict/table_structure_dict.txt') + params.structure_char_dict_path = str( + Path(__file__).parent / 'ppocr/utils/dict/table_structure_dict.txt') else: - params.structure_char_dict_path = str(Path(__file__).parent.parent / 'ppocr/utils/dict/table_structure_dict.txt') + params.structure_char_dict_path = str( + Path(__file__).parent.parent / 'ppocr/utils/dict/table_structure_dict.txt') print(params) super().__init__(params) @@ -146,3 +148,16 @@ def main(): logger.info(item['res']) save_res(result, save_folder, img_name) logger.info('result save to {}'.format(os.path.join(save_folder, img_name))) + + +if __name__ == '__main__': + table_engine = PaddleStructure( + det_model_dir='/Users/zhoujun20/Desktop/工作相关/table/table_pr/PaddleOCR/inference/table/ch_ppocr_mobile_v2.0_table_det_infer', + rec_model_dir='/Users/zhoujun20/Desktop/工作相关/table/table_pr/PaddleOCR/inference/table/ch_ppocr_mobile_v2.0_table_rec_infer', + structure_model_dir='/Users/zhoujun20/Desktop/工作相关/table/table_pr/PaddleOCR/inference/table/ch_ppocr_mobile_v2.0_table_structure_infer', + output='/Users/zhoujun20/Desktop/工作相关/table/table_pr/PaddleOCR/output/table', + show_log=True) + img = cv2.imread('/Users/zhoujun20/Desktop/工作相关/table/table_pr/PaddleOCR/ppstructure/test_imgs/table_1.png') + result = table_engine(img) + for line in result: + print(line) diff --git a/ppstructure/setup.py b/ppstructure/setup.py index 493599b7..0d7b2b9a 100644 --- a/ppstructure/setup.py +++ b/ppstructure/setup.py @@ -11,6 +11,7 @@ # 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. +import os from setuptools import setup from io import open @@ -20,6 +21,7 @@ with open('../requirements.txt', encoding="utf-8-sig") as f: requirements = f.readlines() requirements.append('tqdm') requirements.append('layoutparser') + requirements.append('iopath') def readme(): @@ -27,9 +29,13 @@ def readme(): README = f.read() return README -shutil.copytree('../ppocr','./ppocr') -shutil.copytree('../tools','./tools') -shutil.copytree('../ppstructure','./ppstructure') + +shutil.copytree('../ppstructure/table', './ppstructure/table') +shutil.copyfile('../ppstructure/predict_system.py', './ppstructure/predict_system.py') +shutil.copyfile('../ppstructure/utility.py', './ppstructure/utility.py') +shutil.copytree('../ppocr', './ppocr') +shutil.copytree('../tools', './tools') +shutil.copyfile('../LICENSE', './LICENSE') setup( name='paddlestructure', @@ -62,4 +68,5 @@ setup( shutil.rmtree('ppocr') shutil.rmtree('tools') -shutil.rmtree('ppstructure') \ No newline at end of file +shutil.rmtree('ppstructure') +os.remove('LICENSE') diff --git a/ppstructure/table/README_ch.md b/ppstructure/table/README_ch.md index effd1cf2..10523106 100644 --- a/ppstructure/table/README_ch.md +++ b/ppstructure/table/README_ch.md @@ -8,7 +8,7 @@ python3 table/predict_table.py --det_model_dir=../inference/db --rec_model_dir=. ``` 运行完成后,每张图片的excel表格会保存到table_output字段指定的目录下 -eval +评估 ```python python3 table/eval_table.py --det_model_dir=../inference/db --rec_model_dir=../inference/rec_mv3_large1.0/infer --table_model_dir=../inference/explite3/infer --image_dir=../table/imgs --rec_char_dict_path=../ppocr/utils/dict/table_dict.txt --table_char_dict_path=../ppocr/utils/dict/table_structure_dict.txt --rec_char_type=EN --det_limit_side_len=736 --det_limit_type=min --gt_path=path/to/gt.json diff --git a/ppstructure/table/predict_table.py b/ppstructure/table/predict_table.py index 4a247e40..c4edd22c 100644 --- a/ppstructure/table/predict_table.py +++ b/ppstructure/table/predict_table.py @@ -28,11 +28,11 @@ import numpy as np import time import tools.infer.predict_rec as predict_rec import tools.infer.predict_det as predict_det -import ppstructure.table.predict_structure as predict_strture from ppocr.utils.utility import get_image_file_list, check_and_read_gif from ppocr.utils.logging import get_logger -from matcher import distance, compute_iou +from ppstructure.table.matcher import distance, compute_iou from ppstructure.utility import parse_args +import ppstructure.table.predict_structure as predict_strture logger = get_logger() From 59671466e3ff83f94b42425413c2b52f199256a9 Mon Sep 17 00:00:00 2001 From: WenmuZhou Date: Mon, 7 Jun 2021 13:52:52 +0800 Subject: [PATCH 12/29] opt output --- ppstructure/predict_system.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ppstructure/predict_system.py b/ppstructure/predict_system.py index e40aa8a8..14f7115b 100644 --- a/ppstructure/predict_system.py +++ b/ppstructure/predict_system.py @@ -78,7 +78,7 @@ def save_res(res, save_folder, img_name): pass else: with open(os.path.join(excel_save_folder, 'res.txt'), 'a', encoding='utf8') as f: - for box, rec_res in zip(*region['res']): + for box, rec_res in zip(region['res'][0],region['res'][1]): f.write('{}\t{}\n'.format(np.array(box).reshape(-1).tolist(), rec_res)) From 48eba0289494757079174edec6c1c9eedfd6d687 Mon Sep 17 00:00:00 2001 From: WenmuZhou Date: Mon, 7 Jun 2021 14:45:25 +0800 Subject: [PATCH 13/29] opt output --- ppstructure/predict_system.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/ppstructure/predict_system.py b/ppstructure/predict_system.py index 14f7115b..ede85018 100644 --- a/ppstructure/predict_system.py +++ b/ppstructure/predict_system.py @@ -42,7 +42,7 @@ class OCRSystem(object): self.table_system = TableSystem(args, self.text_system.text_detector, self.text_system.text_recognizer) self.table_layout = lp.PaddleDetectionLayoutModel("lp://PubLayNet/ppyolov2_r50vd_dcn_365e_publaynet/config", threshold=0.5, enable_mkldnn=args.enable_mkldnn, - enforce_cpu=not args.use_gpu) + enforce_cpu=not args.use_gpu,thread_num=args.cpu_threads) self.use_angle_cls = args.use_angle_cls self.drop_score = args.drop_score @@ -60,7 +60,9 @@ class OCRSystem(object): continue else: filter_boxes, filter_rec_res = self.text_system(roi_img) + filter_boxes = [x + [x1, y1] for x in filter_boxes] filter_boxes = [x.reshape(-1).tolist() for x in filter_boxes] + res = (filter_boxes, filter_rec_res) res_list.append({'type': region.type, 'bbox': [x1, y1, x2, y2], 'res': res}) return res_list @@ -78,7 +80,7 @@ def save_res(res, save_folder, img_name): pass else: with open(os.path.join(excel_save_folder, 'res.txt'), 'a', encoding='utf8') as f: - for box, rec_res in zip(region['res'][0],region['res'][1]): + for box, rec_res in zip(region['res'][0], region['res'][1]): f.write('{}\t{}\n'.format(np.array(box).reshape(-1).tolist(), rec_res)) From dec76eb75da6fd5ad799039c8b200ab853f52b54 Mon Sep 17 00:00:00 2001 From: WenmuZhou Date: Tue, 8 Jun 2021 10:52:47 +0800 Subject: [PATCH 14/29] add pad for small image in det --- ppocr/data/imaug/operators.py | 12 ++++++++++-- ppocr/postprocess/db_postprocess.py | 11 +++++------ ppstructure/predict_system.py | 5 +++-- tools/infer/predict_det.py | 4 +++- tools/infer/utility.py | 2 ++ 5 files changed, 23 insertions(+), 11 deletions(-) diff --git a/ppocr/data/imaug/operators.py b/ppocr/data/imaug/operators.py index 9c48b096..ed81d41a 100644 --- a/ppocr/data/imaug/operators.py +++ b/ppocr/data/imaug/operators.py @@ -81,7 +81,7 @@ class NormalizeImage(object): assert isinstance(img, np.ndarray), "invalid input 'img' in NormalizeImage" data['image'] = ( - img.astype('float32') * self.scale - self.mean) / self.std + img.astype('float32') * self.scale - self.mean) / self.std return data @@ -122,6 +122,8 @@ class DetResizeForTest(object): elif 'limit_side_len' in kwargs: self.limit_side_len = kwargs['limit_side_len'] self.limit_type = kwargs.get('limit_type', 'min') + self.pad = kwargs.get('pad', False) + self.pad_size = kwargs.get('pad_size', 480) elif 'resize_long' in kwargs: self.resize_type = 2 self.resize_long = kwargs.get('resize_long', 960) @@ -163,7 +165,7 @@ class DetResizeForTest(object): img, (ratio_h, ratio_w) """ limit_side_len = self.limit_side_len - h, w, _ = img.shape + h, w, c = img.shape # limit the max side if self.limit_type == 'max': @@ -172,6 +174,8 @@ class DetResizeForTest(object): ratio = float(limit_side_len) / h else: ratio = float(limit_side_len) / w + elif self.pad: + ratio = float(self.pad_size) / max(h, w) else: ratio = 1. else: @@ -197,6 +201,10 @@ class DetResizeForTest(object): sys.exit(0) ratio_h = resize_h / float(h) ratio_w = resize_w / float(w) + if self.limit_type == 'max' and self.pad: + padding_im = np.zeros((self.pad_size, self.pad_size, c), dtype=np.float32) + padding_im[:resize_h, :resize_w, :] = img + img = padding_im return img, [ratio_h, ratio_w] def resize_image_type2(self, img): diff --git a/ppocr/postprocess/db_postprocess.py b/ppocr/postprocess/db_postprocess.py index 769ddbe2..0c149610 100755 --- a/ppocr/postprocess/db_postprocess.py +++ b/ppocr/postprocess/db_postprocess.py @@ -49,12 +49,12 @@ class DBPostProcess(object): self.dilation_kernel = None if not use_dilation else np.array( [[1, 1], [1, 1]]) - def boxes_from_bitmap(self, pred, _bitmap, dest_width, dest_height): + def boxes_from_bitmap(self, pred, _bitmap, shape): ''' _bitmap: single map with shape (1, H, W), whose values are binarized as {0, 1} ''' - + dest_height, dest_width, ratio_h, ratio_w = shape bitmap = _bitmap height, width = bitmap.shape @@ -89,9 +89,9 @@ class DBPostProcess(object): box = np.array(box) box[:, 0] = np.clip( - np.round(box[:, 0] / width * dest_width), 0, dest_width) + np.round(box[:, 0] / ratio_w), 0, dest_width) box[:, 1] = np.clip( - np.round(box[:, 1] / height * dest_height), 0, dest_height) + np.round(box[:, 1] / ratio_h), 0, dest_height) boxes.append(box.astype(np.int16)) scores.append(score) return np.array(boxes, dtype=np.int16), scores @@ -175,7 +175,6 @@ class DBPostProcess(object): boxes_batch = [] for batch_index in range(pred.shape[0]): - src_h, src_w, ratio_h, ratio_w = shape_list[batch_index] if self.dilation_kernel is not None: mask = cv2.dilate( np.array(segmentation[batch_index]).astype(np.uint8), @@ -183,7 +182,7 @@ class DBPostProcess(object): else: mask = segmentation[batch_index] boxes, scores = self.boxes_from_bitmap(pred[batch_index], mask, - src_w, src_h) + shape_list[batch_index]) boxes_batch.append({'points': boxes}) return boxes_batch diff --git a/ppstructure/predict_system.py b/ppstructure/predict_system.py index ede85018..87306eae 100644 --- a/ppstructure/predict_system.py +++ b/ppstructure/predict_system.py @@ -38,11 +38,13 @@ logger = get_logger() class OCRSystem(object): def __init__(self, args): + args.det_pad = True + args.det_pad_size = 640 self.text_system = TextSystem(args) self.table_system = TableSystem(args, self.text_system.text_detector, self.text_system.text_recognizer) self.table_layout = lp.PaddleDetectionLayoutModel("lp://PubLayNet/ppyolov2_r50vd_dcn_365e_publaynet/config", threshold=0.5, enable_mkldnn=args.enable_mkldnn, - enforce_cpu=not args.use_gpu,thread_num=args.cpu_threads) + enforce_cpu=not args.use_gpu, thread_num=args.cpu_threads) self.use_angle_cls = args.use_angle_cls self.drop_score = args.drop_score @@ -67,7 +69,6 @@ class OCRSystem(object): res_list.append({'type': region.type, 'bbox': [x1, y1, x2, y2], 'res': res}) return res_list - def save_res(res, save_folder, img_name): excel_save_folder = os.path.join(save_folder, img_name) os.makedirs(excel_save_folder, exist_ok=True) diff --git a/tools/infer/predict_det.py b/tools/infer/predict_det.py index 59bb49f9..b21db4c7 100755 --- a/tools/infer/predict_det.py +++ b/tools/infer/predict_det.py @@ -41,7 +41,9 @@ class TextDetector(object): pre_process_list = [{ 'DetResizeForTest': { 'limit_side_len': args.det_limit_side_len, - 'limit_type': args.det_limit_type + 'limit_type': args.det_limit_type, + 'pad':args.det_pad, + 'pad_size':args.det_pad_size } }, { 'NormalizeImage': { diff --git a/tools/infer/utility.py b/tools/infer/utility.py index a558f490..9fb2e8e5 100755 --- a/tools/infer/utility.py +++ b/tools/infer/utility.py @@ -46,6 +46,8 @@ def init_args(): parser.add_argument("--det_model_dir", type=str) parser.add_argument("--det_limit_side_len", type=float, default=960) parser.add_argument("--det_limit_type", type=str, default='max') + parser.add_argument("--det_pad", type=str2bool, default=False) + parser.add_argument("--det_pad_size", type=int, default=640) # DB parmas parser.add_argument("--det_db_thresh", type=float, default=0.3) From d27360a9b8b638112587f9d23d1054a8333a2c57 Mon Sep 17 00:00:00 2001 From: WenmuZhou Date: Tue, 8 Jun 2021 14:16:32 +0800 Subject: [PATCH 15/29] move draw_result to utilitu.py --- ppstructure/paddlestructure.py | 31 +++++++------------------------ ppstructure/predict_system.py | 7 +++++-- ppstructure/utility.py | 23 +++++++++++++++++++++-- 3 files changed, 33 insertions(+), 28 deletions(-) diff --git a/ppstructure/paddlestructure.py b/ppstructure/paddlestructure.py index cf49fd99..686267e1 100644 --- a/ppstructure/paddlestructure.py +++ b/ppstructure/paddlestructure.py @@ -25,27 +25,23 @@ from pathlib import Path from ppocr.utils.logging import get_logger from ppstructure.predict_system import OCRSystem, save_res -from utility import init_args +from ppstructure.table.predict_table import to_excel +from ppstructure.utility import init_args, draw_result logger = get_logger() from ppocr.utils.utility import check_and_read_gif, get_image_file_list from ppocr.utils.network import maybe_download, download_with_progressbar -__all__ = ['PaddleStructure'] +__all__ = ['PaddleStructure', 'draw_result', 'to_excel'] VERSION = '2.1' BASE_DIR = os.path.expanduser("~/.paddlestructure/") model_urls = { - 'det': { - 'https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_det_infer.tar', - }, - 'rec': { - 'https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_det_infer.tar', - }, - 'structure': { - 'https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_det_infer.tar', - }, + 'det': 'https://paddleocr.bj.bcebos.com/dygraph_v2.0/table/en_ppocr_mobile_v2.0_table_det_infer.tar', + 'rec': 'https://paddleocr.bj.bcebos.com/dygraph_v2.0/table/en_ppocr_mobile_v2.0_table_rec_infer.tar', + 'structure': 'https://paddleocr.bj.bcebos.com/dygraph_v2.0/table/en_ppocr_mobile_v2.0_table_structure_infer.tar' + } @@ -143,21 +139,8 @@ def main(): img_name = os.path.basename(img_path).split('.')[0] logger.info('{}{}{}'.format('*' * 10, img_path, '*' * 10)) result = structure_engine(img_path) - save_res(result, args.output, os.path.basename(img_path).split('.')[0]) for item in result: logger.info(item['res']) save_res(result, save_folder, img_name) logger.info('result save to {}'.format(os.path.join(save_folder, img_name))) - -if __name__ == '__main__': - table_engine = PaddleStructure( - det_model_dir='/Users/zhoujun20/Desktop/工作相关/table/table_pr/PaddleOCR/inference/table/ch_ppocr_mobile_v2.0_table_det_infer', - rec_model_dir='/Users/zhoujun20/Desktop/工作相关/table/table_pr/PaddleOCR/inference/table/ch_ppocr_mobile_v2.0_table_rec_infer', - structure_model_dir='/Users/zhoujun20/Desktop/工作相关/table/table_pr/PaddleOCR/inference/table/ch_ppocr_mobile_v2.0_table_structure_infer', - output='/Users/zhoujun20/Desktop/工作相关/table/table_pr/PaddleOCR/output/table', - show_log=True) - img = cv2.imread('/Users/zhoujun20/Desktop/工作相关/table/table_pr/PaddleOCR/ppstructure/test_imgs/table_1.png') - result = table_engine(img) - for line in result: - print(line) diff --git a/ppstructure/predict_system.py b/ppstructure/predict_system.py index 87306eae..907548e7 100644 --- a/ppstructure/predict_system.py +++ b/ppstructure/predict_system.py @@ -31,7 +31,7 @@ from ppocr.utils.utility import get_image_file_list, check_and_read_gif from ppocr.utils.logging import get_logger from tools.infer.predict_system import TextSystem from ppstructure.table.predict_table import TableSystem, to_excel -from ppstructure.utility import parse_args +from ppstructure.utility import parse_args,draw_result logger = get_logger() @@ -39,7 +39,8 @@ logger = get_logger() class OCRSystem(object): def __init__(self, args): args.det_pad = True - args.det_pad_size = 640 + args.det_pad_size = 960 + args.drop_score = 0 self.text_system = TextSystem(args) self.table_system = TableSystem(args, self.text_system.text_detector, self.text_system.text_recognizer) self.table_layout = lp.PaddleDetectionLayoutModel("lp://PubLayNet/ppyolov2_r50vd_dcn_365e_publaynet/config", @@ -107,6 +108,8 @@ def main(args): starttime = time.time() res = structure_sys(img) save_res(res, save_folder, img_name) + draw_img = draw_result(img,res, args.vis_font_path) + cv2.imwrite(os.path.join(save_folder, img_name, 'show.jpg'), draw_img) logger.info('result save to {}'.format(os.path.join(save_folder, img_name))) elapse = time.time() - starttime logger.info("Predict time : {:.3f}s".format(elapse)) diff --git a/ppstructure/utility.py b/ppstructure/utility.py index 57659920..8112b9ef 100644 --- a/ppstructure/utility.py +++ b/ppstructure/utility.py @@ -11,9 +11,10 @@ # 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. -import logging -from tools.infer.utility import str2bool, init_args as infer_args +from PIL import Image +import numpy as np +from tools.infer.utility import draw_ocr_box_txt, init_args as infer_args def init_args(): @@ -38,3 +39,21 @@ def init_args(): def parse_args(): parser = init_args() return parser.parse_args() + + +def draw_result(image, result, font_path): + if isinstance(image, np.ndarray): + image = Image.fromarray(image) + boxes, txts, scores = [], [], [] + for region in result: + if region['type'] == 'Table': + pass + elif region['type'] == 'Figure': + pass + else: + for box, rec_res in zip(region['res'][0], region['res'][1]): + boxes.append(np.array(box).reshape(-1, 2)) + txts.append(rec_res[0]) + scores.append(rec_res[1]) + im_show = draw_ocr_box_txt(image, boxes, txts, scores, font_path=font_path,drop_score=0) + return im_show \ No newline at end of file From 2f62c953e26e7fab369c5ce2e16068d444f96077 Mon Sep 17 00:00:00 2001 From: WenmuZhou Date: Tue, 8 Jun 2021 14:31:42 +0800 Subject: [PATCH 16/29] fix download model error --- ppstructure/paddlestructure.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/ppstructure/paddlestructure.py b/ppstructure/paddlestructure.py index 686267e1..03f90565 100644 --- a/ppstructure/paddlestructure.py +++ b/ppstructure/paddlestructure.py @@ -78,8 +78,8 @@ class PaddleStructure(OCRSystem): params.structure_model_dir = os.path.join(BASE_DIR, VERSION, 'structure') # download model maybe_download(params.det_model_dir, model_urls['det']) - maybe_download(params.det_model_dir, model_urls['rec']) - maybe_download(params.det_model_dir, model_urls['structure']) + maybe_download(params.rec_model_dir, model_urls['rec']) + maybe_download(params.structure_model_dir, model_urls['structure']) if params.rec_char_dict_path is None: params.rec_char_type = 'EN' @@ -143,4 +143,3 @@ def main(): logger.info(item['res']) save_res(result, save_folder, img_name) logger.info('result save to {}'.format(os.path.join(save_folder, img_name))) - From a30dbf415ade6ebf07bef78bd5176055b5a84cb5 Mon Sep 17 00:00:00 2001 From: WenmuZhou Date: Tue, 8 Jun 2021 14:31:53 +0800 Subject: [PATCH 17/29] add simple doc of whl --- ppstructure/README_ch.md | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/ppstructure/README_ch.md b/ppstructure/README_ch.md index 7ad154f8..22505ad8 100644 --- a/ppstructure/README_ch.md +++ b/ppstructure/README_ch.md @@ -1 +1,30 @@ -# TableStructurer \ No newline at end of file +# TableStructurer + +1. 代码使用 +```python +import cv2 +from paddlestructure import PaddleStructure,draw_result + +table_engine = PaddleStructure( + output='./output/table', + show_log=True) + +img_path = '../doc/table/1.png' +img = cv2.imread(img_path) +result = table_engine(img) +for line in result: + print(line) + +from PIL import Image + +font_path = 'path/tp/PaddleOCR/doc/fonts/simfang.ttf' +image = Image.open(img_path).convert('RGB') +im_show = draw_result(image, result,font_path=font_path) +im_show = Image.fromarray(im_show) +im_show.save('result.jpg') +``` + +2. 命令行使用 +```bash +paddlestructure --image_dir=../doc/table/1.png +``` From 4290c697f726f12cbb9d99497bd2a1488df95af7 Mon Sep 17 00:00:00 2001 From: WenmuZhou Date: Tue, 8 Jun 2021 14:32:29 +0800 Subject: [PATCH 18/29] add table example image --- doc/table/1.png | Bin 0 -> 269101 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 doc/table/1.png diff --git a/doc/table/1.png b/doc/table/1.png new file mode 100644 index 0000000000000000000000000000000000000000..47df618ab1bef431a5dd94418c01be16b09d31aa GIT binary patch literal 269101 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%0A9*y$Rh+-MVo(lhMkL~AO>sMZ+A+AWzF`>ap<0? zlXIo42$Mu@4oFCi#&}9?Er?d~KQ1K04%|=Fgt+mwlyV8bO-x_x0hTjSKh-^x^m_=- zNNwAm!37O^*#n3A+UaSrW6IDBIaUlIjOA%x4b4Kt+aBOrJ({-En zck>m|g{S8+G0>0P&P7j)kKUakB&3X7;qAHf|3rsBx^I5z??x4|-7k6$dz^@Pa`MDK zE1TPg9H!yL3;T73-qnXfQ9MoDeE=&HuJ=MEJB92LF}q?3YrQWlG^Izo$GtIv^a{(L z{P|r^kSfe{l!XgRlthVF`vg2b-+vKe-nmP#YSfrxtbXrpai|7Ys;%pdeWUdVV|^GJ zKvZ4HsITBK>pKM1ay|YD79$flY1<%M`n==9RP^}0ie~Em>8LO)oh-vW*$1a34j49> z+96;L|8An~MZidz>i-ap)UY?x>PObsZTU(K#9@ZzPzrbTPyHflV|=gTimOP z4a-ibxi!p-4=nO2Jc|q1AD;Q&Q}P%sCuHEuXZK=?ty+0S<*g0uEF+oh{VMf+4%3%m z?3?=?SSitQKQ_pR0SKP*z&M&zmnVv;PgqmEM0+H>Uwu|!Y_NV>`s}B+)KJyI|DJ0Q zD#5^~-g3bkBTmE$RV{Ai%FBMCBP4ac{o{o$%*+A2$&5G5ljJudaV3S__Dx)4DS2of>w5 z#|m#`xidyvZW1B}ke6~{h@p($j+zj{A~+@mY0Qe;J_?roe(h1ZxfDF3>f>DXa@VG6 z0%(JcRDU9JwZ^eigz;x90%mlxDAezQaK3hu(OrNd)+V!>q51lpdc>ik>$K@oZH{DU%>FXzf{?X1ni{Q|4DaLoe6B_ox8lf z)$>Qy@(g(%P1wBCAdmlfXjqWr(%CHoYu`kPl6{8XFzUQ(xoBOp#*U;t`T^3n;ImCE zjlN0N%j51zJPSx=$@wm8l<_dJaDqRTm&2BP`ak^+*U9jp1>?AnyRl%Yj75>Q$H~SU z*zSoalP0p!SC{?BVLGs2rbzXSNSLqtBRwE$6KfWznJ^Qj_ult#9qbTEBNl!d{43Zr z6e*lrzn75V%eruRj@@6a_x;JBmY}32g+vpdl30qxtthHO`>rdz4`PTe`alZj9fm3a z9f}v*R4i%wGbtbIU`0KQr35u^1d@GK7+|^o^M#%h9y>Nx;$^=rn4aK(_@D(Jia9?`x$I_O9C|xsL%F$a0OB7?bJ^vefmg2cjbWE>o-0rJ)dVCrVT{ z+KW?t626;F7WC(8?e&ou@2%+N6wb57;rv2=Fchl(qwQ*6`rRdewVYY&)fy7}3>^#a z0=A5JRnz3!f2*3@L6G@HCuOxkgrU=(&XixY!N9hpM^z`9vr$lDJ+eAuhbRQ$3G&TZ`ONCQiVO5w&-J zR;Ecxj%)&c(*q?iAZdZjUmp)lpthw-tDMx&hkP|kEsk=_ROuS)9SxLX=fly#41xa+ zIiX@ae0g(>!xD4~c!4QVWSb7x%gFDuu3AkJ#bt7~DQfjV$;7D)7g1^b4~ncuqKZxb zblw4d{RJ^&+5Pv&Rj7F&uRY*Y85UF&u;Sp#V?Ku|2mmuu57I@7_9o9&f0Hvf=kMG$ z{r&1-*#K{+&NA3DJi!=F_ZPXgCd7i(hW^MQu)CE+LjRnab6kTL-7wTylG0+)coR*$m+Lp-aCCI6OG1o>mXYm7ia4IGO%Qc;W;W@?pwU zeJOPyIH#x-7z_J~2Tu%E@9@b25NQ%+C|*ms)+cQKrAJNL2YbVH;5A-8<2eGC3qZabMureJNeteT(UW8>; z;)xIZtc%*Q>j^Oi3M#c&>v+jjgrq$MGtYRU%6Lu2^$pUrvhF*Tm_J#BJbgPtc{)09 zR~mHqm0M)IJM zyX};ecpJQ!ZOa7F&wlXF{E5ZA{aeV7O7fW&jcv>^?YZ~yI_sr@+TqjkPhW6Z|`cC$1<{pqZz$eZ5a1nN4X93@or z#i_NS;AVnfnJ)~$t(IUDWp!O@gzq-BMDle6p11&YK^vnd1t&rRtDn_kcJBrjBw&73 zxSL3XmwT@7tu-4c68qdb_pViocFP3sZ?JoGc|g$jW1cpzDY!hiL}Jh6ikU%3a_M7o zNGoY-hA0LH$cSLB)IPHs$o{sv@EP=t58L}gn-#?uB7*!8TS&NM#l?pdT)67mtI@qjk5yQQ;v5XUItaz7pv25 zF=Xr{E}3Mi9R~p0mn#ez}^JUx0|wpYU@ldj&kE ziUEMLlv)=6h$$OxtC7iKI89p1TF?v9@4(igAhw~C!sBx_+=m&{Q>+&SZGWvv6~ds| z88^2q*0d(;8&3U3Rg>0MO|MyK*7z7b@RR<|aL0e+TWtli zi!>0!We}m-C>z2+>74fewL>A_gG$4HR<7X3%*_ z_hr2CN?=poi4NScw!k{2t+F%7(~Y0Bl%}^(zAg*ISNG&dm;pI9-=t~CEV9&@FY~-} z0B{H?A1Lg-p>$DIxm@lL&kLYMPhFV6BZ{{4|P=aEW-hU+SLI4xoYb(i0&@Pm`(o&0{ z`X)@tA&!Z&R6zIXF>8z%&=>C0^$?7APe+x`)F}E!HtuM+Qz2E)fuoZ?TmijRZ^lTY z#z%+fa1z(H1m>8H%HNv>&!m8Y)3H@{T-kpoK7$jdB`+j>j~x5+UtHutI$n-&9f?hH z;E0C!%V9tnMkQZYp3Si1D!Vj;QZ>?ZU@0XoNLruuSYnxHMW|d@U{lt7HMf23b}eWc zp&wH@#!{?-Q=7`;yxqFzHx}WST>BZG+1doyfy>VwI%!fd&cQa(EM-AWRdM?tw3fhg>pFTXF`)my5Y#Jwr#;0qn$Shv|$BZ z_0;q`l}l>BZ}_>u5JKPdTk)1Ibjq{Uibgync}e&XQOcx2y?(81P6w|uD?3~<0GjD zSy!NL(y#y(Nb}&lEvDql2%7b_^a#y4G9EfgjPL-D!Zi52J^e=sduc>?>pP*#b0v^n#tJQeX$dMGM?45~V#X~v@zOheu$Yljf-!x4*v zs?k9I66#F{zHEsQB@b;B6ns@&E4Z`P)V!{+u&#p&wT zUcm6&GqrDCF`PHsxruBR_zrQkZH3A{He$T^>`U}-lqaZx*oFxRGPQ}gbUzy*56-i_ zsFVzFL>8>gx}`0OAD=a@)&`Ywy#K4=u%_=9s_n78^ug&8U&Pe$V%0nu7LgoHW(^zrX^lKjWwsM1}KcZ3E#tdodUZcm-xh zJBZDphgqTqJ}Ke3-_)i-`3y=G7(r9D%O?9>5@Xf6+3c?m;ch9%UqWH~K;vBKL%})g zw^N%7c&8u}Sxdnr%Tt*`)qC%FZZTdMz0()2rl?4AggpGXI5=EjT5164d_5Mm3z^oQ9L*I;J*AK$>`23 z`!B1}E8ac282A*4IpLk+W%p3_Pnei0GN$MEU;Yl%>ILW$VuMIbc*)VT@D6EXRt)y0 zDfC|)U3y~U-|@a%GMF}tnykI<_zQnRbY1Bv=&17iIkNtFpRe>yK)D`U2dT&G1bFfz zUB;7RcMTvc94B8HdL^s7Qe$qcG6kl7o}g0+qKG-5=m+y*||YA zk=v*bvgoqb>1JqTBa6Zp(XfrNt`YYH0Zj4vYeRA1*9F09?g^!BDj`(Gjq5n}O(_>+ zB)~rob}Prr%-ndHnVTU}b9&1TCF{B5Dmmc#FZ!dN!;s>C5Z$Fn^L2>%v#~NP49QI? zOwpVDF7SZ4ko>8Lqxxvnw*v_!N>%2Di-A=KukL1t4&Qvyrx!m7fgGQvq>yy7MTO0jh#0G4oI=JoI*FPYv|7h zKOXY4YZo+VnewQRpn$Aeu-r=9eh8MB{om2jB*gcJK>o5`T)9P9$`Fh?Eh1y)P=9OK z-Ev&<{l@l)T@HId0Gh35fg@PGP0PAm7~o1S9}0^KUgD}zdcu>Sg-d|9T}h7^93Kx_ z9o8G)5u+6Y(3Q|V3w+v(|NHT>lb`E%=gyTrg%qT3`n3` zn}j2n?&t%)F#TArL>(a|s(1859sDa_fpnOn&7_}mj{8yiQ8&h?hrI6(BG6*WCa{h;ZCPfZtBP_5@?8HOdLIR$S5!C zT4G)2?u4Ut7BkZZz@13F4;R&Cwq(smYtm5O#K$(AT42?xNO~sp5ajIXou49!83rSM zO%XuR_Q{3geaC%wX*CKrU!Mh&PqP@xT|}wu?TUBf^OpXFED}Q;BPOo&WwV9Q(Psxa zhhj09D@K#or!D3*WNN+FA7hq;4yDybQ74bsG@1^j{@uEX!8CqE^0T{c&~YN1m~DgiYpSPENC$M?QHMep27uWAQc9w&{W8cZ*Cu7LRWy)8%cgEDRRZD`mBLtRpXIOg%p&rwy zgZ~X>|If2mN*>$l?ZmF)T2?Hr5BQ_b7yG2tJOFf&IS)Y;l8W=N*ae7#tJ)4s>eFtm-1 z%c*NMLXt5}QPp`@na8WhwvDYC6yIWLaDZo6ad)X3yzu|(bM(-n_B?UFs}d_1T$w+z z2OQ#7eHOL4k~4l22+?q^3}F)Va+v!DI>~jY{~W)BJ`=pL!EhZx+2q+#Z_I*#C!hLQ zyp`O;u%@$7UhqkQ^sx{UVv0S|B_FX`bFpcC<4TR9(-xGJ?S-cB6HIT%9oajxba`go z9VwZ&?1KJ>UJK!~XOoOwnF{xMW_DY4Qt9?DDqpnlDt)<#X2zaXY{}H7-;zg$Plpkg zHmk+R6s_@!*^m|KB5VbOITr8;AXF5$C{49W_DLT!`&-fxD~D9@2Fgr#QCK&M54NH-@vIq4eWtX-wWs zS6yv;aFpF|7mxGl9^P}JEBg0^mZu*Rv3?Jx(cYO7Pvjr@ou-Y$TRD-N-2Rl5v9;dE16p|8Sw^i>RG(E#P_AlmD%EW<_Fr;U$-TnZr0!1 zB7aPG!x+kDVwo*zWM|}>hxm9*e`{UQW?g+}gR-POdb9Qw1FLnh;1I*7pez;gLwHyz z=S7s%Gr^WY)7JIqn_pedFV6Q+B{6T|ytd}>-iG}T3N0lzAX3iHNl7bQ4v_Mt3>QhzzHK0g}1t}`s?)Fl*Kz2gYgh47sykOJWvixLP(Ba%u-d}oH3Ra56hKISye$3 zXRkpnKp2c(zSDzV{a;d`Rs#xA&JBFd5)Rg|hC6Id64z79xe_&?4 zJC~wKYhs%fA@>4R!zz@Mz1%|5C(dse1cY5-O4|DrNx@RWP_9$O%G?u-UuiUH7L>{l z`w5+4{3U_BT>y1j_D9C}+xMmRTJ~+wl&no}S}P6<9I|^>_5Y8kckqwvi`u?p+nCs% zu(6$qoiw&>+vdc!-Keqcq_G+{PNTMOe)sd<&olqPna|m4?X&jU>-t_wCLH-=anON> zI6EbVdWpjVc}G`Wd?>}|zSyW`XxX~Q9X4&dlk>WBiCoWfseUPGXpCyJwdOcP*!x1wsQa)bLM~$+ z*nj%A*62?S$)ZIfCe{5|eKFo}ej9iQaokW=8vp4O$Mzwba9o`;$1Oz&_ep%39j1C_ z%)*5<=D)0+a~bF%?2L|R|D6ToMD}Zq_8D50*ATm5m}D8`DY!vYz#ia2Ognccy|&Qx ztalz*4ymHDrRYEu=QA{M3zZB!h2ehwF`<#{AS-|52M5z@HBD-Dc{M z-s<(Pbv*8x3htC&nE@NmyP_vBJ)1PG^`AOkO&6zV&Q$8RTg6lJmnE3POt?r7>XL6H zz{LF0vBt64f+TF=yMOKt5L5Uwv~1jS-o+M#!`fDj6WZfRy2bv)?J zaj8xJPn8!CY)o0e?D9ZQ#iwAW4IUy!VMK(UbWS~kZ)V_fS24>F7Mprml!UxR2Z3oS z*${^1c5xEpCdhqJgit_%O(zoDWjg#j8=O#|ZKm)RbV-##vDj*3=37~P|C$^MM)O;7 zm>-Ibd)$ydZoAJ`dkMOcU%yDIn8IW;-0B%Kqa(XTgPgL&%+{Kk!NoJG3s-pqf+y+a zA-;&`i&tcZq)tR0G7Nhew^GXld`cy_gJqfjUJL();Po;W*CQUYhe zQ)9Xl&&BW~wtdY@099bP>E(XlVVXO(yoPi zWoKQ#cFYEP5#}=QFIHb;7}|kTYa1?V%r)9$Kb1@YU-uZ8P>(aqhnb^^ABOJuQFX5ju2dged92O9xMcv9gW*xUZp)6tvbnOVY@-6vo}O z5yM`k_o}c-Qy02C=g-0f{K+Osq(>s+<3Awn`6Jcj3yH*~kmblR$;2uy=;cDb-1~G9 zFc-)6kiRy#yPSetcqGd-`71M0u%}Q(Or#?NlKo)yE<9rd?lEkrS!JFT%;YUG^23i7 zp^uNePg|mcfCU%CdQE?CUzz+LNn6s*)gI^xLazaOK(SgRsW-P=vSWgxR_y;}0T2pl z>K;QYoW)lHUli3K2@D%lb*a%ZV6hWFK}1Y^$r+ImAv8$K3{lJ{MMi#+BW=0h-}y$* zeO7Bz+4&hJV;{dtN9wH^>{zmWzRT`wBNJL8DN)N$>pkgN>C@-t^c_KTWenoG%O zqaCI^(I-%IWdR>D+K~v*in?O(<<%7QOzKAmP2?%~5VY077VN>@+0a*0!6^pS12Z0$ z*=x}&#!OA&pzqd*c$>`^$c{x6Dftr}L^wibzSO$H(Zq=1TE{h$Ajpi;x}d-W|1=8W@IRMZOm>UoOsH8)Z+d5 zjQX8WK&_hsjAUXjapR4Q{XOo?e0{i`Z6V@oF`stlbW{!lwzLNj_>USgo@Ty@d5cmG zDw~Xq=8X`_h0Tn>hXIQ2GA?Tp)+vl ztbc&pNPyNO{l|LQjA@^6IQv~p2!86nbTEv8rVii0oE9jgwT$Rk!QLHgQ-WJi_!xoq zr8eQVAEcuW-Q4qM)EY3qVCadCP?<28N~N5^wRE^QtO7{HS+uozv{r-3rf_*I2}UPf zIeC|>!CNT9N%r3;(;_6IAs|#^#UZ1awji(@VX^j*phVRQysJz~v6J^kP!eoA#EVb_ zLZsPG^;eMaixQh2+)VVt7?@JS^5r06R|OpyX7AE}L`)tx{(EbbYDSQ8F4so%6r-b@ z#M%R(0Ucr_F}IO|%VQhiQBfeMn^lM%VC3$w*v^1=TE)zVG3Lg%4;5g`qpfUz?6$#u zgUHsD8QL>$teu&9YD%3EOp8wUGA99Yg2SJTQqjkhkQz#6iyBPzp_=soI>tkjeQ_kX3Ejmu z8tVj5SnU0Q1BdaU=Dy6sRMVd#F_-RqM>T*Z|8yL-k9@VJ3d6`i<23;#XlmM`4LXvp zZ-I7?|86lb>TE4u;Fjz^5wSX2aw&CRFb~r=4atI03cG$D)f_7u^tFhx>hMYw3i1XL(_{Je;|PW2kAx&En&P?Hz$OC8z?ibQDV72CtQA1z*NP^XGw+0n@B@1 zn1m5Bc{=_vbX$&iYmJ!zgxxVL;`~rvWjXPzHVuXKhD_c(q_cqorr^8)=)n41-}=6z zz(Go6mtu3HZqfHp)M-blrlPUTI8k&C_z_=IF(L|jTo$5zrI}FXKgTCK}-freUnd|>Ow13Wh?GLmA zUK)-bwy@y0|DQnlbFG~BS$LhTS)2x+O6UH|#Q)nOxC3;Rs-Kc7f2bb*H&>*b4jl?} z{i7{r%<*8nWk5>@)`7znY4oJ!GbjGl;N^P3P#8w~M^y4uMzc7N3=1Os!oBDlW(0(o z<+ed@pPuWoiT{i<^Nfs@u(=L%i`|G*@h0c9XBj8>pZ@^8AFS@;z`$7CQNzzfLw0F> znt@oEy75Eq6n2+X$x3ym1ub~8zL9gQHan#xIp_^%e>d8PwCFZHOAP1 zEWuaT)G=gJOh8F<(Th#tqB8z^K~MN(S7a;Dc|YtRxU?G(b|8o-Nv@fB|8ixEiP@#ildm<0zZ2$oj>wDU+_~UX!ZO z(ocM_61TX8muxf2{$Z!Hl7dh70+wB1b>T%LPWhD=-|=~Cvc`zIHKQM)qesfsn#Jn} zH@pcvys!-#Z_%1hUBlcZbpcqYX?Sv7={Wr;-e)SDsul2f-r7Cq@USum8c%8|w0vBWQlG=7hf!|By2cFehgU3 zyDP`Z#kdE^>!a9YsVTW@_QFxa?|f1v{#Jg7p3?uwT*52l`}%2*0;#4;7ks^^@-QLR zS2dcaSLr;X+jj**!*)BYkuWfksQICBk)qDOaO3$Lq2DV75>_d~y{*!~W$032eeg|$ zRr8oLPxG{FK1{>XViy+JBZC-lt5j1w6@ZTX>CgUB&6XVC17zOT@;BA`dQIp@%6FF3 z{E6+HK7Hn}gb3`P2`(r2^&%WL_16SWa1VGfkewi(@>L2LxQcd69)d8>1$F~rnd3D>ga!wopcoJ zvuu^XKwkoRhLKr6S4IatGm7zQDZhetxEAvgN<5)tR0=B%zBM2<9aGkI>rTp4F`d&? zCUi|UP{M&ddKboo__=@b@6X=9zN8uKgt@O?V|>MjGb)9-CQi2}R4sdmHHkmG7)1^) zB*qi5pW3BbczDnC6K#GkQtm-7N&kW#5Y{bZ0}5#EvDh2?(+t291<-87+?Z!@Zn2wV zMN&8pqdxKT?AqL2xOTmxjJUjXbMi#H$&2*mzA(j`OQ71yHW$`l}! zsU3cJqu~$X%7ywJ6QhfK42xWurFHAX1^)UOsMM7@coZB&#wGlPfVNW;pE6FD$+^hv z=082R@cR{KKOa{9+<`tMIvS@QP9%Zsiv&yL9$nzZ^~m8%e94}9qx2Ktr(E_y@nyi~ zcoa$hpKB*S$-)AXpUBXUxVBaPM*pM?nMNtMk(q$-=yavYZ77IjGApBzA(&E&+LHM1 z*hHE zyb$WNfzm+8hfxDxHC08&WUh4RB=4Ro@(9&rEvs+wN+0P-bXxyzAFg5~Up^k@19o*`Ca;kD_A_OlNyPZ^_6z@P7ScRZ<4v6$dwnhwi9uSWL~S)% zEL-}-A^>DLjByz2&DW1Z!1H&^=>x8vA;XXWCG7{~9CvoPm7h&vw=`{7xMR7`*MYD0 z!rKysbM;9Qs?O#AVSO}!nIpSZqO9SBaKhBbDT|}CpFFmCy>mwh4W{Q}T^rqIPFpf8 z;EQqPh8yDTe{h*!vE>?geE3^E7$`5efqQ1Jy~VEXMZ*7m2lfZdcn8=d9?muo;&N%T z&#-5x<6%NwW2$n&cd2E0dNL~ndR?dZee@0Z{IUiSCNJQM|eoEZHr-zOMSlWW&M zN%LVAf@zxniACBVT)}(S!Ggv9(3BYbJ3^2a8xGw!qrGuo1(r~jp3p@P*1N zbglBQF}dl3?B~^jNBDIJ8g<%Rs7MHWeeHWL+ zq)NWQr_5eT)^jzWzbU=stN0{-X`8X?(K9Gn@{DDmpbrU%O~o#6K)IO!L5}zbpqb|- z(_tOZG8jG24OpT z%)rlKupuB)NDD6x67YN`?uQ>r2id9cIZ=J>?PvTyuk9RASp5t!z$DJ4ubE=(bro+K?Mp3fc4W9Bh8 zR7jsp{)i~l0b{dUa=gq~R}=K_chKJAIo@HOus^>f37U?%+hDSe-XIC*9gKdLSWydb zQtV&S-?JnR;^S(jVuglf%ay}StWty|+z)f&Hpf4LwxJcHJz@-PFymye*T_mpo@}*2 z%I)m!;Xja1e9|v70MMZP5@S^@N=bbA6 z{l7S5=UF7ZPr$}C_>4lVYK@ZHpBq2a1WX9KBQb=pNTLqxonqCEs<;)(O-ALUStKYW z;S9e&pe|1EDKCF7H;P-LUq{ZfcXLdC**Mh8c7RQ1S9|U6K%XU^z4E)!{qu`*FEv!} zqP{{bjz}R-Bm1&?bA;I}0D9wnJ0qdktcmDLi2HP;T+|8UvlcNwb7yeEH}aLuOhuGl zJkjkwcg+cpRvMy+z5p8*i~FhWCSZ*;PiKi7By|=ab#H3#gsqrQ92;VCUBs!F8Mb4WUHZ57%WpDzM8;z#U%BhOqwY4>;0m;7+eTK9 zKFb4ZdzZ1Ls#P$dVhT$AU=g0tWBYy6+;k3sf}-@jOPJB7!VB!`zqn>fT=H}y+uQ}w zw06E`K0dQyN)R>7$9?;6r@yC83l(_zC3bj*7UJ;18YAzCz1;4YJO?S>{Pz$}7KVr1 z#PznKhX)ZEerZI9bQ(E~VJk~!9V_-)raE;`3=$@y!bo-N$@+J!{8ti)gdfjNJ^}+5 z>B1OlP4L~Vjwd?-V+~AbNpXVM12`+Bo3P(; zjuc9!?AI#pXUfuS=%K-IX%OCc>4ksK3!jyIsN!uo;{k_AsL_yPg9j%|7_{I`{MaA- zj)~&E@~RO^tUONOKwWw^=*D9LsF#(%i^{;7A6>LGf?9_mARMEAZ|hP6*}tlIEv1em zw?2+`#<555@_H_|jkC8|BeDTAyCa*1LQtH$IXNmPsO@3rfK#+qviD{(Z1N#^y0u)N zv9BRNP9?@zwhf7JGw^8yOr%Rsn+9yUD7XaXa4s0ocv}$QO8YX9Mi`iGtxVEmGbsk) zso`CBMGwPrOr@XDd`V{V-X6dDd4*2T6VrmSyzZ$~dy_*8MvxZo&&ZN!W>kh5Zs??m zd!#8a9jB1rnK4H^34O}iza_CR3!Gx=j-F4PPRb}%s#Z4%yG~OLWt$#o-Z>)2Aq&yM zKJ!y(NSQa)1}>0&NsKIh0Og5XST%gSx21Ue8^?X#-J z+>+F?wER9zap^L=C;S!dL0Y-LCJAoSiQST8d%F)06j)A+)`%VRiB4uD8=GNx)D$|D zj%$8fmMCt07^27}e@Qk=A^e`yYg6I+x|Iu3kBnW3_B#`ttnIsrZHjRZVO5yRs2^a( z^k;A_mWWY)1Cv?8ZNOLsYHXLO6A9-_;<+{CA zmRptTNt$9ke=6aRP9Msbf4DHfRw}VYetvTg&dpbZsh4(LHF3{3U&asx`4gT#YO)e$T$-8PmTcu<4GA9Vw}# z6f!?afBcW8(%8CBo&n8pP+e~5XyBF#$z8=Kcjije%(uwG zt;=xFkVF8sVX=4T2)e@$YKCn^svO~}jM;#r|KfU^%$O{b&M2#Kyk;HOH!`)#GMp-- z3G1C=R?&SM{a2|@E#?gh+EXt}k9z7t$&-TxZ`Cw|d56p+N(rpr{0m|ub9?Cy97-+T zp($>hJv*TD>$pREdZa4mzeg>CVj5CiT3%7+D6RmF&;53~h|nN-3aUUOKJAVnA|`Lr zD-}JCk4H`@S}A%RmV&&aH-EGRy}*YrL7c?8Y-Xj|_gmSTtYJG3>Tud_HZUld*0^5t zE2M3TXib+GLYzOPmdM_cyQp$GiGwrfF|8|jyjrcg4F6GuRfojuVO|{LCnpU`RK#G}Wi6QG@jK@>Zt+uQ2RePP56>Wi$ z2qCsxAiiN~`;2sp8pmCDO~@$Ri2Y$#l> zdq_g)2onZv?GM{f03M@88dgRP}F0{Ta}&NkXC{ z0?kdy-D@PTv@)C|X54jvcm%2&r7}(SF&eds{X8G0>mR6eC4693EX)cx;k*BLQkcH& zkp`RZNr`me0r1!VOyW=g>kl8ZcXf+6J8xt(S)$2O#QdRZb~ROe;a99rbM$29FS_59 zs59@cFkDR`Ca7q+e@PoU)B2ufBC^57QjP`>N)85GraXb|@J=EmS^il#(Ns|_qLeYf zJS=9|Naq9YC3YM!d-%(RLfwi(NW^tAx%rzJRTB3q%X7$2`yk55i#I?aP9I-;#r392 zH~nKyQW%m7_5Z4IXV83(Djizkq6xR0im-&BzkEYS+`=ks*nl$h+^Ml)4=cw5=e0{i z!LyJO>FHK@`=U1Dsj+Cr@Iy*(Nnvxy6IZr~7WDMBNH91&%W)iVu+eng1BR&nO#*-f z4b0PwZZyX`p}M$0xIGphc$1&J#_U&Kzs}w6MII?!WIFJ620l6S9D+J?snUq8-~0XQ zf??A`@c&R-_CH4&V?0rMLn#ef=swXF0!wFaNq*s`Xp*Yhy+sRr<&S%^{DsI{e|*23 zi71L)BK)(m?_m*SRm~FhTKt+`xgjEGaGN|^#+wlCo^A=@#1P~C30T{Rg>{yEM-(NR zP~|>Q7B<~jU`-;m|BJ5}^xH^bIPu>gVDW0m)Z5d$|D5dZsw{m$M6fJzp`2bH0sDg; zlQMOU(p7euxo&je`P{+~*PeDmqgwG}T$K{@RJ0QD*7$7@4HZtl(xi?I{3m;p_lb1G zAL4)5e$M;Yhr#E6cLOEuM{0npbPJXw051tBEnCB*GYV+hp(^f#Pip5vWwR!ALxiKr zuqtUx?HAr>h&VhPc4*^B-H}{TBG@ru*r_No+*n)+aIxf!(_Qqm0#i8Lmo(|rVuq4; zYSJ2%K58I!ZK$};cIxl+%0oHI7wFTeEsfx`q_Z!J+O8!UA-VLmHdwPWh7iJyr_6}_ zFi6DW>6J9OCCS4yBvxF=u}mYZCXs=?Hb7uKZZNx`LGbV=?Ep89YQ-0;Oq)Toln4w>`n*Xc9cH^n}Fs0~<3S<6{A6!p!E{6Px z&~XC@;!aumB|E?9MDFH5DxPx5n)R6RTxfCbV<2DB&N#sGW>_y0RsdtBRL;i(Crx>` zkfGg!zGwr4(^oeiSU zvpxA|9sJrEC~2C?PKvn6X3~&}W5ZN}KWw2zaYAtX1i71ec4mztPR^U+oUhaVX9qq4 zk52L}!{sK;!)s@b*}rkQ#%7l)Hd|XogGRyRxr8Wq;<*Br-czwAZMZJ#Q6n(ULBkl> z8N*e|AI|4U=vx6?oQ4(LMo+Em5VH;yg5IlG%Shcs8{~XDBt1`zcN|Brq~x3o6w%0T zE7wM7IOC<|DPC@B=t$3X_4=-ZtxfstrrqzV@4Uq=HuYc_P5}&XzO*Wu{jPMFjfG23 z7tOBe{7e7MyD)q?FcaZhDC5(wb~9Iv?lz4$94j@o;~}awwkcHDMRy1k;Nl#bScyXC zYHPRoGiKAtddK{S9FL~l1_sOVdf@eXs?%0`;>vf1p|jK~synVpRRcTsEipy>I~{{l z+*|-PzxG)(r%hSJ1cIe~nNppMxPSboDa)HN(}3Vcahko$o(*}9{&Y%LBsA2k-1fBX zTbQ>J<$-ORu{Z50d~Kj`aEu0ofRv9I4Q0ZbM`|E=CaTOvDWbUXp*2o2a?k{ZxywC= zSr0Eyx#x)o%;SYLZ_-LIjW*>5G5Li7QL3at_fAd4^Ft}Zd&Ts{)gz#WuPw8WAHr09 zUaXkTO`>8S@uq-w0EAmg2Bd4-2bqhr z56yJQQmP2iIJR0L>3?=Yr4Wv!R}OI#Pjsvu6OohD*2I^YDzABsLBnZ%K6JVcKRj`m z^>A2WB^n>48kj(9o`ax6$*;sYY_l11CbI&E`t+T26ib7Lsh%l!jgWy` z(NqU5sC)f|tHHdrA2DTMIf7eIJsR1R4%SsUl87=UbIONqY{FU{C6>aeTq<*6+fW9( z#41Z|NR?WI8qtZ?kG`!n;t&(nCgUQ}&S)60!>gE`i4o|peSU!R18=ad%9gv8SSskI z6p_MaEz})yt^Ha-x3hkNcRUuPl|!B{A#?1m{T+{RSN_}rOf(vdx^%&wP1od=2f51% zbBU)spOVrgA%k_R^+QnBr#-JI_ME8vb}d-$JABwddZ1iFgQE*U?J8>r2<>^!jn&er z1E*~W>9Wjlf>Sd`8#!-;BUz*JI@zy=9|dvPib0S|sg5&^k3kHW^(D9SfUe3IDTj}A zuG!a=wbtS=7tzRLq2;l5Yr)nyENf+u(2xnxN}XG_%M~E=v)!7G73gvGsY*p=PI7rV@oR6ReXsRUA|g+`=1Gr670Uas)nP1=Y}{5j@=d`+Z4NWqj>L!S6_*%QMK z=X8_Hec~A8CfiEaJ40tzkfL}RDlN$fPDTLVMCP?YMN$W6Q26k(Q#;hCHBvU5%=5J< zTJ4G&hrr`eN1)$T2UIVX@_oDMWvr&bD&VQn^wyJA9^RE{r+oR1hPsI%u1^%-taLd0 zfrBO8@+4F}mNd{UXNsx)D8+!q5-?5JZK_*w%i1DamUAnaFZjjbqk7No%Gl5_?#j z#`$IgMcb}%1(lvhVlqDz`OGj*tiERq7D+Ral}=yCey`eEF8^{;MA*cFhG)E}=gFGC ztamEP@m19k!KUX|$6F1KdR$>&Gupd!FfOg8eX(TuS~4Z!r^(t+bJ1T%CRR_O;O3$_ zp0C__euD%HB)6@5ao4T1Gx1gl4(KpwM@xXtoAbVdlm50$VxC9Qfd5*U@S~8Jo79l0 z)h?$2CHXj3V6&`L8z1}w7R3Tp0ZwO$pkti6NY{OACu*C&}Q1QJbFM0R0gLezBEC{p~*e4 zr5!M6s6N`TFow!kL`I*dEUY6gjyl&+pKK`q{a~+!k8F`jctD^=y)t(S9i#`Q7hAIk zWX;*IOpBJljEK*Z34J!Q&B!kscT+0-REeHlMsY{6KVio_sWi`bO|8CzbqQ&!a1N1V z<&p{P^eR?_#wZ^A33}G{rerjR*Iu8wN#O7wef+;0mV;m${zMT<@Xjb@t_IK}6%qm= zCQpd|`UBT=RUHm4&V~<5tmG<{{OJh2+}2yNt(7%dZmkQZqf#K%_>6m3B2Yh&3@ka? z+5g1y>^AmrlFk6a)-*QW$;us^KTKcBTRIK^^>{ zi;n0A^hlFLmTzk<8wLP`;ZaVZ2(jziPRm-|As=e;w&j!mjP#NgXU@762#QPaZ~+;q zH3l;JJzBnCrjcY7f4#e$)Q3wIpvmRXYOh;43?MCg8VPI|P2dT2{(fR9!~3F_4fwuCW% z1z^AvD@N^oJ~lHrYaVyVeZK5_R28%RqT_glLIczQ=D9V5E z5^3nZx^jo3ApYg%VGIjtyfpx`1x6;vhKdc%rvb0coz$dlYK>8vP`>30j;g=oXZX(t z!DDse16i}Be#6e9gz?P~FGVuhjSDxdsM)IT2$Onhv1OP3fKBS&M`T>Tm8_^-IxFTf z!$&6yQ22ZK$8fgB?LRM&|045}Pt;Ry_fAp=TcA@9M=&cH%uYNdD@?)s2xsRzM}h=D zlP8}Dd29hMHwMH74z*??!$X>#ILMId(#S{N4df}Q>2b%xL4fq)qIhG>mzbl&bq?uG zcz~lnF~j(D9T^Lg<(8A{na6EXAb2Sg2_*26hg{7qa!RXSMi|6o~!E1yM}g4Fwk*H~x^j zsGz9LVBYZ(WOGp|FehUd_%=i{m_h0oiFnwmFo2dTd}A$uAIePXU(?d!gSbh9QvaAC zDozd+kc=G;MLT*>LT1}GLbgkZa96&u1|-Nnfe^XZ{UCDr5}c;%}+ zh$16Ac@*55vI2{3SJ7G`Zcj3Fu8xRRs4Dy!i}pWDAJz+RFH9BZ%>7Lx#)KMBg$eAc zS|7o0Qvc@GcDVjnY_#A0uNhwhFZo=0VdyV$(10l-q#%@iBsNW?3o6w59?)0@h@Np0}R@<0(b|{+je`b zE+Xk~`R9V=h_f9tE%jAk0!_LItxJ3h2@`#i!|LGfB6GF@jKhzsGdJ@l80##|l4cXy zHIurF?Z|l(auhd|@KZJJX}pX>KPN_UszG+eB3(G;No4VLv@M7{fNW?;)w-Z$h;GB} z@N&o(2D$l;a<}lSBNLN$J-iu{CjiTm^Uxk6Z)Em}iX9&XbN3Eb16C(AO7Znj~i;dT1&*;e#AMftLOl3FgN~>)7pz z#h8r07=L=uiyZ11M$OBx{I|^rhC-Vv)F0O|zY3G8Ua^RtGu8e2G(yAc{pWq*K7{&@ zgzO}0i8=}m;RG{lHi)j^&#DIA$LClE*%Ue7nc)Aq75s0@FXV?8*3d`xVkqpae=hWe z58S`*T&D`T^Ai|>@Z}1q6_`OjjRrTVsvPemrZH)sfvfudEH1l_h;Jd>wzYVvT~JvM z+M;emw=YrUi*s;j&Rz88^ zgXmS!*rX9fh146bN>Dd8D0H7{9Ltw01m{XL<{#wCPf%F-vli=N@`Za0hwFJM#-+^Y z6Q5T%wFh1y9pgL)P&MekQXp-bkHg!t%JV$uWQ=cSrb~pHNC4iLXh7`jMtff0Pa9ZC zS2n%4uv(HQdb~-7Y5W}O+gVRf2v0-4;$rrH__2cjQjTh~%%TgD9sqDqORPQiDPCyd zLTNwAMH2mcbg~wF`Ot!w_^luij^_V{=JN3Pci1c>4n}0V_bFm@q@Jn@luO*Hw!Kt-A8?G4n1E_38GdIBRK!7)L(zy}IZ;?KoDxiN06{u=H2rssLEYU! zGikH{NXtjTLcj0d^{X{*`hI=;c@;1x{QH@(Zn;O>%j5I+=d#^ah!)N1w+?{uBEyO@ zqt!|En{U+D0@Cwn*;M`P_j(-`>w^Sdr+%NHnGW;*TN}yZ6Q~RG7KJ{O@}*0MHiY^y zv1X@BfLPuh^5d($6Mrg)ZFcOg%z{q&SV#HqX`#EUOf;(PRc#K4jK^3@kU|jSiC
    xF@r^jtLO~azY?T4w8LVf$v+fZU3qiB*U>bd9HCoW2k&zZ-1kx z>BTRkFR)KjyWJXSr`!w@RWOjV2h|n}4RG956kN51NLVO32-k~zmv+s}{BIhZFEE`e z*+eiaXEe{Jud^%YvDIacuPqlDb}ds%g9agwxY|MgcD+#taJA(c)W4C&rh!JBR zI=ZU;p&UrW=8`A4CP)y_N0XR+bJI0S40=9ToOxeuTd(SUR|ZVO4_A5JxVD_|Ju2}8 z_Gfs}t?%f2+CJjmJbjL~(3K}&g~DX@a_`z!jG9Q9&0s-H=)xtmyje|zz9Q02VdO^wUbFIAeMxJ} zw&JeHv7z<+RCT!*Q;^NJ?&5`&0oQ;*O9_Q5QEAz>*2~DJom9_;+YD4|Q-4Lpth{Z; zXRw5r98>wzCjOm6J>giyf;_nj4Yqn2jU5T$aEw||ZKqtz+A*8f*;KktYlC%Aam^Q( z?p?~Tn2`+gjJlyC6(!8zfgR2gtBWV*)(SKDBWYqE}jAYAV0sMXQzfxTSk{Ndm|UpPIcSVS`Qf(Xi}cQfrj5xj@VX*iOD0~ z`g9*dqfhR9@s{A^4}G&a1j2(-5z7ifTVCcLqbj2-%iz zV%L8^M57r)vC)2p7pTP$lB;X(_j$Z(Jguc_lW?HKEHvX35z2`i62(MjtS1V6K|~O< zRXKTUy)>~7Sf$cPp692;Y1TT+2S5QFicB!CjS5V}lm_KrNn3aV^}b5P)9^4r7dvO| z(!I`dP_T_jXu4O0jP+t*Oc#3+xfck+Q?S5)(>UZ$?HEQ>T+6h}UBQ82Qe!I6_h_FI zWU3p@GV7IRJbSW?jEi=9S$1CI;##SluL}O!7mVz5JYQfk2TzEG>Zn5d$j~vQVy-Da z^-%@~pkr9x<|+w&oLOPCli(NelzwX2gc?!D0eL)?b-06(I+90ke`TtC^JrnhNzG5* zE!Sv!J*N9aq|oeUq{>2bFBxze$&yVURf^DkeJ%^NcqWHpk9soN^>I1RfR?Ml;c{rg zK@G_mMzW12JiIL@5XO?nU;`H2p}d($@#sId zIC|BJcBm^Q>cA8bg>)m94T&Bc2~Tf1;EgMZ2v-o^ zbpo8TE#e^}nERkYf9eOUcD0ra2=C{dE15DLCynO$n#SddpDnOIVRp6Y$_cHNI2*Uz zCO=Z9a2nAPJxC?cSQ5h)T?VUE9m4wn4O|*|n=+-Y-P#Noz9|)t_1QyO=6#v&mHz@~Lzn-^P%KYdCF?9;pTrY^YM%#6_hoq@mir?;j7f{w`_0upEuDb#$BP6T?jH0xf?4lV0Rz-(boTU z>(OZwp8NV4%b_2~zih+yyPH(`Z|_o$)5!KZ`tSQnYGesY%HQPz!-zF}-0`LrZf0Z> z#(S?6S9mOxB38)+eXztSwkM^|bw~v8eP69DD&?GXcW`}kAqThZM*Xr>D^>{THRaM% z4E*VeVOnJWWUIFKdLH1Jv2=;ykgPMR1gaiST^hB=oYS!mwf;;gq7AHhE;=w1k>Z_X zfaQjXf_ezWADORZhX(ucW z+_9!4C;9i>%b;~!8%h>zZTb5|A&Q>SCC6%E2AZ3`)Yo+n;+Z~%-n%?YQ#}hBXIk;)`IyO>=%b`w@q2QRJ|{Jac3Zl zFBqt|l)T|$7|GLnep>gawGr$^FPoSH|YL=8TqwP0S9!9Y>eDih$;=Ir3e zT-fkz;`;Wq>vzL$QcUgCM_<99BW%x{Tt^FZkxC#?h0~u4-WSv!iG|THU?G&F!HedA z!{ULM1+NJ^yq@I;~A(j9xrzL-Ta=48E12xt#_R7eQ*+`aX`Y9F^^9#W*qd^?<3y>gu55C zf@7F$m#hDZd+w6MM-~oiGdwDxi8ZR%Zt0j-e2c+x_A7iEV5Wk1#$ii8XlHHKZ9`s~rDJvw^VaM!1SgXUtjVE4(B4h0*nu-x`kK3;KR!QRkBi{XU z>SIW=qdCTTR(I-R{k4L26%gHbmBvy(^W9xs< zzRD)bju&ifW#~FlhKywBT3Z-gHh`m4oj^VSz3T=oO#IYaY3e4&qiuvkVyf1Dv^`T5 z1-vPjg`_Cj;cY;Q1*o(Oz(5h6^p>}{yIletwB;Mrt6bRhjYW-jy4J%au6p7#pkO7S z*N&c~i>l-&z@XSgE{rJWeQhtR&@F;ENuS44r zJ!={@R>iX4*XQ~b6IOcib`8SBGdlaeIEKNDFr}`w-60kY-g(Sx={2_~;i^(|n9;xN za5oJGk}gs}N}+%Bv?4KOEn+%p7FSQBRheT2CfX6L|(pZ9sN;jtYTNz}T7t z`caiD%+~Bym6}$EUCH?TSFQknVR-Nk{m;W3Y72QrMSr7DERD?i&xMRrtGM^-#q3{n z`*p%T!mjs@zs2vKz0M++L=|+_-ZLHa`Bu5Q)UgTj6T%c;d9<#kg4%=It#U8ypsU`v zmviMHet~66Gjz{WY%Hr_PIAHCG2(r~Bl5(kP&4gH|5L0%?`=Dt2r=f>s*O-9gwrdT|fOMo~BxG>@ zLwX5MrhwcJF*%Y5tZ|&TbT*wxEc74jFs>O1xG!f61VuA7p4>(_7)lr{qwOtF*tDm+K~=)Tf?wY#4sYKerj=-6eExRpeMlP^3i=* zL<`p(>Jmfbhm9dJK!~%93FS4wPtu4NzpG!^&N+_lQCE z_5hijd{Kaa3?kAfo{wv1)PixlbfhG26x1cD1&*QmljMsQqwtw6YUjX2?RvElVk~hC zGy?{e&>nv=aJKx>>|-cXFzK1AA~Nc9zNV@Y80v(y_SJrtM{3{+7KuO$*Sxg)uF0|* zCQA48JOzMKLNCvFdz|IwUZXXxEzMaZFy~$%uADg68K4-RVdQ0qpTdNQ?=0QW7VvK{b8SW#8f+}w4%}cLMF1++#{j7wwjf}b76w5I=Rk#U&JJ;#so1kvn zIv`OMVOi~1b>sOuXnBBMs&=JGMJP%^4>uf#e{=;XW52k4kyalP&FN(q@TrBP{Z;cd zq5Vp5cpLKo;nqb;)GlW#BmP3{SDBXj87`Xc-9>@#b5s)svG}$Eb23kTF|xQVlICaW zD!b$|J)hBGv&^8&d%X74(3X>(j8T-PC_~)NDy{KsjyRn$XQsosE*MweEP;BlW}XtU zsA5F-%#6c+fk(23N!q89`eVOJqxETe1`1k9G#hPVj^p$IW@XUJceCBzarTiWV##nb zegqMroJf@h*$`M1RJuVck6$ZyzBGb}*osSUTma1^#-^IbHO)PS$C<-9AT}Oo``Au~ z8Otbz8kA4IO)T~`6SXz?T$Ff|xmT=DQVupDIX%BDPJ+D~ETOY^$V2ZpG8<0hEC|JnOk9)|OF#rsywq6GLF)JsmAD3j|l30}0`k z3&I!1SzIZEm(vbL6RpUR3OsVo)L3fYtvSi9wU`q3$mL)(jbN|&AU@6Hj+m=mux2ee zFf>QqJ*RgGIWI68^#1OfEY&979S^*A={!#^b}>SE+Evv@4ISSdnAqO+2p2r53cE=wJmlK7Jc z{t2hpQKD{H7EgSWEAhs-$`t((5pR24QHF%tXSps;{uLN6MJY*5>8Oo10c6OWsBazt z9~JpSo=2||S$Gc0OtZ78vqHfsQ8=TCqpnYuV~NH3=F>VRWQG7g%bY;GS}#i_qjfz; z=Z_hfgKejBmsxrdvV*thpr{FG)FHbo$=vJ3)KPY+y%P~uOxAhbrS|iE-r#Xd0lxJk zeNiwlL@nrHl`M*`~Ij8yy ztXkFU>DAr$bKP*lL(IEtU2QII8kwc6?cvqPL07J-)e;cr1XvqvMVU}8KUW`GA9u=J z5FHVF>#^=8BFk`+K9BG_4YFyDnQ!RehLgQqJ>1d{s9fKP1Nn}-_&$C>Jkm8(K@evM zG`exLDP{fu{>l97Tz8zQV{_O}-}L{+T@d2-Lf-su8T%|A*#CdvAa0kg9&Ga7Zuup7 z4n9vZ2!cfF_M+i%iRQQcGS=OKSFeVo z==P{~4alMX!aAjO%QhqBLH#eJLJeB=sWP$*k17=*Y2vaZ>8>aY5lO^e!10dMW&Qs> zde2jefN0&qunb6;Zf&>Au2yAS@lsZGa4-?jbfvWZY$Gi(Y!BTFu8Hf;tKkS0MXXE; zXz*PPRSrnd?d=o=>OYZ-F0KDn{r5{04c?a;P)=>NpK;4L_>)(6v!bt$iP!%JurXlO zp>Gx?^J{i?Z`?`SyD-snZoK*OuseKy%#CxaCrFg2h=vhePd6z{MQn)iUlHMGz`F67 zh|deblRE}PkNMw93%Qeeg`$GVsrnb!376Q($!AlYB_^&2Jpd4X};^NO&NcS^z^!bb)O`#lct>l4=D0K$(^Fs zf6qatG>H_Q@NW*p^`}PW_j;+)x1?(_g7;)|PZRROGtqjjB;*1q29b+U*ekFuAk;QG zyhfU|3Mm1L*Q271CvuZ#)Zh69-|&bh(z4cJ-Q@?4GavVfoF58&secg~+wF0C>8S+r zAqVyP_%*CUUoy91({=}WiLoq)ja_DvRnflJ8FUzSlGjNY+v}lIV~$L5RiuMaO_;tH zsOoCw1ZvBeNE0`n+f3$k^9`6scLaFt68f7y_g#BRSn$w1$4W7Y7<7oHd95&BwK>iN zu2nm%M(-+Y<(W!bWqW5?K=Sp2GL7S`z^VxgX)BO~G}z|EnVkPr80 z;S5q3n|bm532Hlz^nDwe283?!mrDm^8*s;Di>mUUG>o<1#wk1wua#B>?H$Q0{v{xi zH!sJATuy0JRIs0n2GG68-70&X7Y+}m0r}E!?5SiMTT#JL%))NQu=;tu3y6KXYcU zT0dr3VXNeYg%mbvq&_Bz!Exvd}r#a3L{8Bb*sriYoyEh4_IaCkY5`~zIwhx8AF^1f6F`j#`Q;KJ^ z8Ya_tO}}!E{bdGrL*GyuB36!h6nhMgCWEJ;^&5dby%TkyHq4P1nO57(J6OcC=4OS5 zHzm^My7#bKTvR27brkS6J4ss9|4^at<8WOp!{bSHk$-+VWhd~yLL@a5OE7q+3`rO2 z$d|(A@FQ*08(f@dF*TS#EdJZUTP9_(w7Bqe(sua@Jyz*!-iw_RiPPYQfNV{H9`Uibwr_4C?Sy3b< zJ{is?Y8W~_ESrQqw*|2ieD5MP1f*4{EpUP^8ADnU-m1`vS+xsNGux_-MG@21Cl^oo zal%IyfapJ-sTr;k{cRD~#Ox8%vi0eoG7y}&YoYV|zdc7BJhBXnW^HZuh>8fF_qg&X zj`KJ^v#Kx?#(>1QfpgG7RR)Kxm~VU-|Mmo_Nj@JcN4)1)B}ZDAw`Q?dwK`APLgbe{ zdy7krDLB7SU*yRhCFQ4`XjThLof~31WT8eS36+<2@!nv0nx#CA)yhN!bCew3WV4cO zEhJ(Oqw)x2F zAHCxSa1b=yhPTft!d&mF0Vg(1}72!kc2QzdpQA z1bq4)bmA1wS{O99?3NJT-)blSqbvl`=a_%5HAHuf`*x{KqcEju0xJmoEWtQKyVC(s z0rH1=RI|2fcA*wPx6p8o5N~U!KqNoKqeQXi$;$X z#FE=Kin9K4*LFDE95(e~va-E(ubR*Xojo1k?>X2`Al=wp`yo>UUnYt;mMI#C80X(a zLQ$PJ7PQPXo0-j#Gv&~?bTS9L64mT?(w)DORPiqbOyuf3Y5sA}8BQP3P=)bIvp1gQ z(Hs_Jzew>nr!b0!Ed$qwY*Hol-gT2t9TzM!^B}K(VU0-zZmo|_MNt_b=9)PTfE3)IB7 z2l0mj5qdH_%@-&MC@{5W+~TT{CJuNli0e7L!61SaAcz-|xlXi!L6;r`APb}P!U6b4 zaiXWDO(kEZD!>7Hk~NuB5s8d4OT$ncny~}}2HH8D+X0vewgQd?~wI4KCn#61z|dg{}Qs{STNSC@wZyBXI0R9QkveH9E=8kV&k` zw}_Qg$G*9!Yd(Vocw+oI#)It4=|AcMEpR%LYCi60G_Nnfj(ndB9lNhDvhvG|tV%e)m=VRpmN zUZBO8I^ZVE*ONwvs2J-+F_6sg&SD;q9uYI}h(^HAkiy!}RJbVG_i2!`LhX>_9txp; z6_-wW%=bP@8IrPkgGB`D;a3BgpI09g@H8z(Z(zI?IBl2X53O*|#-c5@Obxr}_*AIj zoYVBZUUJ>y&k^SQCN@I1B<4Y*<*3#$IBh_+D5hGm1Fo=aKs)T6~n*G`UB8lZYcbjl#GBmQE__K7BD=+gb`*KN%UuN(LWWhvSF(OKW zSBbbR1!qMQk}{mT5+qC+od>IVo9T_D$!PTg%y^?~wOJn@i)FQ#P8jYY76%YrQ4JOa zX*{Op3!v4BZg<%lcVd9<8|tw9(uWKfMxw{Eb^gP>nFbX2xw>GR!SUH_Je6BlFJ`O`*>V!; zffO>6N-I|HF#1Ma?u+S|DYiM_milJo;j~}yw@W|X?~Al>p7ygY&2!oNBjdnIQ~m6$ z`uPKpVt7n!9$8zJpB7xpWol`sz$S1$j$^!eD^|}43D#@;yhZL%Ln)Hjuy7d0G9;JB z$bZr%cFI2ycg_{g?UO~IM}IwXj;x3RV%u@hth0V(-+c8h!v)$nzCh(Bk2f@eI{IA8 zVqt%!BbneMnd(4HcKO_rc+(K@&3>{MF}*t+-8NwIaVjGx&IV5aCyrVCwT@S^_$gD% zdXTQMZaxIqU>bmaWhVX+-sBrI8{6)Wo&g62jgaL{E{kk1B}W?HLBwqun?`0&ck9yX zRREXStt>{X82lR;FSUJ)Jed%GXnBg+@X&DyKC6~}ZJ7I9;SXZ~%K_2eFO zFq5&2Y0!Y zQ#iUNyoKLj3Z!<@ODKtt;elFoR^wtZrA!!J6L#!7HyU(E35D}24b81|ux90nQ9V|r z)*Wv*(qDwpOL4RE!7ZVu2H!#)wI;2%kt-gnDL9uVyyVP|13Xh7rcC7_sO73R0w8oLRT{1Qr3;7uPO4=`B5}SD ztWe1|800lPAnI&J!1V?nA{2rq$LxjZhzcr%W{$t4{H!?;Wvi4a8`8-xsPCalFXS&h zuyB~N5+fxK+7KQmUfgFMU2+4NgFx4zJFTWFo~a}r{F7Y(39e2T{Z|6^^z~xOzxs-x zS$T~CwvyfMMDKh!CN{=~h5uv~*5b7^fDvUJ?j1N2x>Vi_^z`mp_>l+$s4|E$b?tF0lVK}zBqdIoL6d)XOUMoJ`cdxIXCa2aIQ~gunJzYSeVxcRu}yJ`?xHC~s0`?I`8*})xcWE| z?uONw2OYfe>j-sRog+`TVx@L~UaHV-d|Tsu3m2Cl)X+yG#L}cERCH0^ zA|*=ENmP^t6Ep=$&PCr{fnOInXk(aNW$jd1=|S)zw~=g=)yf(e3ng{Il*1SpdlFeL zeo7~TB)+vG8XOhRZC;{pfi8F19q}FNnm{Kvie`_EZJhp4mzJv=U=kJ1lcuRV$B7y7 zZB|K+?J#?fApT}svcs16NzowJ)U%PEjc$~4BiEj8XrpW;Rer4`5m{Bm>OMyAx`_aK zvx^HzJ-%|N(fl9cKJxx1IuA8bwH#`QXqZ2uu0om>M{e2b{#}#89@FlyFUH>fY12c_ z1e5GI3}U5NeNlq>`>D$w*c7uIX~tGUO^J;2g#ULWyf=CTR@*biZUUl`V&nPmQA9%8U&&Q12y<8eaZAGYD%zm z1#`mOnSlm4+S_bvB>yRuPhH7cJcn{$B(d+~#FBQAK|@(VYITzPB7e)kW1Y`TDN06{ z*VOyI*D~`!kel$6Wo?Q9S+Zp;c1Qt7ToQs)f`oPiF_bOI3UnpMLI5%!t?##Um*iKA zJZeMguxc|5XrG7ezm9D#L~1|1!}^RFm#c~;OsPNB*Y4x7G~A~A74q6(FkfdH;8E}c z;csu{LUT~67kRo~^U=2eMkYvVtF7uoY22~)xP**4xUz3aq?q86Ne8vi5jL*68(2)3 zpfa6w&ekHWnu@QbRvRi7%#!e$r6~3X)fY6f)>Iir?wPbq(7k3I$YKC^vGH}ym zKq(;mEHpbB(N68X*%t>{+Mld?=UyGeJdzp8(W@WM7IL8>574UgkZkUReuKhYVmS`>ElEN*8)m{s4?2I08SMQ{2u{f3@24tz@P9Y4%rDhv$%@863{W*~2LHvDK`vv+*94z}Rf z_w5tnpO-1YptG6|Wq6mp0Eb0J=v}3=rzDN%;{39Iz36v*lnrIQLHURGNc3{~r1fb* z-rUNTvI;*q)(5tyHFj&6CzRV566Cdn#X}h{Q7l~|gobqIgQAfh!hg65p$Kto3StL2 z57{?E(y86ns$qK<;I5_%ZCoh{o{RRz-D|FNO?U1B|7^V*=?g0pvTpn5ITI@sLdRff zG>T!!)z7kM9sUEjK*#7BBJRQNZ^QDsi26FX2_jv+*|JjjPp)xB;f|ap79ZhTa0y;} z`@+o8+b1KqKd;O)Vs9|+{`dItH)BtPMpv8rdy?NG{PFMtZ=W@@2SH!q;Xkd+?p~P& z+0mQ6!aUA`l{N<;<4MW$AC4tt_=BIl?v9LSg-| z|I@Pd+8FcgnS=p~hf!GTainYZ^9Z2(23p zcC7J?7?%jI*x?1NpC3&Wnm&Y8orFLO5Af&sLNr3LmZ)yIDCuLm(3cDn(Z7i<`5n3+ca?n4e@C=T|)<_`9>H>)BIvxEwE16skR8>8PmXmA}@f$%XpdsROHAo)?f}0%cMEqiFOd4i8iz!GuBKR zhq&!qb7$_r>y4+^F=Ca|%XVf&^=Ui@QthPlp56qpZah$l^wNJgO5BD{+4^XKe-k(7 zHR%^m>*C7>aIT{KW{k#llK-xFdDgDiv8;F~H-8&^7;hQrLWNyu zf81o4(()CFiHh(5K_05Hg0s`iOx|`QZdMhx@opcU%X7Isg?FX3{vcQDo|haAxBsrD zkcK9#n<21{2xlIET;a8gwB(w~%e->tTX?zS)$VU(%uD_vtfRgo5a}TiXF@s`5ms}( zXA3Jxb<5R=pF7?K?S=(dN=i7$=>qXV)Ncm(_@$A*|K0ec$iivK#cq(&(%Mo8G7zX? z7`#QNUVP)ihw$^7A?Q&3wK7sMOHgY|ok|oQwMA(&o{FH!hr$)>(`&VdEg}kIH&Q+e zR}Vee*#Li8fs06%<)rw8^ZDaAK&<97s)t;d^(x-hM@mV4+8xXmF)@n#8{tp4rW!1X2M zRLIy=W_uw&7(F$Z{8J*#gUwIZ8<4hnHj7zVIeR-7SddN7V#Bq{O9=|utnP3o5+%H) zW@0DHj{E0atXp@3(~4XhIy+bWltrM!+2(D(0qSlMCMT;SL~7gWjBDcg#=2-Ql`oAG z!_0)>Qsh-Fxq?RxR#qZ)pbjH1zrrkh)MB!JnfwFWSoU{$38V&H2uBTvBox`ajlUkK zMAf=vO+5jrI21cTYsx%`)N%Xmce5OOJhgUHXE~1cxh9keu0}Qbc;I0CBagEfM8YbC zeC<*Z&s6IBbJH{BR0?UuOh zQf$>J9If*tlRPxy7GzHy3!xD&M>s1ILKG*gsR7jQi~FlZvu&P9fPLeXC8N-mrx({#C(l&=oG4gmEKM_WYFLyNQ%YMb-(@Cizi#F?48VoU_) zcef((S4zG{z~+lN=?on}BO8BGF54-MTD-1C8k46Oaq2yDQ4Scn55A{%{VyL6xn zi0mVTJbIW%et!bs{NV}B2K4St)`|U7mkwM7t^49p|F|aVrmz~lhHEK3AHX7sZ7(x2 z=`$secQLltA6pd5icD3f4hvTn=9jpyxGTsSr$S~Z=KKQMNPskHSTf=RR4mwDbt{a^ zDt;6v5L{rx{F%g=9{FBx$Thv^}H7Y#mZ;$b@57F4h2AU(tzQ_i!DZATkP_o-lU_RZ5~izpEH| zymIR?%r8aLeoaq9r<@U9-6SUZp;Mqm{jeE^*mTM}A=2WszC(7xF*dL|71%zt^i#js zeml#6TFv?p=sd#Ruf+*aCsr8(sR;cggTo#WIOWv9NnDz6I*8L8A$*q>hR@y4Ptqk* z1kdpnnYRl#3P08bv)POA^{^PK!6nt}z0AS!Fg`HCH3nWZu4h}?ISa@&j+x9S) zX(V82vDjJ(J|wv*Itb)Y*&?76!Z=imdUy(_KK3z?B&7H7xUmqlCz$QBouEH4r?YJT z77tHalVBvH|E$%!oc*veA%jurwe_yC#22vc#f`@@XGfRi~21Y2nK0?8p>LdL8PQ`{@@xZ+I@ z$!q_oc4B6Mz42cL{&!y59*TlR7mU**Ys?nq#=TEL}nsoglD+DLY(Me#=M zE<5Uc8vy9J*#Ek3#BG~f#MXqm+46@07K`t39&2u$ruY`ikPzzKh?dP@?6Iw_fUbS-qoW#;e5l8Et7 zQIb~(+=6!Q(!G*?#Q&sQA5s?p6l~TIls_0AjXW4SZ)<}IuD~>o`09#vU zTd3i=2HAid)xNA`b%-YVCV95)hC^v*c*F#824_w@$y#aH0lWOY0M7OTsDZ&OS ztbMJxy`0w=MUW$&u9r*7VSZ{ogi*-q(Rz0fd~ZXk*X$P^+yf$?A?9RR*rU* z{Z935e+Xw%MMs*#ucUal2uf(2yLtE@by!t`NJlnzwOneqREacyjf~Zl1SnOOL$bIc z4vg0i$3q1-rOWU~CtQ1wh8$|U4f|ddgATS?r@;6FUN8d!uCj=p&0LoH1fYqOv+d5T zxU#1PaKvb3DA9kBGeB{u5sID?`Hf>0YvPCvn!*?&?Twi$Y{xO$t*4;dU0v9yowM9|nXyvPaLeIz)*=+{EkGLXisg9OZ#C(ATJ~Oq z0p8!w(MD_Yb8+MRttpZD7(a^>yx;{D(wd>(jsgEoD}K4(_|cI|m>kMC!h@i84nKxe z;v6oY_M^SL5lB<+uz$EJ1Sn?nc;h>nk@FG0O*OotteT^38Rlq;VagA#GI4s#X{obG zTY5x%hN=v?@}#dX)>+6%a0W*!$ukmeF_R|?qO(FtW~Az`K%|uRjL*@vBWq&PHz7jj zE->9^z@1z+^73fgD_RB)b?_1%d#w>bWLPti`?q!z0SA?Bl$N%yn%KTS=_Bp^Mju{u z1eQyXu;qBt&HI@SY-*U9sa#_L&WpU@Oq8%pV2&-zs5v6B(v?7`p@xoGaFObo?Lu|VgtMWN?oaF#)Au(^uMJ@ z8fwv_(Fd62sRp}B)DB_cX{JN6kMp1Cmt$q|l6`za1PhA3ixkbY=15hzG?aM}h@;4D zmOh5xiGCqq&pL$I4~NpNyE3_(=GaU>#d7*Ekb!&6av%u1o8=7RPD$2U{0rszaT z)7pogEL*dyy5jDU$@X0Blh&sjc<5f@ND}^VW!j1I4WM9R7?ZpVi{Qmjtn+;bL^oB> zQu;A}c$%@AGpi8jW8s8I4dMwW(T-$5Y!uH-#nKHAnI<0Mm$z_f7I@(Q21hB%-1<^_ z^0gigkhWXwsLgb5xRGQ{#zD{7$9ec7kpho%{{ui~hN25zkn^RN+{Fx3fx9TfhTiM- z$+a3g%KPfLtuA%L*lKByFz&(M+&^NSm|`>Ew1JDPu?j47EIR*|H8RHsh`6fQDir^*SA{*#zY`*z;Fc(d>@Et7ZbdjUoQ)jP z;JH*(-9{&U=FyVA8xdaDvlew59&TrwwQv7+tu%Vql zxx!bozscxv;dwP{-=(z`j;<6Y9VTH9C+0s6x$Hr%2{F?q#v%}QD09pA1G{!6g>g_B z-ivIFv8bIV8Db-wHQPIO^E~VeV*8^wsFd&h#8fIEooj7s1IGy#9D0|H8dXJVHabF;0#TL6;A`?LIUqkl<1!dEwAiQb(y0Jgl4~_Aox4NAXp) z6!i&Q`(e>@PPF^F9Cv0I9QQv@HH1WWQd#PMBNh$)uN=(FiUHrnifQPKmFQaXBj7h| zB6Pe=AThpkxp70tCszjR15E$492#n%Lg5Qo&B7&nHnPC>HsaPrR%>baXq-)Rk>w9F z6Sou};suK?JU|4SnP(uO>XWMCIFS&_Xmxn1> zSj9}rX~iepZ#4o;07nhLiM(zQ6{s^)-SXiNk@bm!%DKTycP`6+l>&xjyD&MhNjIXy z6Cj{@81ZR(qqIRn4HvXgL0|@3jI3AXbxlJYuNbV*m zE-dM~Iv}dd*Qkwmm_p6-4Z8ERwD26^O_o2Aec80{VrHaN?}UQd2ac15lX2Iiu@wxp zbo(~>Lda{UNjVgO^(EVzLPOYHV%7360~hcNt~6Qj9i#aX z$^b0jjf`;RtQ|Ry_SjmXTBI{KEUY!fwl3%MH(T*<~Awm|xzr1*1Pe;V$&{TF?7 zcrzR*=Q(xG#_kw{9E0+mt|K#~VrWSJ?8RpDhf9Vs- zt~4s4&4)*8r`t@`q3thdcPo8Aj&|dpk%QKZ+Z4r=c%XucX`-zM1v8#lPVak^ei84F2>Mj_>Ep%BByY2x*N{(xSMWTDZ6 zy(HMZ^1XuG*TO4ySixs1mgBClL+jhu9jCV5L*%BapI@@uyJ{@cdrkvlOb>*7KcS0_ zfF`BTQdYk{%`p5(%lW_Io^(0zA*iN!%iO9LU!2p)rWthfAG5C)r5FI z|JBkm`&r-p{<2CCl6{TP^|hT=fENlHwMw~X%aS?hqX^b(D)#GCv=)CgA$rJz66{DU zbpBIVqC<|j*Oj?S28(v-H#}`XQuetk(%3ikBJJq2{eK<;535tHZnWzbHu53+Pgjv5 zP7XwG)&`;P#`5${-^t<>2Be?Xl10}zrE!M#uI-(=;c?Bku_F>6p|zU=1AMES#h1=F zTn;PI2joGwmf)9zbmRe#%L%F{CJ*pDY=Q^#w^U=16t%INiN78H)+WI-cAXPF3WX3t zo1#`G$F!EOi6kD{Vab-BS5Gc(IkWGde{X-GTO>Nl%O#b&5l0Y6^kDv{n%1VX$NTYX8r&!rN(k1!Qf$<*OjU;x{9Jz8wS5_S ze+5;{KyvW)p`XVfv$hBtx=yge=7g%S-5YbEyZ~BLr-pjOY2AD&bMqaT(g;^sYKFVu zT=Y%S)fQe7$V~Dtq3*T?z*Md}Ro7ja1pwpc6fW^v1cBo9BE}ZLq~2Q`nz7vW(bDz-=k z9P#1qVU-l5;a10D7B}Uhtu#>w%irbaQyoK`la_FA3GFxw00|X>g9i<_S@k5o)q=0t zj>_%G^pANLtE zbvbd+(Y=atz06PbSvpcrI}ZoJUZ+D_oKG5q#i_WHm=!s^uv96Y3CI^^;VSnNST%jP zy9Z&_p7vxNASVb-D~%gsa#p4YZD}(??0UsP$<$oA-xm5_A++p}kOc??^fBpwO=OlX z|IYb<5@$3EYQveL9r?vAixst5zJYb|XO)&qDZ{LT$gDhCzl@QuNdFxkPuPS@b|XW1 zPg&B}lRb8a-DW?=c*;NckaK2YbJ(Ifv1O1$kTN>pOd;ev!V&LHdcnY?4w9FvYA|QO zqpW5Zf7wMjgVoMQn^|^AW-5a|kA9|-Ua{0|;~W1LF0k>7Y{f>Lc4J9wFo7KigqsPd)Ml!T*vk z?egq}pQFMEg}?$QdvT0TF30x80Q43um;RW+9~8*YBKfIX$zv(ZBAHWPUhcRFZ(`7o z$TWKcXQG63U^CixOVee9*TV@B>xiNt^ zzCX%*r<#T>Ov_M#W<`|0nR81=QzJZwTK5BQ;~(Pb_lwtT5{p7a|H6ZxL4W2eh{$oI z3L4CUUw)CQNJ6GdhFztW_8cK&xSW|lKqnj$i>Zc|$n!KkbWpq}jFdYvfW+{@p;=6! z5s%xD+KMKB`?$g@m`kf*?s$ddkKCtKlXdhK_~J+;KJtp&jj^THQni``T~~B9m&Bk- zZz1d92GX)cAx_53IdEv`J*gHEx%X5QWyL~k8TF{v9#JS!<@p-@feM=}KeJZ2Z80NAmV5vYy8@duq&Yd}qF)EsBh3R)6yGIdQ!7Q; zn$uGKTJ8qX*1o!H42Jqnq7+BHD5m-O(j++8CNgAjl`)Nc(I0HUxeNp&$}(P8d^DCN z@AN7<-r`mD1d&oZrvZw7WDm%lz1P@60RBo-!mu6KlV;iUFNI~m4hnNz{B4rjbWD%q zQAZRXsS4b~HSUCG|44djCbi4LsS2yauPIuQ^Sm+7teg)2)87Q|zy@Ud_Yg+}frzKC zDrE^-_pj&1NF{_Of$ZoB681JNd0M=AhMWjy{0OCoDaN=o?yM|fd?=tom79gZ&O%kr z=w2(z)#?%YAW6vH^IC4A_3BJ)xWB=eZ{&*2z}^&Frj(!@LPd{GY)%Ct$bO*IRp%k6 zuxGEA1;tWz$;SBT%wko}35w~B41#i;Jr>rI{Te8Wsln-RJ;}anoXSYSUJy2*;EHqjZ%e5k+?=Oykrs4{V;io9h$K8aX8N0Ep7FF!d6C>c2oT_%sGb zE+>4%1pT{c{IpL_fyxn4!kjLPuVk>}J}H5!%lyzP)*R7X4mNZ`-qDiJh{DCzJrEX< zpZz7W7S1hEV+v4IVKT=R(BiZYxzQv8Z&D!kn@1aA;l0iUv31-$hlCOBKXcZnWta23 z#n}$L1Rua{mT^Y1clSgkHyMV8juxmlUAn^I>`VVe5NZx>E6d;J>VW$cXEaEs-=md7 z6oL|eBdXU9VWeRaIe-9K6D`Y^dYMM6 z4R-H}2x%!1U<0b91~n`^2^y(>OM$EP8FV^RZrw$eDK;8M5(70(kLEx|njnhY$kY!W z*rn}CExqq}a8YdJg-i`1!AbDJDEnOUEsLT`5DfVSGMgV~7$`H-FY>j=#U=0WhqWA# z|FB{K_)_F5Qj(U#h5Q)n>1@C+ z^gpjniCQ%GPD@x5K1CyGA-!z;wiX#VC&;Qr%M`hFaZtAXQLt2{a45_x=pRz)TDcMlT@`ox_Gz}_+p=)@DcOG6Z!|Xtdv!%wZf2u zy<_FjJ#nJ#60!QJOv7+Sl_7HqqqN{EIs%Ikfj?NQ%X4Up$ZlV;e>qt2*?K@&rbrJz zf{A6NwhHE=TIR{{+@z9rRKm{1L2e8X?A`dA57Jbf@fq`kupq|L!MCLUUXzrHjwPGT z7y@Q>b{YQMI=Bhz<|0Q->|YS*3{oJXY9;Kmi7*fRHWk+rqPM(e68Jd^lLDKBpwavz z*~*HO*hyfH$}CcKO?Ii#%^$`6-&IP5D%n4@?n+hz8x zv2Gt#?lK2lXX-U`ufHY-N7@1c8_YKx!rHFWxR_XYE69nvbyGY)l@4e_#m(qCQ@KH! zzh->s?x-iWiA7t+Cwz$y4eUHi{;8S32Z#=eDg+Q8+_wE=n zg^_&OheJUUXVtwY!rvg+4iZ3?A9d`r%FuDIuQNpc?k>={;mw{sAuUaqW9Dm^_cueuH@>Tki}k9aGlr zAp}LG198F-j#;Q0p&v=$PA)I zp)zd#M6T)C1I>0-$h~|sMj4-FgN>u(&^JJy)6PUb&CW@jfUH4s@8H}IkpJGXCm%+;mZ5$$vUC;(oIv)g5}u(JD3!uAiS1{Z$E zdKbecTqt-WQAr;k9(uIO|6VsQ#W%vs-5~6(@bn~>t(`1JymIEkw}kLMB4X+vxzdTaL~RY|rx1qje~wrj)O_ad&IEq92sqru+xI-I{! zi`j7_F+6TK^i0iP7z}WAC}zgLE?&OX{@o(e?K2!}xdbGND9U1JO^Xe;POkQbXtj^< zLy>9A8g%c&*7cdU@4|Y}eClY{4s}nFMdXZoGHrK1z0<9KO;gOJMze8{qwSoTk~~IH zi*M;ksvOn24;qg{vE)y;HXI7Qtb*K)5iadjj5SvA<50v@iV-3Q1(4n75NNU3*YZ}o)xt(--cp1n9 z#-rVHOifB{5zq>_Dq^0#JRyoN2FOKp zhO{{`WyWAO#y+uO&2HEbuQg6OS=xwVIz^!+5NYaPiCSzE5);@%pnWjs&jj2l|L9_) zx*c7hBi&Fe>os~@8bgqlxn7&j?d->?gCVXIPr7nEM&#rk4_vFUZim9$O0sii%*J|j z8F$KX3#&snUc3KT-$AZnjg5dT1=vsTM!U#N;1z#mI8wlZl6Q_{;2&6KH+qo`?)1(C zG0g0P>u)0BBoYEi0P>my))cT4ZU6H;Vuiu#ocr>%V^+Nqg>=pM{%`&Jj?8HV`Vqu^7!^%f$Ym|6!#Y)1F+`=6Hg1bNSQ!yc7cCDD{vt!xnLU18{9#K` zfSkQg4RcBjmy(fsJ7ZrDv*)EN2t5fIE`n=AZ-rk5B7zdO&}p1$w=%F|4F1gvAIH}~ zuDQn*(^)W-E*XZ*em-r77xk4bB2dR?O<@Zy^ZNM2XxJP3nCEm|87}FYYIblKk~Jlr ze*%v3nW||KBsy9rpCD{d=9rblLXb7uf-6lab@}j4s%+tz>l9hYIes>585kNj7r9No zT2DPfCA5I?Yr(-6Y&!YY6}7ZLqI;|lEKnpw%Y*!eeM|~~1+Vm!FN5ZNTfFbCm&M#D z{_3#}_HlOdBzB%X4(Fa(C>&eVZP1lqQ+&b_VBMew9HG>x(Od5mtr(;<6nO+#YzIYO z(FxihC_K_7Pe7&`G~CM%7r;mw=i@sm^R(~dS9RMk8OgS$f15!zJJTGhLdz|}M-_Ea zZ>S>=GuT&6aCjtehM50c6b_R3f&GPi149qa>vDUdXr9q~hO8D_V8Dp}8Hc+K5@Vr? zvB;Zk5uT~swN8#?Vhn@-(okW&M@PE;7+$F+t}_noXUg`l^_dFMn}uq(SfWi`lyDM{ z4ZO9b3m?vpWP5Gssa zd=k*kgDS1m?%KO7-Tu<`y>H5N`R0IZ(iShv+p$mgIOg``+BqI{6M9paGE20;=-7g$ zM;~WY9JIgK&|#!>1yMCh(`p-EQ8s5C%|8XQ)Nlgjw@n|vJL)={TNu#YHjXt`nYX`&Ks8uV&ZZcJHs8vtB8S*)Ev z%39`k7NmdiGAArjxPe7DJromUj@+uQJeI1hPvz-j6<6C2Y5S#+W0dd6wF%VG8S#y; zzhG+*hdTsK)r?|a1EC?*#4&cj@NqA}zQv!tp9kZijkwwBkn%0A>V(atZvg@;%c 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%0Dr7F+?o{ihLC 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®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