From 97111112703358f70c38fddf0187d3399dff2257 Mon Sep 17 00:00:00 2001 From: Jethong <1147925384@qq.com> Date: Mon, 15 Mar 2021 13:58:53 +0800 Subject: [PATCH] fix error --- configs/e2e/e2e_r50_vd_pg.yml | 25 ++- doc/doc_ch/inference.md | 77 +++++++++- ppocr/data/__init__.py | 4 +- ppocr/data/imaug/label_ops.py | 48 +++--- ppocr/data/imaug/pg_process.py | 19 ++- ppocr/metrics/e2e_metric.py | 8 +- ppocr/modeling/heads/e2e_pg_head.py | 17 ++- ppocr/postprocess/pg_postprocess.py | 51 +++---- ppocr/utils/e2e_metric/polygon_fast.py | 2 +- ppocr/utils/e2e_utils/extract_textpoint.py | 22 +-- ppocr/utils/e2e_utils/visual.py | 11 +- ppocr/utils/pgnet_dict.txt | 36 +++++ tools/infer/predict_e2e.py | 168 +++++++++++++++++++++ tools/infer/utility.py | 35 ++++- tools/infer_e2e.py | 3 +- train_data/total_text/train/poly/2.txt | 2 + train_data/total_text/train/rgb/2.jpg | Bin 0 -> 41876 bytes 17 files changed, 417 insertions(+), 111 deletions(-) create mode 100644 ppocr/utils/pgnet_dict.txt create mode 100755 tools/infer/predict_e2e.py create mode 100644 train_data/total_text/train/poly/2.txt create mode 100644 train_data/total_text/train/rgb/2.jpg diff --git a/configs/e2e/e2e_r50_vd_pg.yml b/configs/e2e/e2e_r50_vd_pg.yml index 22548a3c..08c485a6 100644 --- a/configs/e2e/e2e_r50_vd_pg.yml +++ b/configs/e2e/e2e_r50_vd_pg.yml @@ -3,7 +3,7 @@ Global: epoch_num: 600 log_smooth_window: 20 print_batch_step: 10 - save_model_dir: ./output/pg_r50_vd_tt/ + save_model_dir: ./output/pgnet_r50_vd_totaltext/ save_epoch_step: 10 # evaluation is run every 0 iterationss after the 1000th iteration eval_batch_step: [ 0, 1000 ] @@ -18,7 +18,11 @@ Global: save_inference_dir: use_visualdl: False infer_img: - save_res_path: ./output/pg_r50_vd_tt/predicts_pg.txt + valid_set: totaltext #two mode: totaltext valid curved words, partvgg valid non-curved words + save_res_path: ./output/pgnet_r50_vd_totaltext/predicts_pgnet.txt + character_dict_path: ppocr/utils/pgnet_dict.txt + character_type: EN + max_text_length: 50 Architecture: model_type: e2e @@ -51,30 +55,26 @@ Optimizer: PostProcess: name: PGPostProcess score_thresh: 0.8 - cover_thresh: 0.1 - nms_thresh: 0.2 - Metric: name: E2EMetric - Lexicon_Table: [ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z' ] + character_dict_path: ppocr/utils/pgnet_dict.txt main_indicator: f_score_e2e Train: dataset: - name: PGDateSet - label_file_list: [./train_data/total_text/train/] + name: PGDataSet + label_file_list: [.././train_data/total_text/train/] ratio_list: [1.0] - data_format: icdar + data_format: icdar #two data format: icdar/textnet transforms: - DecodeImage: # load image img_mode: BGR channel_first: False - PGProcessTrain: - batch_size: 14 + batch_size: 14 # same as loader: batch_size_per_card min_crop_size: 24 min_text_size: 4 max_text_size: 512 - Lexicon_Table: [ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z' ] - KeepKeys: keep_keys: [ 'images', 'tcl_maps', 'tcl_label_maps', 'border_maps','direction_maps', 'training_masks', 'label_list', 'pos_list', 'pos_mask' ] # dataloader will return list in this order loader: @@ -93,10 +93,7 @@ Eval: img_mode: BGR channel_first: False - E2ELabelEncode: - Lexicon_Table: [ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z' ] - max_len: 50 - E2EResizeForTest: - valid_set: totaltext max_side_len: 768 - NormalizeImage: scale: 1./255. diff --git a/doc/doc_ch/inference.md b/doc/doc_ch/inference.md index 7968b355..40f4d8c5 100755 --- a/doc/doc_ch/inference.md +++ b/doc/doc_ch/inference.md @@ -12,7 +12,8 @@ inference 模型(`paddle.jit.save`保存的模型) - [一、训练模型转inference模型](#训练模型转inference模型) - [检测模型转inference模型](#检测模型转inference模型) - [识别模型转inference模型](#识别模型转inference模型) - - [方向分类模型转inference模型](#方向分类模型转inference模型) + - [方向分类模型转inference模型](#方向分类模型转inference模型) + - [端到端模型转inference模型](#端到端模型转inference模型) - [二、文本检测模型推理](#文本检测模型推理) - [1. 超轻量中文检测模型推理](#超轻量中文检测模型推理) @@ -27,10 +28,13 @@ inference 模型(`paddle.jit.save`保存的模型) - [4. 自定义文本识别字典的推理](#自定义文本识别字典的推理) - [5. 多语言模型的推理](#多语言模型的推理) -- [四、方向分类模型推理](#方向识别模型推理) +- [四、端到端模型推理](#端到端模型推理) + - [1. PGNet端到端模型推理](#SAST文本检测模型推理) + +- [五、方向分类模型推理](#方向识别模型推理) - [1. 方向分类模型推理](#方向分类模型推理) -- [五、文本检测、方向分类和文字识别串联推理](#文本检测、方向分类和文字识别串联推理) +- [六、文本检测、方向分类和文字识别串联推理](#文本检测、方向分类和文字识别串联推理) - [1. 超轻量中文OCR模型推理](#超轻量中文OCR模型推理) - [2. 其他模型推理](#其他模型推理) @@ -118,6 +122,32 @@ python3 tools/export_model.py -c configs/cls/cls_mv3.yml -o Global.pretrained_mo ├── inference.pdiparams.info # 分类inference模型的参数信息,可忽略 └── inference.pdmodel # 分类inference模型的program文件 ``` + +### 端到端模型转inference模型 + +下载端到端模型: +``` +wget -P ./ch_lite/ https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_cls_train.tar && tar xf ./ch_lite/ch_ppocr_mobile_v2.0_cls_train.tar -C ./ch_lite/ +``` + +端到端模型转inference模型与检测的方式相同,如下: +``` +# -c 后面设置训练算法的yml配置文件 +# -o 配置可选参数 +# Global.pretrained_model 参数设置待转换的训练模型地址,不用添加文件后缀 .pdmodel,.pdopt或.pdparams。 +# Global.load_static_weights 参数需要设置为 False。 +# Global.save_inference_dir参数设置转换的模型将保存的地址。 + +python3 tools/export_model.py -c configs/e2e/e2e_r50_vd_pg.yml -o Global.pretrained_model=./ch_lite/ch_ppocr_mobile_v2.0_cls_train/best_accuracy Global.load_static_weights=False Global.save_inference_dir=./inference/e2e/ +``` + +转换成功后,在目录下有三个文件: +``` +/inference/e2e/ + ├── inference.pdiparams # 分类inference模型的参数文件 + ├── inference.pdiparams.info # 分类inference模型的参数信息,可忽略 + └── inference.pdmodel # 分类inference模型的program文件 +``` ## 二、文本检测模型推理 @@ -332,8 +362,45 @@ python3 tools/infer/predict_rec.py --image_dir="./doc/imgs_words/korean/1.jpg" - Predicts of ./doc/imgs_words/korean/1.jpg:('바탕으로', 0.9948904) ``` + +## 四、端到端模型推理 + +端到端模型推理,默认使用PGNet模型的配置参数。当不使用PGNet模型时,在推理时,需要通过传入相应的参数进行算法适配,细节参考下文。 + +### 1. PGNet端到端模型推理 +#### (1). 四边形文本检测模型(ICDAR2015) +首先将PGNet端到端训练过程中保存的模型,转换成inference model。以基于Resnet50_vd骨干网络,在ICDAR2015英文数据集训练的模型为例([模型下载地址](https://paddleocr.bj.bcebos.com/dygraph_v2.0/en/det_r50_vd_sast_icdar15_v2.0_train.tar)),可以使用如下命令进行转换: +``` +python3 tools/export_model.py -c configs/e2e/e2e_r50_vd_pg.yml -o Global.pretrained_model=./det_r50_vd_sast_icdar15_v2.0_train/best_accuracy Global.load_static_weights=False Global.save_inference_dir=./inference/e2e +``` +**PGNet端到端模型推理,需要设置参数`--e2e_algorithm="PGNet"`**,可以执行如下命令: +``` +python3 tools/infer/predict_e2e.py --e2e_algorithm="PGNet" --image_dir="./doc/imgs_en/img_10.jpg" --e2e_model_dir="./inference/e2e_pgnet_ic15/" +``` +可视化文本检测结果默认保存到`./inference_results`文件夹里面,结果文件的名称前缀为'e2e_res'。结果示例如下: + +![](../imgs_results/det_res_img_10_sast.jpg) + +#### (2). 弯曲文本检测模型(Total-Text) +首先将PGNet端到端训练过程中保存的模型,转换成inference model。以基于Resnet50_vd骨干网络,在Total-Text英文数据集训练的模型为例([模型下载地址](https://paddleocr.bj.bcebos.com/dygraph_v2.0/en/det_r50_vd_sast_totaltext_v2.0_train.tar)),可以使用如下命令进行转换: + +``` +python3 tools/export_model.py -c configs/e2e/e2e_r50_vd_pg.yml -o Global.pretrained_model=./det_r50_vd_sast_totaltext_v2.0_train/best_accuracy Global.load_static_weights=False Global.save_inference_dir=./inference/e2e_pgnet_tt +``` + +**PGNet端到端模型推理,需要设置参数`--e2e_algorithm="PGNet"`,同时,还需要增加参数`--e2e_pgnet_polygon=True`,**可以执行如下命令: +``` +python3 tools/infer/predict_e2e.py --e2e_algorithm="PGNet" --image_dir="./doc/imgs_en/img623.jpg" --e2e_model_dir="./inference/e2e_pgnet_tt/" --e2e_pgnet_polygon=True +``` +可视化文本端到端结果默认保存到`./inference_results`文件夹里面,结果文件的名称前缀为'e2e_res'。结果示例如下: + +![](../imgs_results/e2e_res_img623_pg.jpg) + +**注意**:本代码库中,SAST后处理Locality-Aware NMS有python和c++两种版本,c++版速度明显快于python版。由于c++版本nms编译版本问题,只有python3.5环境下会调用c++版nms,其他情况将调用python版nms。 + + -## 四、方向分类模型推理 +## 五、方向分类模型推理 下面将介绍方向分类模型推理。 @@ -358,7 +425,7 @@ Predicts of ./doc/imgs_words/ch/word_4.jpg:['0', 0.9999982] ``` -## 五、文本检测、方向分类和文字识别串联推理 +## 六、文本检测、方向分类和文字识别串联推理 ### 1. 超轻量中文OCR模型推理 diff --git a/ppocr/data/__init__.py b/ppocr/data/__init__.py index 26a2a8dc..728b8317 100644 --- a/ppocr/data/__init__.py +++ b/ppocr/data/__init__.py @@ -73,14 +73,14 @@ def build_dataloader(config, mode, device, logger, seed=None): else: use_shared_memory = True if mode == "Train": - #Distribute data to multiple cards + # Distribute data to multiple cards batch_sampler = DistributedBatchSampler( dataset=dataset, batch_size=batch_size, shuffle=shuffle, drop_last=drop_last) else: - #Distribute data to single card + # Distribute data to single card batch_sampler = BatchSampler( dataset=dataset, batch_size=batch_size, diff --git a/ppocr/data/imaug/label_ops.py b/ppocr/data/imaug/label_ops.py index 85aa8bb2..6c2fc8e4 100644 --- a/ppocr/data/imaug/label_ops.py +++ b/ppocr/data/imaug/label_ops.py @@ -34,28 +34,6 @@ class ClsLabelEncode(object): return data -class E2ELabelEncode(object): - def __init__(self, Lexicon_Table, max_len, **kwargs): - self.Lexicon_Table = Lexicon_Table - self.max_len = max_len - self.pad_num = len(self.Lexicon_Table) - - def __call__(self, data): - text_label_index_list, temp_text = [], [] - texts = data['strs'] - for text in texts: - text = text.upper() - temp_text = [] - for c_ in text: - if c_ in self.Lexicon_Table: - temp_text.append(self.Lexicon_Table.index(c_)) - temp_text = temp_text + [self.pad_num] * (self.max_len - - len(temp_text)) - text_label_index_list.append(temp_text) - data['strs'] = np.array(text_label_index_list) - return data - - class DetLabelEncode(object): def __init__(self, **kwargs): pass @@ -209,6 +187,32 @@ class CTCLabelEncode(BaseRecLabelEncode): return dict_character +class E2ELabelEncode(BaseRecLabelEncode): + def __init__(self, + max_text_length, + character_dict_path=None, + character_type='EN', + use_space_char=False, + **kwargs): + super(E2ELabelEncode, + self).__init__(max_text_length, character_dict_path, + character_type, use_space_char) + + def __call__(self, data): + texts = data['strs'] + temp_texts = [] + for text in texts: + text = text.upper() + text = self.encode(text) + if text is None: + return None + text = text + [36] * (self.max_text_len - len(text) + ) # use 36 to pad + temp_texts.append(text) + data['strs'] = np.array(temp_texts) + return data + + class AttnLabelEncode(BaseRecLabelEncode): """ Convert between text-label and text-index """ diff --git a/ppocr/data/imaug/pg_process.py b/ppocr/data/imaug/pg_process.py index a496ed43..58837d7b 100644 --- a/ppocr/data/imaug/pg_process.py +++ b/ppocr/data/imaug/pg_process.py @@ -21,6 +21,7 @@ __all__ = ['PGProcessTrain'] class PGProcessTrain(object): def __init__(self, + character_dict_path, batch_size=14, min_crop_size=24, min_text_size=10, @@ -30,13 +31,19 @@ class PGProcessTrain(object): self.min_crop_size = min_crop_size self.min_text_size = min_text_size self.max_text_size = max_text_size - self.Lexicon_Table = [ - '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', - 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', - 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z' - ] + self.Lexicon_Table = self.get_dict(character_dict_path) self.img_id = 0 + def get_dict(self, character_dict_path): + character_str = "" + 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") + character_str += line + dict_character = list(character_str) + return dict_character + def quad_area(self, poly): """ compute area of a polygon @@ -853,7 +860,7 @@ class PGProcessTrain(object): for i in range(len(label_list)): label_list[i] = np.array(label_list[i]) - if len(pos_list) <= 0 or len(pos_list) > 30: + if len(pos_list) <= 0 or len(pos_list) > 30: #一张图片中最多存在30行文本 return None for __ in range(30 - len(pos_list), 0, -1): pos_list.append(pos_list_temp) diff --git a/ppocr/metrics/e2e_metric.py b/ppocr/metrics/e2e_metric.py index c6cd1db9..04b73e0c 100644 --- a/ppocr/metrics/e2e_metric.py +++ b/ppocr/metrics/e2e_metric.py @@ -19,11 +19,15 @@ from __future__ import print_function __all__ = ['E2EMetric'] from ppocr.utils.e2e_metric.Deteval import * +from ppocr.utils.e2e_utils.extract_textpoint import * class E2EMetric(object): - def __init__(self, Lexicon_Table, main_indicator='f_score_e2e', **kwargs): - self.label_list = Lexicon_Table + def __init__(self, + character_dict_path, + main_indicator='f_score_e2e', + **kwargs): + self.label_list = get_dict(character_dict_path) self.max_index = len(self.label_list) self.main_indicator = main_indicator self.reset() diff --git a/ppocr/modeling/heads/e2e_pg_head.py b/ppocr/modeling/heads/e2e_pg_head.py index 106cdfa6..a3bc39aa 100644 --- a/ppocr/modeling/heads/e2e_pg_head.py +++ b/ppocr/modeling/heads/e2e_pg_head.py @@ -228,11 +228,11 @@ class PGHead(nn.Layer): f_score = self.conv1(f_score) f_score = F.sigmoid(f_score) - # f_boder - f_boder = self.conv_f_boder1(x) - f_boder = self.conv_f_boder2(f_boder) - f_boder = self.conv_f_boder3(f_boder) - f_boder = self.conv2(f_boder) + # f_border + f_border = self.conv_f_boder1(x) + f_border = self.conv_f_boder2(f_border) + f_border = self.conv_f_boder3(f_border) + f_border = self.conv2(f_border) f_char = self.conv_f_char1(x) f_char = self.conv_f_char2(f_char) @@ -246,4 +246,9 @@ class PGHead(nn.Layer): f_direction = self.conv_f_direc3(f_direction) f_direction = self.conv4(f_direction) - return f_score, f_boder, f_direction, f_char + predicts = {} + predicts['f_score'] = f_score + predicts['f_border'] = f_border + predicts['f_char'] = f_char + predicts['f_direction'] = f_direction + return predicts diff --git a/ppocr/postprocess/pg_postprocess.py b/ppocr/postprocess/pg_postprocess.py index 1f1ab60e..6d1b7d7a 100644 --- a/ppocr/postprocess/pg_postprocess.py +++ b/ppocr/postprocess/pg_postprocess.py @@ -30,30 +30,14 @@ import paddle class PGPostProcess(object): """ - The post process for SAST. + The post process for PGNet. """ - def __init__(self, - score_thresh=0.5, - nms_thresh=0.2, - sample_pts_num=2, - shrink_ratio_of_width=0.3, - expand_scale=1.0, - tcl_map_thresh=0.5, - **kwargs): - self.result_path = "" - self.valid_set = 'totaltext' - self.Lexicon_Table = [ - '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', - 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', - 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z' - ] + def __init__(self, character_dict_path, valid_set, score_thresh, **kwargs): + + self.Lexicon_Table = get_dict(character_dict_path) + self.valid_set = valid_set self.score_thresh = score_thresh - self.nms_thresh = nms_thresh - self.sample_pts_num = sample_pts_num - self.shrink_ratio_of_width = shrink_ratio_of_width - self.expand_scale = expand_scale - self.tcl_map_thresh = tcl_map_thresh # c++ la-nms is faster, but only support python 3.5 self.is_python35 = False @@ -61,16 +45,23 @@ class PGPostProcess(object): self.is_python35 = True def __call__(self, outs_dict, shape_list): - p_score, p_border, p_direction, p_char = outs_dict[:4] - p_score = p_score[0].numpy() - p_border = p_border[0].numpy() - p_direction = p_direction[0].numpy() - p_char = p_char[0].numpy() - src_h, src_w, ratio_h, ratio_w = shape_list[0] - if self.valid_set != 'totaltext': - is_curved = False + p_score = outs_dict['f_score'] + p_border = outs_dict['f_border'] + p_char = outs_dict['f_char'] + p_direction = outs_dict['f_direction'] + if isinstance(p_score, paddle.Tensor): + p_score = p_score[0].numpy() + p_border = p_border[0].numpy() + p_direction = p_direction[0].numpy() + p_char = p_char[0].numpy() else: - is_curved = True + p_score = p_score[0] + p_border = p_border[0] + p_direction = p_direction[0] + p_char = p_char[0] + + src_h, src_w, ratio_h, ratio_w = shape_list[0] + is_curved = self.valid_set == "totaltext" instance_yxs_list = generate_pivot_list( p_score, p_char, diff --git a/ppocr/utils/e2e_metric/polygon_fast.py b/ppocr/utils/e2e_metric/polygon_fast.py index c78e2a1e..81c9ad70 100755 --- a/ppocr/utils/e2e_metric/polygon_fast.py +++ b/ppocr/utils/e2e_metric/polygon_fast.py @@ -1,4 +1,4 @@ -# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# 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. diff --git a/ppocr/utils/e2e_utils/extract_textpoint.py b/ppocr/utils/e2e_utils/extract_textpoint.py index 2d793aa9..53552809 100644 --- a/ppocr/utils/e2e_utils/extract_textpoint.py +++ b/ppocr/utils/e2e_utils/extract_textpoint.py @@ -24,6 +24,17 @@ from itertools import groupby from skimage.morphology._skeletonize import thin +def get_dict(character_dict_path): + character_str = "" + 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") + character_str += line + dict_character = list(character_str) + return dict_character + + def softmax(logits): """ logits: N x d @@ -164,7 +175,6 @@ def sort_and_expand_with_direction(pos_list, f_direction): h, w, _ = f_direction.shape sorted_list, point_direction = sort_with_direction(pos_list, f_direction) - # expand along point_num = len(sorted_list) sub_direction_len = max(point_num // 3, 2) left_direction = point_direction[:sub_direction_len, :] @@ -207,7 +217,6 @@ def sort_and_expand_with_direction_v2(pos_list, f_direction, binary_tcl_map): h, w, _ = f_direction.shape sorted_list, point_direction = sort_with_direction(pos_list, f_direction) - # expand along point_num = len(sorted_list) sub_direction_len = max(point_num // 3, 2) left_direction = point_direction[:sub_direction_len, :] @@ -268,7 +277,6 @@ def generate_pivot_list_curved(p_score, instance_count, instance_label_map = cv2.connectedComponents( skeleton_map.astype(np.uint8), connectivity=8) - # get TCL Instance all_pos_yxs = [] center_pos_yxs = [] end_points_yxs = [] @@ -279,7 +287,6 @@ def generate_pivot_list_curved(p_score, ys, xs = np.where(instance_label_map == instance_id) pos_list = list(zip(ys, xs)) - ### FIX-ME, eliminate outlier if len(pos_list) < 3: continue @@ -290,7 +297,6 @@ def generate_pivot_list_curved(p_score, pos_list_sorted, _ = sort_with_direction(pos_list, f_direction) all_pos_yxs.append(pos_list_sorted) - # use decoder to filter backgroud points. p_char_maps = p_char_maps.transpose([1, 2, 0]) decode_res = ctc_decoder_for_image( all_pos_yxs, logits_map=p_char_maps, keep_blank_in_idxs=True) @@ -335,11 +341,9 @@ def generate_pivot_list_horizontal(p_score, ys, xs = np.where(instance_label_map == instance_id) pos_list = list(zip(ys, xs)) - ### FIX-ME, eliminate outlier if len(pos_list) < 5: continue - # add rule here main_direction = extract_main_direction(pos_list, f_direction) # y x reference_directin = np.array([0, 1]).reshape([-1, 2]) # y x @@ -370,7 +374,6 @@ def generate_pivot_list_horizontal(p_score, f_direction) all_pos_yxs.append(pos_list_sorted) - # use decoder to filter backgroud points. p_char_maps = p_char_maps.transpose([1, 2, 0]) decode_res = ctc_decoder_for_image( all_pos_yxs, logits_map=p_char_maps, keep_blank_in_idxs=True) @@ -417,7 +420,6 @@ def generate_pivot_list(p_score, image_id=image_id) -# for refine module def extract_main_direction(pos_list, f_direction): """ f_direction: h x w x 2 @@ -504,14 +506,12 @@ def generate_pivot_list_tt_inference(p_score, instance_count, instance_label_map = cv2.connectedComponents( skeleton_map.astype(np.uint8), connectivity=8) - # get TCL Instance all_pos_yxs = [] if instance_count > 0: for instance_id in range(1, instance_count): pos_list = [] ys, xs = np.where(instance_label_map == instance_id) pos_list = list(zip(ys, xs)) - ### FIX-ME, eliminate outlier if len(pos_list) < 3: continue pos_list_sorted = sort_and_expand_with_direction_v2( diff --git a/ppocr/utils/e2e_utils/visual.py b/ppocr/utils/e2e_utils/visual.py index 6f8a429e..e6e4fd06 100644 --- a/ppocr/utils/e2e_utils/visual.py +++ b/ppocr/utils/e2e_utils/visual.py @@ -28,7 +28,6 @@ def resize_image(im, max_side_len=512): resize_w = w resize_h = h - # Fix the longer side if resize_h > resize_w: ratio = float(max_side_len) / resize_h else: @@ -50,13 +49,11 @@ def resize_image(im, max_side_len=512): def resize_image_min(im, max_side_len=512): """ """ - # print('--> Using resize_image_min') h, w, _ = im.shape resize_w = w resize_h = h - # Fix the longer side if resize_h < resize_w: ratio = float(max_side_len) / resize_h else: @@ -84,12 +81,7 @@ def resize_image_for_totaltext(im, max_side_len=512): ratio = 1.25 if h * ratio > max_side_len: ratio = float(max_side_len) / resize_h - # Fix the longer side - # if resize_h > resize_w: - # ratio = float(max_side_len) / resize_h - # else: - # ratio = float(max_side_len) / resize_w - ### + resize_h = int(resize_h * ratio) resize_w = int(resize_w * ratio) @@ -114,7 +106,6 @@ def point_pair2poly(point_pair_list): pair_info = (pair_length_list.max(), pair_length_list.min(), pair_length_list.mean()) - # constract poly point_num = len(point_pair_list) * 2 point_list = [0] * point_num for idx, point_pair in enumerate(point_pair_list): diff --git a/ppocr/utils/pgnet_dict.txt b/ppocr/utils/pgnet_dict.txt new file mode 100644 index 00000000..b52d16e6 --- /dev/null +++ b/ppocr/utils/pgnet_dict.txt @@ -0,0 +1,36 @@ +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +A +B +C +D +E +F +G +H +I +J +K +L +M +N +O +P +Q +R +S +T +U +V +W +X +Y +Z \ No newline at end of file diff --git a/tools/infer/predict_e2e.py b/tools/infer/predict_e2e.py new file mode 100755 index 00000000..1a92c4ab --- /dev/null +++ b/tools/infer/predict_e2e.py @@ -0,0 +1,168 @@ +# 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 time +import sys + +import tools.infer.utility as utility +from ppocr.utils.logging import get_logger +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 + +logger = get_logger() + + +class TextE2e(object): + def __init__(self, args): + self.args = args + self.e2e_algorithm = args.e2e_algorithm + pre_process_list = [{ + 'E2EResizeForTest': { + 'max_side_len': 768, + 'valid_set': 'totaltext' + } + }, { + 'NormalizeImage': { + 'std': [0.229, 0.224, 0.225], + 'mean': [0.485, 0.456, 0.406], + 'scale': '1./255.', + 'order': 'hwc' + } + }, { + 'ToCHWImage': None + }, { + 'KeepKeys': { + 'keep_keys': ['image', 'shape'] + } + }] + postprocess_params = {} + if self.e2e_algorithm == "PGNet": + pre_process_list[0] = { + 'E2EResizeForTest': { + 'max_side_len': args.e2e_limit_side_len, + 'valid_set': 'totaltext' + } + } + postprocess_params['name'] = 'PGPostProcess' + postprocess_params["score_thresh"] = args.e2e_pgnet_score_thresh + postprocess_params["character_dict_path"] = args.e2e_char_dict_path + postprocess_params["valid_set"] = args.e2e_pgnet_valid_set + self.e2e_pgnet_polygon = args.e2e_pgnet_polygon + if self.e2e_pgnet_polygon: + postprocess_params["expand_scale"] = 1.2 + postprocess_params["shrink_ratio_of_width"] = 0.2 + else: + postprocess_params["expand_scale"] = 1.0 + postprocess_params["shrink_ratio_of_width"] = 0.3 + else: + logger.info("unknown e2e_algorithm:{}".format(self.e2e_algorithm)) + sys.exit(0) + + 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, 'e2e', logger) # paddle.jit.load(args.det_model_dir) + # self.predictor.eval() + + def clip_det_res(self, points, img_height, img_width): + for pno in range(points.shape[0]): + points[pno, 0] = int(min(max(points[pno, 0], 0), img_width - 1)) + points[pno, 1] = int(min(max(points[pno, 1], 0), img_height - 1)) + return points + + def filter_tag_det_res_only_clip(self, dt_boxes, image_shape): + img_height, img_width = image_shape[0:2] + dt_boxes_new = [] + for box in dt_boxes: + box = self.clip_det_res(box, img_height, img_width) + dt_boxes_new.append(box) + dt_boxes = np.array(dt_boxes_new) + return dt_boxes + + def __call__(self, img): + ori_im = img.copy() + data = {'image': img} + data = transform(data, self.preprocess_op) + img, shape_list = data + if img is None: + return None, 0 + img = np.expand_dims(img, axis=0) + print(img.shape) + shape_list = np.expand_dims(shape_list, 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 = {} + if self.e2e_algorithm == 'PGNet': + preds['f_score'] = outputs[0] + preds['f_border'] = outputs[1] + preds['f_direction'] = outputs[2] + preds['f_char'] = outputs[3] + else: + raise NotImplementedError + + post_result = self.postprocess_op(preds, shape_list) + points, strs = post_result['points'], post_result['strs'] + dt_boxes = self.filter_tag_det_res_only_clip(points, ori_im.shape) + elapse = time.time() - starttime + return dt_boxes, strs, elapse + + +if __name__ == "__main__": + args = utility.parse_args() + image_file_list = get_image_file_list(args.image_dir) + text_detector = TextE2e(args) + count = 0 + total_time = 0 + draw_img_save = "./inference_results" + if not os.path.exists(draw_img_save): + os.makedirs(draw_img_save) + 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 + points, strs, elapse = text_detector(img) + if count > 0: + total_time += elapse + count += 1 + logger.info("Predict time of {}: {}".format(image_file, elapse)) + src_im = utility.draw_e2e_res(points, strs, image_file) + img_name_pure = os.path.split(image_file)[-1] + img_path = os.path.join(draw_img_save, + "e2e_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))) diff --git a/tools/infer/utility.py b/tools/infer/utility.py index a4a91efd..9aa0afed 100755 --- a/tools/infer/utility.py +++ b/tools/infer/utility.py @@ -74,6 +74,21 @@ def parse_args(): "--vis_font_path", type=str, default="./doc/fonts/simfang.ttf") parser.add_argument("--drop_score", type=float, default=0.5) + # params for e2e + parser.add_argument("--e2e_algorithm", type=str, default='PGNet') + parser.add_argument("--e2e_model_dir", type=str) + parser.add_argument("--e2e_limit_side_len", type=float, default=768) + parser.add_argument("--e2e_limit_type", type=str, default='max') + + # PGNet parmas + parser.add_argument("--e2e_pgnet_score_thresh", type=float, default=0.5) + parser.add_argument( + "--e2e_char_dict_path", + type=str, + default="./ppocr/utils/pgnet_dict.txt") + parser.add_argument("--e2e_pgnet_valid_set", type=str, default='totaltext') + parser.add_argument("--e2e_pgnet_polygon", type=bool, default=False) + # params for text classifier parser.add_argument("--use_angle_cls", type=str2bool, default=False) parser.add_argument("--cls_model_dir", type=str) @@ -93,8 +108,10 @@ def create_predictor(args, mode, logger): model_dir = args.det_model_dir elif mode == 'cls': model_dir = args.cls_model_dir - else: + elif mode == 'rec': model_dir = args.rec_model_dir + else: + model_dir = args.e2e_model_dir if model_dir is None: logger.info("not find {} model file path {}".format(mode, model_dir)) @@ -147,6 +164,22 @@ def create_predictor(args, mode, logger): return predictor, input_tensor, output_tensors +def draw_e2e_res(dt_boxes, strs, img_path): + src_im = cv2.imread(img_path) + for box, str in zip(dt_boxes, strs): + box = box.astype(np.int32).reshape((-1, 1, 2)) + cv2.polylines(src_im, [box], True, color=(255, 255, 0), thickness=2) + cv2.putText( + src_im, + str, + org=(int(box[0, 0, 0]), int(box[0, 0, 1])), + fontFace=cv2.FONT_HERSHEY_COMPLEX, + fontScale=0.7, + color=(0, 255, 0), + thickness=1) + return src_im + + def draw_text_det_res(dt_boxes, img_path): src_im = cv2.imread(img_path) for box in dt_boxes: diff --git a/tools/infer_e2e.py b/tools/infer_e2e.py index c40b8e02..b7503adb 100755 --- a/tools/infer_e2e.py +++ b/tools/infer_e2e.py @@ -71,7 +71,8 @@ def main(): init_model(config, model, logger) # build post process - post_process_class = build_post_process(config['PostProcess']) + post_process_class = build_post_process(config['PostProcess'], + global_config) # create data ops transforms = [] diff --git a/train_data/total_text/train/poly/2.txt b/train_data/total_text/train/poly/2.txt new file mode 100644 index 00000000..961d9680 --- /dev/null +++ b/train_data/total_text/train/poly/2.txt @@ -0,0 +1,2 @@ +2.0,165.0,20.0,167.0,39.0,170.0,57.0,173.0,76.0,176.0,94.0,179.0,113.0,182.0,109.0,218.0,90.0,215.0,72.0,213.0,54.0,210.0,36.0,208.0,18.0,205.0,0.0,203.0 izza +2.0,411.0,30.0,412.0,58.0,414.0,87.0,416.0,115.0,418.0,143.0,420.0,172.0,422.0,172.0,476.0,143.0,474.0,114.0,472.0,86.0,471.0,57.0,469.0,28.0,467.0,0.0,466.0 ISA diff --git a/train_data/total_text/train/rgb/2.jpg b/train_data/total_text/train/rgb/2.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f3bc7a06e911ef87c0831e779d20e44b6d2bbea5 GIT binary patch literal 41876 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 literal 0 HcmV?d00001 -- GitLab