From a0d1f9236d66fc113e7d54462b255651ad7f8f24 Mon Sep 17 00:00:00 2001 From: Jethong <1147925384@qq.com> Date: Fri, 9 Apr 2021 16:04:02 +0800 Subject: [PATCH] add different post process --- configs/e2e/e2e_r50_vd_pg.yml | 4 +- ppocr/data/imaug/label_ops.py | 16 +- ppocr/data/pgnet_dataset.py | 16 +- ppocr/metrics/e2e_metric.py | 14 +- ppocr/postprocess/pg_postprocess.py | 101 ++++- ppocr/utils/e2e_metric/Deteval.py | 302 +------------ ppocr/utils/e2e_utils/extract_textpoint.py | 493 ++++++++++++++++----- tools/infer/predict_e2e.py | 2 +- train_data/total_text/train/poly/2.txt | 2 - train_data/total_text/train/rgb/2.jpg | Bin 41876 -> 0 bytes 10 files changed, 516 insertions(+), 434 deletions(-) delete mode 100644 train_data/total_text/train/poly/2.txt delete 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 be7529d7..0a232f7a 100644 --- a/configs/e2e/e2e_r50_vd_pg.yml +++ b/configs/e2e/e2e_r50_vd_pg.yml @@ -11,7 +11,7 @@ Global: # from static branch, load_static_weights must be set as True. # 2. If you want to finetune the pretrained models we provide in the docs, # you should set load_static_weights as False. - load_static_weights: True + load_static_weights: False cal_metric_during_train: False pretrained_model: checkpoints: @@ -94,7 +94,7 @@ Eval: label_file_list: [./train_data/total_text/test/] transforms: - DecodeImage: # load image - img_mode: BGR + img_mode: RGB channel_first: False - E2ELabelEncode: - E2EResizeForTest: diff --git a/ppocr/data/imaug/label_ops.py b/ppocr/data/imaug/label_ops.py index cbb11009..47e0cbf0 100644 --- a/ppocr/data/imaug/label_ops.py +++ b/ppocr/data/imaug/label_ops.py @@ -200,16 +200,18 @@ class E2ELabelEncode(BaseRecLabelEncode): self.pad_num = len(self.dict) # the length to pad def __call__(self, data): + text_label_index_list, temp_text = [], [] texts = data['strs'] - temp_texts = [] for text in texts: text = text.lower() - text = self.encode(text) - if text is None: - return None - text = text + [self.pad_num] * (self.max_text_len - len(text)) - temp_texts.append(text) - data['strs'] = np.array(temp_texts) + temp_text = [] + for c_ in text: + if c_ in self.dict: + temp_text.append(self.dict[c_]) + temp_text = temp_text + [self.pad_num] * (self.max_text_len - + len(temp_text)) + text_label_index_list.append(temp_text) + data['strs'] = np.array(text_label_index_list) return data diff --git a/ppocr/data/pgnet_dataset.py b/ppocr/data/pgnet_dataset.py index 10109512..ae063835 100644 --- a/ppocr/data/pgnet_dataset.py +++ b/ppocr/data/pgnet_dataset.py @@ -24,6 +24,7 @@ class PGDataSet(Dataset): self.logger = logger self.seed = seed + self.mode = mode global_config = config['Global'] dataset_config = config[mode]['dataset'] loader_config = config[mode]['loader'] @@ -62,10 +63,13 @@ class PGDataSet(Dataset): with open(poly_txt_path) as f: for line in f.readlines(): poly_str, txt = line.strip().split('\t') - poly = map(float, poly_str.split(',')) + poly = list(map(float, poly_str.split(','))) + if self.mode.lower() == "eval": + while len(poly) < 100: + poly.append(-1) text_polys.append( np.array( - list(poly), dtype=np.float32).reshape(-1, 2)) + poly, dtype=np.float32).reshape(-1, 2)) txts.append(txt) txt_tags.append(txt == '###') @@ -135,8 +139,12 @@ class PGDataSet(Dataset): try: if self.data_format == 'icdar': im_path = os.path.join(data_path, 'rgb', data_line) - poly_path = os.path.join(data_path, 'poly', - data_line.split('.')[0] + '.txt') + if self.mode.lower() == "eval": + poly_path = os.path.join(data_path, 'poly_gt', + data_line.split('.')[0] + '.txt') + else: + poly_path = os.path.join(data_path, 'poly', + data_line.split('.')[0] + '.txt') text_polys, text_tags, text_strs = self.extract_polys(poly_path) else: image_dir = os.path.join(os.path.dirname(data_path), 'image') diff --git a/ppocr/metrics/e2e_metric.py b/ppocr/metrics/e2e_metric.py index 75ffbfb0..684d7742 100644 --- a/ppocr/metrics/e2e_metric.py +++ b/ppocr/metrics/e2e_metric.py @@ -33,10 +33,20 @@ class E2EMetric(object): self.reset() def __call__(self, preds, batch, **kwargs): - gt_polyons_batch = batch[2] + temp_gt_polyons_batch = batch[2] temp_gt_strs_batch = batch[3] ignore_tags_batch = batch[4] + gt_polyons_batch = [] gt_strs_batch = [] + + temp_gt_polyons_batch = temp_gt_polyons_batch[0].tolist() + for temp_list in temp_gt_polyons_batch: + t = [] + for index in temp_list: + if index[0] != -1 and index[1] != -1: + t.append(index) + gt_polyons_batch.append(t) + temp_gt_strs_batch = temp_gt_strs_batch[0].tolist() for temp_list in temp_gt_strs_batch: t = "" @@ -46,7 +56,7 @@ class E2EMetric(object): gt_strs_batch.append(t) for pred, gt_polyons, gt_strs, ignore_tags in zip( - [preds], gt_polyons_batch, [gt_strs_batch], ignore_tags_batch): + [preds], [gt_polyons_batch], [gt_strs_batch], ignore_tags_batch): # prepare gt gt_info_list = [{ 'points': gt_polyon, diff --git a/ppocr/postprocess/pg_postprocess.py b/ppocr/postprocess/pg_postprocess.py index 2cc7dc24..d9c0048f 100644 --- a/ppocr/postprocess/pg_postprocess.py +++ b/ppocr/postprocess/pg_postprocess.py @@ -23,7 +23,8 @@ __dir__ = os.path.dirname(__file__) sys.path.append(__dir__) sys.path.append(os.path.join(__dir__, '..')) -from ppocr.utils.e2e_utils.extract_textpoint import get_dict, generate_pivot_list, restore_poly +from ppocr.utils.e2e_utils.extract_textpoint import * +from ppocr.utils.e2e_utils.visual import * import paddle @@ -37,6 +38,11 @@ class PGPostProcess(object): self.valid_set = valid_set self.score_thresh = score_thresh + # c++ la-nms is faster, but only support python 3.5 + self.is_python35 = False + if sys.version_info.major == 3 and sys.version_info.minor == 5: + self.is_python35 = True + def __call__(self, outs_dict, shape_list): p_score = outs_dict['f_score'] p_border = outs_dict['f_border'] @@ -52,17 +58,96 @@ class PGPostProcess(object): 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] - instance_yxs_list, seq_strs = generate_pivot_list( + is_curved = self.valid_set == "totaltext" + instance_yxs_list = generate_pivot_list( p_score, p_char, p_direction, - self.Lexicon_Table, - score_thresh=self.score_thresh) - poly_list, keep_str_list = restore_poly(instance_yxs_list, seq_strs, - p_border, ratio_w, ratio_h, - src_w, src_h, self.valid_set) + score_thresh=self.score_thresh, + is_backbone=True, + is_curved=is_curved) + p_char = paddle.to_tensor(np.expand_dims(p_char, axis=0)) + char_seq_idx_set = [] + for i in range(len(instance_yxs_list)): + gather_info_lod = paddle.to_tensor(instance_yxs_list[i]) + f_char_map = paddle.transpose(p_char, [0, 2, 3, 1]) + feature_seq = paddle.gather_nd(f_char_map, gather_info_lod) + feature_seq = np.expand_dims(feature_seq.numpy(), axis=0) + feature_len = [len(feature_seq[0])] + featyre_seq = paddle.to_tensor(feature_seq) + feature_len = np.array([feature_len]).astype(np.int64) + length = paddle.to_tensor(feature_len) + seq_pred = paddle.fluid.layers.ctc_greedy_decoder( + input=featyre_seq, blank=36, input_length=length) + seq_pred_str = seq_pred[0].numpy().tolist()[0] + seq_len = seq_pred[1].numpy()[0][0] + temp_t = [] + for c in seq_pred_str[:seq_len]: + temp_t.append(c) + char_seq_idx_set.append(temp_t) + seq_strs = [] + for char_idx_set in char_seq_idx_set: + pr_str = ''.join([self.Lexicon_Table[pos] for pos in char_idx_set]) + seq_strs.append(pr_str) + poly_list = [] + keep_str_list = [] + all_point_list = [] + all_point_pair_list = [] + for yx_center_line, keep_str in zip(instance_yxs_list, seq_strs): + if len(yx_center_line) == 1: + yx_center_line.append(yx_center_line[-1]) + + offset_expand = 1.0 + if self.valid_set == 'totaltext': + offset_expand = 1.2 + + point_pair_list = [] + for batch_id, y, x in yx_center_line: + offset = p_border[:, y, x].reshape(2, 2) + if offset_expand != 1.0: + offset_length = np.linalg.norm( + offset, axis=1, keepdims=True) + expand_length = np.clip( + offset_length * (offset_expand - 1), + a_min=0.5, + a_max=3.0) + offset_detal = offset / offset_length * expand_length + offset = offset + offset_detal + ori_yx = np.array([y, x], dtype=np.float32) + point_pair = (ori_yx + offset)[:, ::-1] * 4.0 / np.array( + [ratio_w, ratio_h]).reshape(-1, 2) + point_pair_list.append(point_pair) + + all_point_list.append([ + int(round(x * 4.0 / ratio_w)), + int(round(y * 4.0 / ratio_h)) + ]) + all_point_pair_list.append(point_pair.round().astype(np.int32) + .tolist()) + + detected_poly, pair_length_info = point_pair2poly(point_pair_list) + detected_poly = expand_poly_along_width( + detected_poly, shrink_ratio_of_width=0.2) + detected_poly[:, 0] = np.clip( + detected_poly[:, 0], a_min=0, a_max=src_w) + detected_poly[:, 1] = np.clip( + detected_poly[:, 1], a_min=0, a_max=src_h) + + if len(keep_str) < 2: + continue + + keep_str_list.append(keep_str) + if self.valid_set == 'partvgg': + middle_point = len(detected_poly) // 2 + detected_poly = detected_poly[ + [0, middle_point - 1, middle_point, -1], :] + poly_list.append(detected_poly) + elif self.valid_set == 'totaltext': + poly_list.append(detected_poly) + else: + print('--> Not supported format.') + exit(-1) data = { 'points': poly_list, 'strs': keep_str_list, diff --git a/ppocr/utils/e2e_metric/Deteval.py b/ppocr/utils/e2e_metric/Deteval.py index 37fa5c00..8033a9ff 100755 --- a/ppocr/utils/e2e_metric/Deteval.py +++ b/ppocr/utils/e2e_metric/Deteval.py @@ -35,7 +35,7 @@ def get_socre(gt_dict, pred_dict): gt = [] n = len(gt_dict) for i in range(n): - points = gt_dict[i]['points'].tolist() + points = gt_dict[i]['points'] h = len(points) text = gt_dict[i]['text'] xx = [ @@ -51,7 +51,7 @@ def get_socre(gt_dict, pred_dict): t_y.append(points[j][1]) xx[1] = np.array([t_x], dtype='int16') xx[3] = np.array([t_y], dtype='int16') - if text != "": + if text != "" and "#" not in text: xx[4] = np.array([text], dtype='U{}'.format(len(text))) xx[5] = np.array(['c'], dtype=' tr) - gt_matching_num_qualified_sigma_candidates = gt_matching_qualified_sigma_candidates[ - 0].shape[0] - gt_matching_qualified_tau_candidates = np.where( - local_tau_table[gt_id, :] > tp) - gt_matching_num_qualified_tau_candidates = gt_matching_qualified_tau_candidates[ - 0].shape[0] - - det_matching_qualified_sigma_candidates = np.where( - local_sigma_table[:, gt_matching_qualified_sigma_candidates[0]] - > tr) - det_matching_num_qualified_sigma_candidates = det_matching_qualified_sigma_candidates[ - 0].shape[0] - det_matching_qualified_tau_candidates = np.where( - local_tau_table[:, gt_matching_qualified_tau_candidates[0]] > - tp) - det_matching_num_qualified_tau_candidates = det_matching_qualified_tau_candidates[ - 0].shape[0] - - if (gt_matching_num_qualified_sigma_candidates == 1) and (gt_matching_num_qualified_tau_candidates == 1) and \ - (det_matching_num_qualified_sigma_candidates == 1) and ( - det_matching_num_qualified_tau_candidates == 1): - global_accumulative_recall = global_accumulative_recall + 1.0 - global_accumulative_precision = global_accumulative_precision + 1.0 - local_accumulative_recall = local_accumulative_recall + 1.0 - local_accumulative_precision = local_accumulative_precision + 1.0 - - gt_flag[0, gt_id] = 1 - matched_det_id = np.where(local_sigma_table[gt_id, :] > tr) - # recg start - - gt_str_cur = global_gt_str[idy][gt_id] - pred_str_cur = global_pred_str[idy][matched_det_id[0].tolist()[ - 0]] - - if pred_str_cur == gt_str_cur: - hit_str_num += 1 - else: - if pred_str_cur.lower() == gt_str_cur.lower(): - hit_str_num += 1 - # recg end - det_flag[0, matched_det_id] = 1 - return local_accumulative_recall, local_accumulative_precision, global_accumulative_recall, global_accumulative_precision, gt_flag, det_flag, hit_str_num - - def one_to_many(local_sigma_table, local_tau_table, - local_accumulative_recall, local_accumulative_precision, - global_accumulative_recall, global_accumulative_precision, - gt_flag, det_flag, idy): - hit_str_num = 0 - for gt_id in range(num_gt): - # skip the following if the groundtruth was matched - if gt_flag[0, gt_id] > 0: - continue - - non_zero_in_sigma = np.where(local_sigma_table[gt_id, :] > 0) - num_non_zero_in_sigma = non_zero_in_sigma[0].shape[0] - - if num_non_zero_in_sigma >= k: - ####search for all detections that overlaps with this groundtruth - qualified_tau_candidates = np.where((local_tau_table[ - gt_id, :] >= tp) & (det_flag[0, :] == 0)) - num_qualified_tau_candidates = qualified_tau_candidates[ - 0].shape[0] - - if num_qualified_tau_candidates == 1: - if ((local_tau_table[gt_id, qualified_tau_candidates] >= tp) - and - (local_sigma_table[gt_id, qualified_tau_candidates] >= - tr)): - # became an one-to-one case - global_accumulative_recall = global_accumulative_recall + 1.0 - global_accumulative_precision = global_accumulative_precision + 1.0 - local_accumulative_recall = local_accumulative_recall + 1.0 - local_accumulative_precision = local_accumulative_precision + 1.0 - - gt_flag[0, gt_id] = 1 - det_flag[0, qualified_tau_candidates] = 1 - # recg start - gt_str_cur = global_gt_str[idy][gt_id] - pred_str_cur = global_pred_str[idy][ - qualified_tau_candidates[0].tolist()[0]] - - if pred_str_cur == gt_str_cur: - hit_str_num += 1 - else: - if pred_str_cur.lower() == gt_str_cur.lower(): - hit_str_num += 1 - # recg end - elif (np.sum(local_sigma_table[gt_id, qualified_tau_candidates]) - >= tr): - gt_flag[0, gt_id] = 1 - det_flag[0, qualified_tau_candidates] = 1 - # recg start - - gt_str_cur = global_gt_str[idy][gt_id] - pred_str_cur = global_pred_str[idy][ - qualified_tau_candidates[0].tolist()[0]] - - if pred_str_cur == gt_str_cur: - hit_str_num += 1 - else: - if pred_str_cur.lower() == gt_str_cur.lower(): - hit_str_num += 1 - # recg end - - global_accumulative_recall = global_accumulative_recall + fsc_k - global_accumulative_precision = global_accumulative_precision + num_qualified_tau_candidates * fsc_k - - local_accumulative_recall = local_accumulative_recall + fsc_k - local_accumulative_precision = local_accumulative_precision + num_qualified_tau_candidates * fsc_k - - return local_accumulative_recall, local_accumulative_precision, global_accumulative_recall, global_accumulative_precision, gt_flag, det_flag, hit_str_num - - def many_to_one(local_sigma_table, local_tau_table, - local_accumulative_recall, local_accumulative_precision, - global_accumulative_recall, global_accumulative_precision, - gt_flag, det_flag, idy): - hit_str_num = 0 - for det_id in range(num_det): - # skip the following if the detection was matched - if det_flag[0, det_id] > 0: - continue - - non_zero_in_tau = np.where(local_tau_table[:, det_id] > 0) - num_non_zero_in_tau = non_zero_in_tau[0].shape[0] - - if num_non_zero_in_tau >= k: - ####search for all detections that overlaps with this groundtruth - qualified_sigma_candidates = np.where(( - local_sigma_table[:, det_id] >= tp) & (gt_flag[0, :] == 0)) - num_qualified_sigma_candidates = qualified_sigma_candidates[ - 0].shape[0] - - if num_qualified_sigma_candidates == 1: - if ((local_tau_table[qualified_sigma_candidates, det_id] >= - tp) and - (local_sigma_table[qualified_sigma_candidates, det_id] - >= tr)): - # became an one-to-one case - global_accumulative_recall = global_accumulative_recall + 1.0 - global_accumulative_precision = global_accumulative_precision + 1.0 - local_accumulative_recall = local_accumulative_recall + 1.0 - local_accumulative_precision = local_accumulative_precision + 1.0 - - gt_flag[0, qualified_sigma_candidates] = 1 - det_flag[0, det_id] = 1 - # recg start - pred_str_cur = global_pred_str[idy][det_id] - gt_len = len(qualified_sigma_candidates[0]) - for idx in range(gt_len): - ele_gt_id = qualified_sigma_candidates[0].tolist()[ - idx] - if ele_gt_id not in global_gt_str[idy]: - continue - gt_str_cur = global_gt_str[idy][ele_gt_id] - if pred_str_cur == gt_str_cur: - hit_str_num += 1 - break - else: - if pred_str_cur.lower() == gt_str_cur.lower(): - hit_str_num += 1 - break - # recg end - elif (np.sum(local_tau_table[qualified_sigma_candidates, - det_id]) >= tp): - det_flag[0, det_id] = 1 - gt_flag[0, qualified_sigma_candidates] = 1 - # recg start - - pred_str_cur = global_pred_str[idy][det_id] - gt_len = len(qualified_sigma_candidates[0]) - for idx in range(gt_len): - ele_gt_id = qualified_sigma_candidates[0].tolist()[idx] - if ele_gt_id not in global_gt_str[idy]: - continue - gt_str_cur = global_gt_str[idy][ele_gt_id] - if pred_str_cur == gt_str_cur: - hit_str_num += 1 - break - else: - if pred_str_cur.lower() == gt_str_cur.lower(): - hit_str_num += 1 - break - # recg end - - global_accumulative_recall = global_accumulative_recall + num_qualified_sigma_candidates * fsc_k - global_accumulative_precision = global_accumulative_precision + fsc_k - - local_accumulative_recall = local_accumulative_recall + num_qualified_sigma_candidates * fsc_k - local_accumulative_precision = local_accumulative_precision + fsc_k - return local_accumulative_recall, local_accumulative_precision, global_accumulative_recall, global_accumulative_precision, gt_flag, det_flag, hit_str_num + global_sigma = local_sigma_table + global_tau = local_tau_table + global_pred_str = local_pred_str + global_gt_str = local_gt_str single_data = {} - for idx in range(len(global_sigma)): - local_sigma_table = global_sigma[idx] - local_tau_table = global_tau[idx] - - num_gt = local_sigma_table.shape[0] - num_det = local_sigma_table.shape[1] - - total_num_gt = total_num_gt + num_gt - total_num_det = total_num_det + num_det - - local_accumulative_recall = 0 - local_accumulative_precision = 0 - gt_flag = np.zeros((1, num_gt)) - det_flag = np.zeros((1, num_det)) - - #######first check for one-to-one case########## - local_accumulative_recall, local_accumulative_precision, global_accumulative_recall, global_accumulative_precision, \ - gt_flag, det_flag, hit_str_num = one_to_one(local_sigma_table, local_tau_table, - local_accumulative_recall, local_accumulative_precision, - global_accumulative_recall, global_accumulative_precision, - gt_flag, det_flag, idx) - - hit_str_count += hit_str_num - #######then check for one-to-many case########## - local_accumulative_recall, local_accumulative_precision, global_accumulative_recall, global_accumulative_precision, \ - gt_flag, det_flag, hit_str_num = one_to_many(local_sigma_table, local_tau_table, - local_accumulative_recall, local_accumulative_precision, - global_accumulative_recall, global_accumulative_precision, - gt_flag, det_flag, idx) - hit_str_count += hit_str_num - #######then check for many-to-one case########## - local_accumulative_recall, local_accumulative_precision, global_accumulative_recall, global_accumulative_precision, \ - gt_flag, det_flag, hit_str_num = many_to_one(local_sigma_table, local_tau_table, - local_accumulative_recall, local_accumulative_precision, - global_accumulative_recall, global_accumulative_precision, - gt_flag, det_flag, idx) - - hit_str_count += hit_str_num - - # fid = open(fid_path, 'a+') - try: - local_precision = local_accumulative_precision / num_det - except ZeroDivisionError: - local_precision = 0 - - try: - local_recall = local_accumulative_recall / num_gt - except ZeroDivisionError: - local_recall = 0 - - try: - local_f_score = 2 * local_precision * local_recall / ( - local_precision + local_recall) - except ZeroDivisionError: - local_f_score = 0 - single_data['sigma'] = global_sigma single_data['global_tau'] = global_tau single_data['global_pred_str'] = global_pred_str single_data['global_gt_str'] = global_gt_str - single_data["recall"] = local_recall - single_data['precision'] = local_precision - single_data['f_score'] = local_f_score return single_data @@ -435,10 +163,10 @@ def combine_results(all_data): global_pred_str = [] global_gt_str = [] for data in all_data: - global_sigma.append(data['sigma'][0]) - global_tau.append(data['global_tau'][0]) - global_pred_str.append(data['global_pred_str'][0]) - global_gt_str.append(data['global_gt_str'][0]) + global_sigma.append(data['sigma']) + global_tau.append(data['global_tau']) + global_pred_str.append(data['global_pred_str']) + global_gt_str.append(data['global_gt_str']) global_accumulative_recall = 0 global_accumulative_precision = 0 @@ -676,6 +404,8 @@ def combine_results(all_data): local_accumulative_recall, local_accumulative_precision, global_accumulative_recall, global_accumulative_precision, gt_flag, det_flag, idx) + hit_str_count += hit_str_num + try: recall = global_accumulative_recall / total_num_gt except ZeroDivisionError: diff --git a/ppocr/utils/e2e_utils/extract_textpoint.py b/ppocr/utils/e2e_utils/extract_textpoint.py index d64f1e83..975ca161 100644 --- a/ppocr/utils/e2e_utils/extract_textpoint.py +++ b/ppocr/utils/e2e_utils/extract_textpoint.py @@ -17,9 +17,11 @@ from __future__ import division from __future__ import print_function import cv2 +import math + import numpy as np from itertools import groupby -from cv2.ximgproc import thinning as thin +from skimage.morphology._skeletonize import thin def get_dict(character_dict_path): @@ -33,39 +35,87 @@ def get_dict(character_dict_path): return dict_character -def instance_ctc_greedy_decoder(gather_info, logits_map, pts_num=4): +def softmax(logits): + """ + logits: N x d + """ + max_value = np.max(logits, axis=1, keepdims=True) + exp = np.exp(logits - max_value) + exp_sum = np.sum(exp, axis=1, keepdims=True) + dist = exp / exp_sum + return dist + + +def get_keep_pos_idxs(labels, remove_blank=None): + """ + Remove duplicate and get pos idxs of keep items. + The value of keep_blank should be [None, 95]. + """ + duplicate_len_list = [] + keep_pos_idx_list = [] + keep_char_idx_list = [] + for k, v_ in groupby(labels): + current_len = len(list(v_)) + if k != remove_blank: + current_idx = int(sum(duplicate_len_list) + current_len // 2) + keep_pos_idx_list.append(current_idx) + keep_char_idx_list.append(k) + duplicate_len_list.append(current_len) + return keep_char_idx_list, keep_pos_idx_list + + +def remove_blank(labels, blank=0): + new_labels = [x for x in labels if x != blank] + return new_labels + + +def insert_blank(labels, blank=0): + new_labels = [blank] + for l in labels: + new_labels += [l, blank] + return new_labels + + +def ctc_greedy_decoder(probs_seq, blank=95, keep_blank_in_idxs=True): + """ + CTC greedy (best path) decoder. + """ + raw_str = np.argmax(np.array(probs_seq), axis=1) + remove_blank_in_pos = None if keep_blank_in_idxs else blank + dedup_str, keep_idx_list = get_keep_pos_idxs( + raw_str, remove_blank=remove_blank_in_pos) + dst_str = remove_blank(dedup_str, blank=blank) + return dst_str, keep_idx_list + + +def instance_ctc_greedy_decoder(gather_info, + logits_map, + keep_blank_in_idxs=True): + """ + gather_info: [[x, y], [x, y] ...] + logits_map: H x W X (n_chars + 1) + """ _, _, C = logits_map.shape ys, xs = zip(*gather_info) - logits_seq = logits_map[list(ys), list(xs)] - probs_seq = logits_seq - labels = np.argmax(probs_seq, axis=1) - dst_str = [k for k, v_ in groupby(labels) if k != C - 1] - detal = len(gather_info) // (pts_num - 1) - keep_idx_list = [0] + [detal * (i + 1) for i in range(pts_num - 2)] + [-1] + logits_seq = logits_map[list(ys), list(xs)] # n x 96 + probs_seq = softmax(logits_seq) + dst_str, keep_idx_list = ctc_greedy_decoder( + probs_seq, blank=C - 1, keep_blank_in_idxs=keep_blank_in_idxs) keep_gather_list = [gather_info[idx] for idx in keep_idx_list] return dst_str, keep_gather_list -def ctc_decoder_for_image(gather_info_list, - logits_map, - Lexicon_Table, - pts_num=6): +def ctc_decoder_for_image(gather_info_list, logits_map, + keep_blank_in_idxs=True): """ CTC decoder using multiple processes. """ - decoder_str = [] - decoder_xys = [] + decoder_results = [] for gather_info in gather_info_list: - if len(gather_info) < pts_num: - continue - dst_str, xys_list = instance_ctc_greedy_decoder( - gather_info, logits_map, pts_num=pts_num) - dst_str_readable = ''.join([Lexicon_Table[idx] for idx in dst_str]) - if len(dst_str_readable) < 2: - continue - decoder_str.append(dst_str_readable) - decoder_xys.append(xys_list) - return decoder_str, decoder_xys + res = instance_ctc_greedy_decoder( + gather_info, logits_map, keep_blank_in_idxs=keep_blank_in_idxs) + decoder_results.append(res) + return decoder_results def sort_with_direction(pos_list, f_direction): @@ -107,6 +157,58 @@ def sort_with_direction(pos_list, f_direction): return sorted_point, np.array(sorted_direction) +def add_id(pos_list, image_id=0): + """ + Add id for gather feature, for inference. + """ + new_list = [] + for item in pos_list: + new_list.append((image_id, item[0], item[1])) + return new_list + + +def sort_and_expand_with_direction(pos_list, f_direction): + """ + f_direction: h x w x 2 + pos_list: [[y, x], [y, x], [y, x] ...] + """ + 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, :] + right_dirction = point_direction[point_num - sub_direction_len:, :] + + left_average_direction = -np.mean(left_direction, axis=0, keepdims=True) + left_average_len = np.linalg.norm(left_average_direction) + left_start = np.array(sorted_list[0]) + left_step = left_average_direction / (left_average_len + 1e-6) + + right_average_direction = np.mean(right_dirction, axis=0, keepdims=True) + right_average_len = np.linalg.norm(right_average_direction) + right_step = right_average_direction / (right_average_len + 1e-6) + right_start = np.array(sorted_list[-1]) + + append_num = max( + int((left_average_len + right_average_len) / 2.0 * 0.15), 1) + left_list = [] + right_list = [] + for i in range(append_num): + ly, lx = np.round(left_start + left_step * (i + 1)).flatten().astype( + 'int32').tolist() + if ly < h and lx < w and (ly, lx) not in left_list: + left_list.append((ly, lx)) + ry, rx = np.round(right_start + right_step * (i + 1)).flatten().astype( + 'int32').tolist() + if ry < h and rx < w and (ry, rx) not in right_list: + right_list.append((ry, rx)) + + all_list = left_list[::-1] + sorted_list + right_list + return all_list + + def sort_and_expand_with_direction_v2(pos_list, f_direction, binary_tcl_map): """ f_direction: h x w x 2 @@ -116,6 +218,7 @@ 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, :] @@ -159,108 +262,258 @@ def sort_and_expand_with_direction_v2(pos_list, f_direction, binary_tcl_map): return all_list -def point_pair2poly(point_pair_list): - """ - Transfer vertical point_pairs into poly point in clockwise. - """ - point_num = len(point_pair_list) * 2 - point_list = [0] * point_num - for idx, point_pair in enumerate(point_pair_list): - point_list[idx] = point_pair[0] - point_list[point_num - 1 - idx] = point_pair[1] - return np.array(point_list).reshape(-1, 2) - - -def shrink_quad_along_width(quad, begin_width_ratio=0., end_width_ratio=1.): - ratio_pair = np.array( - [[begin_width_ratio], [end_width_ratio]], dtype=np.float32) - p0_1 = quad[0] + (quad[1] - quad[0]) * ratio_pair - p3_2 = quad[3] + (quad[2] - quad[3]) * ratio_pair - return np.array([p0_1[0], p0_1[1], p3_2[1], p3_2[0]]) - - -def expand_poly_along_width(poly, shrink_ratio_of_width=0.3): - """ - expand poly along width. - """ - point_num = poly.shape[0] - left_quad = np.array( - [poly[0], poly[1], poly[-2], poly[-1]], dtype=np.float32) - left_ratio = -shrink_ratio_of_width * np.linalg.norm(left_quad[0] - left_quad[3]) / \ - (np.linalg.norm(left_quad[0] - left_quad[1]) + 1e-6) - left_quad_expand = shrink_quad_along_width(left_quad, left_ratio, 1.0) - right_quad = np.array( - [ - poly[point_num // 2 - 2], poly[point_num // 2 - 1], - poly[point_num // 2], poly[point_num // 2 + 1] - ], - dtype=np.float32) - right_ratio = 1.0 + shrink_ratio_of_width * np.linalg.norm(right_quad[0] - right_quad[3]) / \ - (np.linalg.norm(right_quad[0] - right_quad[1]) + 1e-6) - right_quad_expand = shrink_quad_along_width(right_quad, 0.0, right_ratio) - poly[0] = left_quad_expand[0] - poly[-1] = left_quad_expand[-1] - poly[point_num // 2 - 1] = right_quad_expand[1] - poly[point_num // 2] = right_quad_expand[2] - return poly - - -def restore_poly(instance_yxs_list, seq_strs, p_border, ratio_w, ratio_h, src_w, - src_h, valid_set): - poly_list = [] - keep_str_list = [] - for yx_center_line, keep_str in zip(instance_yxs_list, seq_strs): - if len(keep_str) < 2: - print('--> too short, {}'.format(keep_str)) - continue - - offset_expand = 1.0 - if valid_set == 'totaltext': - offset_expand = 1.2 - - point_pair_list = [] - for y, x in yx_center_line: - offset = p_border[:, y, x].reshape(2, 2) * offset_expand - ori_yx = np.array([y, x], dtype=np.float32) - point_pair = (ori_yx + offset)[:, ::-1] * 4.0 / np.array( - [ratio_w, ratio_h]).reshape(-1, 2) - point_pair_list.append(point_pair) - - detected_poly = point_pair2poly(point_pair_list) - detected_poly = expand_poly_along_width( - detected_poly, shrink_ratio_of_width=0.2) - detected_poly[:, 0] = np.clip(detected_poly[:, 0], a_min=0, a_max=src_w) - detected_poly[:, 1] = np.clip(detected_poly[:, 1], a_min=0, a_max=src_h) - - keep_str_list.append(keep_str) - if valid_set == 'partvgg': - middle_point = len(detected_poly) // 2 - detected_poly = detected_poly[ - [0, middle_point - 1, middle_point, -1], :] - poly_list.append(detected_poly) - elif valid_set == 'totaltext': - poly_list.append(detected_poly) +def generate_pivot_list_curved(p_score, + p_char_maps, + f_direction, + score_thresh=0.5, + is_expand=True, + is_backbone=False, + image_id=0): + """ + return center point and end point of TCL instance; filter with the char maps; + """ + p_score = p_score[0] + f_direction = f_direction.transpose(1, 2, 0) + p_tcl_map = (p_score > score_thresh) * 1.0 + skeleton_map = thin(p_tcl_map) + 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 = [] + instance_center_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 + + if is_expand: + pos_list_sorted = sort_and_expand_with_direction_v2( + pos_list, f_direction, p_tcl_map) + else: + 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) + for decoded_str, keep_yxs_list in decode_res: + if is_backbone: + keep_yxs_list_with_id = add_id(keep_yxs_list, image_id=image_id) + instance_center_pos_yxs.append(keep_yxs_list_with_id) + else: + end_points_yxs.extend((keep_yxs_list[0], keep_yxs_list[-1])) + center_pos_yxs.extend(keep_yxs_list) + + if is_backbone: + return instance_center_pos_yxs + else: + return center_pos_yxs, end_points_yxs + + +def generate_pivot_list_horizontal(p_score, + p_char_maps, + f_direction, + score_thresh=0.5, + is_backbone=False, + image_id=0): + """ + return center point and end point of TCL instance; filter with the char maps; + """ + p_score = p_score[0] + f_direction = f_direction.transpose(1, 2, 0) + p_tcl_map_bi = (p_score > score_thresh) * 1.0 + instance_count, instance_label_map = cv2.connectedComponents( + p_tcl_map_bi.astype(np.uint8), connectivity=8) + + # get TCL Instance + all_pos_yxs = [] + center_pos_yxs = [] + end_points_yxs = [] + instance_center_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) < 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 + is_h_angle = abs(np.sum( + main_direction * reference_directin)) < math.cos(math.pi / 180 * + 70) + + point_yxs = np.array(pos_list) + max_y, max_x = np.max(point_yxs, axis=0) + min_y, min_x = np.min(point_yxs, axis=0) + is_h_len = (max_y - min_y) < 1.5 * (max_x - min_x) + + pos_list_final = [] + if is_h_len: + xs = np.unique(xs) + for x in xs: + ys = instance_label_map[:, x].copy().reshape((-1, )) + y = int(np.where(ys == instance_id)[0].mean()) + pos_list_final.append((y, x)) + else: + ys = np.unique(ys) + for y in ys: + xs = instance_label_map[y, :].copy().reshape((-1, )) + x = int(np.where(xs == instance_id)[0].mean()) + pos_list_final.append((y, x)) + + pos_list_sorted, _ = sort_with_direction(pos_list_final, + 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) + for decoded_str, keep_yxs_list in decode_res: + if is_backbone: + keep_yxs_list_with_id = add_id(keep_yxs_list, image_id=image_id) + instance_center_pos_yxs.append(keep_yxs_list_with_id) else: - print('--> Not supported format.') - exit(-1) - return poly_list, keep_str_list + end_points_yxs.extend((keep_yxs_list[0], keep_yxs_list[-1])) + center_pos_yxs.extend(keep_yxs_list) + + if is_backbone: + return instance_center_pos_yxs + else: + return center_pos_yxs, end_points_yxs def generate_pivot_list(p_score, p_char_maps, f_direction, - Lexicon_Table, - score_thresh=0.5): + score_thresh=0.5, + is_backbone=False, + is_curved=True, + image_id=0): + """ + Warp all the function together. + """ + if is_curved: + return generate_pivot_list_curved( + p_score, + p_char_maps, + f_direction, + score_thresh=score_thresh, + is_expand=True, + is_backbone=is_backbone, + image_id=image_id) + else: + return generate_pivot_list_horizontal( + p_score, + p_char_maps, + f_direction, + score_thresh=score_thresh, + is_backbone=is_backbone, + image_id=image_id) + + +# for refine module +def extract_main_direction(pos_list, f_direction): + """ + f_direction: h x w x 2 + pos_list: [[y, x], [y, x], [y, x] ...] + """ + pos_list = np.array(pos_list) + point_direction = f_direction[pos_list[:, 0], pos_list[:, 1]] + point_direction = point_direction[:, ::-1] # x, y -> y, x + average_direction = np.mean(point_direction, axis=0, keepdims=True) + average_direction = average_direction / ( + np.linalg.norm(average_direction) + 1e-6) + return average_direction + + +def sort_by_direction_with_image_id_deprecated(pos_list, f_direction): + """ + f_direction: h x w x 2 + pos_list: [[id, y, x], [id, y, x], [id, y, x] ...] + """ + pos_list_full = np.array(pos_list).reshape(-1, 3) + pos_list = pos_list_full[:, 1:] + point_direction = f_direction[pos_list[:, 0], pos_list[:, 1]] # x, y + point_direction = point_direction[:, ::-1] # x, y -> y, x + average_direction = np.mean(point_direction, axis=0, keepdims=True) + pos_proj_leng = np.sum(pos_list * average_direction, axis=1) + sorted_list = pos_list_full[np.argsort(pos_proj_leng)].tolist() + return sorted_list + + +def sort_by_direction_with_image_id(pos_list, f_direction): + """ + f_direction: h x w x 2 + pos_list: [[y, x], [y, x], [y, x] ...] + """ + + def sort_part_with_direction(pos_list_full, point_direction): + pos_list_full = np.array(pos_list_full).reshape(-1, 3) + pos_list = pos_list_full[:, 1:] + point_direction = np.array(point_direction).reshape(-1, 2) + average_direction = np.mean(point_direction, axis=0, keepdims=True) + pos_proj_leng = np.sum(pos_list * average_direction, axis=1) + sorted_list = pos_list_full[np.argsort(pos_proj_leng)].tolist() + sorted_direction = point_direction[np.argsort(pos_proj_leng)].tolist() + return sorted_list, sorted_direction + + pos_list = np.array(pos_list).reshape(-1, 3) + point_direction = f_direction[pos_list[:, 1], pos_list[:, 2]] # x, y + point_direction = point_direction[:, ::-1] # x, y -> y, x + sorted_point, sorted_direction = sort_part_with_direction(pos_list, + point_direction) + + point_num = len(sorted_point) + if point_num >= 16: + middle_num = point_num // 2 + first_part_point = sorted_point[:middle_num] + first_point_direction = sorted_direction[:middle_num] + sorted_fist_part_point, sorted_fist_part_direction = sort_part_with_direction( + first_part_point, first_point_direction) + + last_part_point = sorted_point[middle_num:] + last_point_direction = sorted_direction[middle_num:] + sorted_last_part_point, sorted_last_part_direction = sort_part_with_direction( + last_part_point, last_point_direction) + sorted_point = sorted_fist_part_point + sorted_last_part_point + sorted_direction = sorted_fist_part_direction + sorted_last_part_direction + + return sorted_point + + +def generate_pivot_list_tt_inference(p_score, + p_char_maps, + f_direction, + score_thresh=0.5, + is_backbone=False, + is_curved=True, + image_id=0): """ return center point and end point of TCL instance; filter with the char maps; """ p_score = p_score[0] f_direction = f_direction.transpose(1, 2, 0) - ret, p_tcl_map = cv2.threshold(p_score, score_thresh, 255, - cv2.THRESH_BINARY) - skeleton_map = thin(p_tcl_map.astype('uint8')) + p_tcl_map = (p_score > score_thresh) * 1.0 + skeleton_map = thin(p_tcl_map) instance_count, instance_label_map = cv2.connectedComponents( - skeleton_map, connectivity=8) + skeleton_map.astype(np.uint8), connectivity=8) # get TCL Instance all_pos_yxs = [] @@ -269,15 +522,11 @@ def generate_pivot_list(p_score, 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( pos_list, f_direction, p_tcl_map) - all_pos_yxs.append(pos_list_sorted) - - p_char_maps = p_char_maps.transpose([1, 2, 0]) - decoded_str, keep_yxs_list = ctc_decoder_for_image( - all_pos_yxs, logits_map=p_char_maps, Lexicon_Table=Lexicon_Table) - return keep_yxs_list, decoded_str + pos_list_sorted_with_id = add_id(pos_list_sorted, image_id=image_id) + all_pos_yxs.append(pos_list_sorted_with_id) + return all_pos_yxs diff --git a/tools/infer/predict_e2e.py b/tools/infer/predict_e2e.py index 406e1bf3..a5c57914 100755 --- a/tools/infer/predict_e2e.py +++ b/tools/infer/predict_e2e.py @@ -151,7 +151,7 @@ if __name__ == "__main__": 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_{}_pgnet".format(img_name_pure)) + "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: diff --git a/train_data/total_text/train/poly/2.txt b/train_data/total_text/train/poly/2.txt deleted file mode 100644 index 961d9680..00000000 --- a/train_data/total_text/train/poly/2.txt +++ /dev/null @@ -1,2 +0,0 @@ -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 deleted file mode 100644 index f3bc7a06e911ef87c0831e779d20e44b6d2bbea5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 41876 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 -- GitLab