├── .gitignore ├── LICENSE ├── README.md ├── direction_visualize.py ├── eval.py ├── format_transfer.py ├── format_transfer_art_dilated.py ├── loss_visualize.py └── utils ├── __init__.py └── iou.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | data/ 107 | tmp/ 108 | .DS_Store -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 tkianai 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ICDAR2019-tools 2 | 3 | This repo develops the tools for ICDAR2019 competitions. You can find details [here](http://rrc.cvc.uab.es/?ch=12). 4 | 5 | > Competitions 6 | 7 | - [x] LSVT 8 | 9 | [Large-scale Street View Text with Partial Labeling](http://rrc.cvc.uab.es/?ch=16) 10 | 11 | - [x] ReCTS 12 | 13 | [Reading Chinese Text on Signboard](http://rrc.cvc.uab.es/?ch=12) 14 | 15 | - [x] ArT 16 | 17 | [Arbitrary-Shaped Text](http://rrc.cvc.uab.es/?ch=14) 18 | 19 | ## Usage 20 | 21 | - Transfer the detection format from coco-style to submission style 22 | 23 | ```sh 24 | # for rects 25 | python format_transfer.py --dt-file task3-results/results.pkl.json --mode rects --save data/rects_task3.txt 26 | ``` 27 | 28 | - [x] rects format test passed 29 | - [x] lsvt format test passed 30 | 31 | - Evaluating performace of the model 32 | 33 | ```sh 34 | # for lsvt 35 | python eval.py --gt-file data/lsvt_val_v2.json --dt-file data/lsvt_val_v2_det.json 36 | ``` 37 | 38 | - [x] Too Slow for evaluation 39 | -------------------------------------------------------------------------------- /direction_visualize.py: -------------------------------------------------------------------------------- 1 | 2 | import json 3 | import cv2 4 | import numpy as np 5 | import argparse 6 | 7 | parser = argparse.ArgumentParser(description='visulize points extending direction.') 8 | parser.add_argument('--json') 9 | parser.add_argument('--key') 10 | args = parser.parse_args() 11 | data = json.load(open(args.json)) 12 | sample = data[args.key] 13 | for itm in sample[2:3]: 14 | image = np.zeros((1000, 1000, 3), dtype=np.uint8) 15 | for point in itm['points']: 16 | image = cv2.circle(image, (int(point[0]), int(point[1])), 3, (0, 255, 0), 1) 17 | cv2.imshow("direction", image) 18 | cv2.waitKey() 19 | 20 | -------------------------------------------------------------------------------- /eval.py: -------------------------------------------------------------------------------- 1 | """This is the eval tools for ICDAR2019 competitions 2 | ==== 3 | Features: 4 | > mAP evaluation by cocoAPI 5 | > F score evaluation 6 | ==== 7 | Author: 8 | > tkianai 9 | """ 10 | 11 | import os 12 | import json 13 | import argparse 14 | import cv2 15 | import numpy as np 16 | import matplotlib.pyplot as plt 17 | from tqdm import tqdm 18 | from pycocotools.coco import COCO 19 | from pycocotools.cocoeval import COCOeval 20 | import pycocotools.mask as maskUtils 21 | from imantics import Mask as maskToPolygon 22 | from utils.iou import compute_polygons_iou 23 | 24 | class IcdarEval(object): 25 | def __init__(self, dt_file, gt_file=None, iou_threshold=0.5): 26 | self.gt_file = gt_file 27 | self.dt_file = dt_file 28 | self.iou_threshold = iou_threshold 29 | self.dt_anns = json.load(open(dt_file)) 30 | if gt_file is None: 31 | self.gt_anns = None 32 | else: 33 | self.gt_anns = json.load(open(gt_file)) 34 | 35 | self.Precision = None 36 | self.Recall = None 37 | 38 | def calculate_F_score(self, Precision, Recall): 39 | eps = 1e-7 40 | F_score = 2.0 * Precision * Recall / (Precision + Recall + eps) 41 | return F_score 42 | 43 | def eval_map(self, mode='segm'): 44 | """evaluate mean average precision 45 | 46 | Keyword Arguments: 47 | mode {str} -- could be choose from ['bbox' | 'segm'] (default: {'segm'}) 48 | """ 49 | if mode not in ['bbox', 'segm']: 50 | raise NotImplementedError("Mode [{}] doesn't been implemented, choose from [bbox, segm]!".format(mode)) 51 | 52 | # eval map 53 | Gt = COCO(self.gt_file) 54 | Dt = Gt.loadRes(self.dt_file) 55 | 56 | evalObj = COCOeval(Gt, Dt, mode) 57 | imgIds = sorted(Gt.getImgIds()) 58 | evalObj.params.imgIds = imgIds 59 | evalObj.evaluate() 60 | evalObj.accumulate() 61 | evalObj.summarize() 62 | 63 | def save_pr_curve(self, save_name='./data/P_R_curve.png'): 64 | if self.Precision is None or self.Recall is None: 65 | return 66 | 67 | # save the P-R curve 68 | save_dir = os.path.dirname(save_name) 69 | if not os.path.exists(save_dir): 70 | os.makedirs(save_dir) 71 | 72 | plt.clf() 73 | plt.plot(self.Recall, self.Precision) 74 | plt.xlim(0, 1) 75 | plt.ylim(0, 1) 76 | plt.title('Precision-Recall Curve') 77 | plt.grid() 78 | plt.xlabel('Recall') 79 | plt.ylabel('Precision') 80 | plt.savefig(save_name, dpi=400) 81 | print('Precision-recall curve has been written to {}'.format(save_name)) 82 | 83 | def eval_F_by_coco(self, threshold=None, mode='segm'): 84 | iou_threshold = self.iou_threshold if threshold is None else threshold 85 | assert iou_threshold >=0.0 and iou_threshold <= 1.0, "The IOU threshold [{}] is illegal!".format(iou_threshold) 86 | if mode not in ['bbox', 'segm']: 87 | raise NotImplementedError("Mode [{}] doesn't been implemented, choose from [bbox, segm]!".format(mode)) 88 | 89 | # eval map 90 | Gt = COCO(self.gt_file) 91 | Dt = Gt.loadRes(self.dt_file) 92 | 93 | evalObj = COCOeval(Gt, Dt, mode) 94 | imgIds = sorted(Gt.getImgIds()) 95 | evalObj.params.imgIds = imgIds 96 | evalObj.params.iouThrs = [iou_threshold] 97 | evalObj.params.areaRng = [[0, 10000000000.0]] 98 | evalObj.params.maxDets = [100] 99 | 100 | evalObj.evaluate() 101 | evalObj.accumulate() 102 | 103 | Precision = evalObj.eval['precision'][0, :, 0, 0, 0] 104 | Recall = evalObj.params.recThrs 105 | Scores = evalObj.eval['scores'][0, :, 0, 0, 0] 106 | 107 | F_score = self.calculate_F_score(Precision, Recall) 108 | 109 | # calculate highest F score 110 | idx = np.argmax(F_score) 111 | results = dict( 112 | F_score=F_score[idx], 113 | Precision=Precision[idx], 114 | Recall=Recall[idx], 115 | score=Scores[idx], 116 | ) 117 | 118 | # summarize 119 | print('---------------------- F1 ---------------------- ') 120 | print('Maximum F-score: %f' % results['F_score']) 121 | print(' |-- Precision: %f' % results['Precision']) 122 | print(' |-- Recall : %f' % results['Recall']) 123 | print(' |-- Score : %f' % results['score']) 124 | print('------------------------------------------------ ') 125 | 126 | self.Precision = Precision 127 | self.Recall = Recall 128 | 129 | def eval_F(self, threshold=None): 130 | iou_threshold = self.iou_threshold if threshold is None else threshold 131 | assert iou_threshold >=0.0 and iou_threshold <= 1.0, "The IOU threshold [{}] is illegal!".format(iou_threshold) 132 | assert self.gt_anns is not None, "GroundTruth must be needed!" 133 | 134 | gt_polygons_number = 0 135 | gt_polygons_set = {} 136 | 137 | gt_annotations = self.gt_anns['annotations'] 138 | gt_imgIds = set() 139 | for itm in gt_annotations: 140 | gt_imgIds.add(itm['image_id']) 141 | 142 | for imgId in gt_imgIds: 143 | polygons = [] 144 | for itm in gt_annotations: 145 | if itm['image_id'] == imgId: 146 | # polygons 147 | gt_segm = np.array(itm['segmentation']).ravel().tolist() 148 | polygons.append(gt_segm) 149 | 150 | gt_polygons_number += len(polygons) 151 | gt_polygons_set[imgId] = polygons 152 | 153 | dt_gt_match_all = [] 154 | dt_scores_all = [] 155 | 156 | dt_imgIds = set() 157 | dt_annotations = self.dt_anns 158 | for itm in dt_annotations: 159 | dt_imgIds.add(itm['image_id']) 160 | 161 | for imgId in tqdm(dt_imgIds): 162 | if imgId not in gt_polygons_set: 163 | print("Image ID [{}] not found in GroundTruth file, this will be ignored!".format(imgId)) 164 | continue 165 | 166 | gt_polygons = gt_polygons_set[imgId] 167 | dt_polygons = [] 168 | dt_scores = [] 169 | 170 | for itm in dt_annotations: 171 | if itm['image_id'] == imgId: 172 | # mask 173 | _mask = maskUtils.decode(itm['segmentation']).astype(np.bool) 174 | polygons = maskToPolygon(_mask).polygons() 175 | roi_areas = [cv2.contourArea(points) for points in polygons.points] 176 | idx = roi_areas.index(max(roi_areas)) 177 | dt_polygons.append(polygons.points[idx].tolist()) 178 | dt_scores.append(itm['score']) 179 | 180 | dt_gt_match = [] 181 | # TODO: methods of match should be optimizied according to LSVT requirements 182 | for dt_polygon in dt_polygons: 183 | match_flag = False 184 | for gt_polygon in gt_polygons: 185 | if compute_polygons_iou(dt_polygon, gt_polygon) >= iou_threshold: 186 | match_flag = True 187 | break 188 | dt_gt_match.append(match_flag) 189 | dt_gt_match_all.extend(dt_gt_match) 190 | dt_scores_all.extend(dt_scores) 191 | 192 | assert len(dt_gt_match_all) == len(dt_scores_all), "each polygon should have it's score!" 193 | 194 | # calculate precision, recall and F score under different score threshold 195 | dt_gt_match_all = np.array(dt_gt_match_all, dtype=np.bool).astype(np.int) 196 | dt_scores_all = np.array(dt_scores_all) 197 | 198 | # sort according to score 199 | sort_idx = np.argsort(dt_scores_all)[::-1] 200 | dt_gt_match_all = dt_gt_match_all[sort_idx] 201 | dt_scores_all = dt_scores_all[sort_idx] 202 | 203 | number_positive = np.cumsum(dt_gt_match_all) 204 | number_detected = np.arange(1, len(dt_gt_match_all) + 1) 205 | Precision = number_positive.astype(np.float) / number_detected.astype(np.float) 206 | Recall = number_positive.astype(np.float) / float(gt_polygons_number) 207 | F_score = self.calculate_F_score(Precision, Recall) 208 | 209 | # calculate highest F score 210 | idx = np.argmax(F_score) 211 | results = dict( 212 | F_score=F_score[idx], 213 | Precision=Precision[idx], 214 | Recall=Recall[idx], 215 | score=dt_scores_all[idx], 216 | ) 217 | 218 | # summarize 219 | print('---------------------- F1 ---------------------- ') 220 | print('Maximum F-score: %f' % results['F_score']) 221 | print(' |-- Precision: %f' % results['Precision']) 222 | print(' |-- Recall : %f' % results['Recall']) 223 | print(' |-- Score : %f' % results['score']) 224 | print('------------------------------------------------ ') 225 | 226 | self.Precision = Precision 227 | self.Recall = Recall 228 | 229 | if __name__ == "__main__": 230 | parser = argparse.ArgumentParser(description='mAP evaluation on ICDAR2019') 231 | parser.add_argument('--gt-file', default='data/gt.json', type=str, help='annotation | groundtruth file.') 232 | parser.add_argument('--dt-file', default='data/dt.json', type=str, help='detection results of coco annotation style.') 233 | args = parser.parse_args() 234 | # judge file existence 235 | if not os.path.exists(args.gt_file): 236 | print("File Not Found Error: {}".format(args.gt_file)) 237 | exit(404) 238 | if not os.path.exists(args.dt_file): 239 | print("File Not Found Error: {}".format(args.dt_file)) 240 | exit(404) 241 | eval_icdar = IcdarEval(args.dt_file, args.gt_file) 242 | eval_icdar.eval_map(mode='bbox') 243 | eval_icdar.eval_map(mode='segm') 244 | eval_icdar.eval_F_by_coco(threshold=0.5, mode='segm') 245 | eval_icdar.save_pr_curve() 246 | # eval_icdar.eval_F() 247 | -------------------------------------------------------------------------------- /format_transfer.py: -------------------------------------------------------------------------------- 1 | """Transfer the detection results of coco-style to submission format 2 | ==== 3 | Features: 4 | > LSVT 5 | > ReCTS 6 | ==== 7 | Author: 8 | > tkianai 9 | """ 10 | 11 | import os 12 | import json 13 | import cv2 14 | import numpy as np 15 | import argparse 16 | from tqdm import tqdm 17 | import pycocotools.mask as maskUtils 18 | from imantics import Mask as maskToPolygon 19 | import pickle 20 | 21 | CONFIG = { 22 | "lsvt": { 23 | "name": "lsvt", 24 | "TOTAL_NUM": 20000, 25 | "START": 0, 26 | "id_prefix": "res_", 27 | "id_form": "{}", 28 | "id_suffix": "", 29 | "file_suffix": 'json', 30 | "points_len": None, 31 | }, 32 | "rects": { 33 | "name": "rects", 34 | "TOTAL_NUM": 5000, 35 | "START": 1, 36 | "id_prefix": "test_", 37 | "id_form": "{:0>6d}", 38 | "id_suffix": ".jpg", 39 | "file_suffix": 'txt', 40 | "points_len": 4 41 | }, 42 | "art": { 43 | "name": "art", 44 | "TOTAL_NUM": 4563, 45 | "START": 0, 46 | "id_prefix": "res_", 47 | "id_form": "{}", 48 | "id_suffix": "", 49 | "file_suffix": 'json', 50 | "points_len": None, 51 | } 52 | } 53 | 54 | class FORMAT(object): 55 | def __init__(self, dt_file, mode='lsvt'): 56 | mode = mode.lower() 57 | assert mode in ['lsvt', 'rects', 'art'], "Mode [{}] is not supported! Try [lsvt | rects | art]!" 58 | self.results = json.load(open(dt_file)) 59 | self.config = CONFIG[mode] 60 | 61 | def check_clockwise(self, points): 62 | points = np.array(points) 63 | points = cv2.convexHull(points) 64 | points = points[:, 0, :] 65 | y = points[:, 1] 66 | idx = np.argmax(y) 67 | x1 = points[(idx - 1 + len(points)) % len(points)] 68 | x2 = points[idx] 69 | x3 = points[(idx + 1) % len(points)] 70 | x2_x1 = x2 - x1 71 | x3_x2 = x3 - x2 72 | judge_result = x2_x1[0] * x3_x2[1] - x2_x1[1] * x3_x2[0] 73 | if judge_result < 0: 74 | points = points[::-1] 75 | 76 | return points.tolist() 77 | 78 | def check_points_len(self, points, bbox): 79 | if len(points) == self.config['points_len']: 80 | return points 81 | 82 | roi_area = cv2.contourArea(points) 83 | thr = 0.03 84 | while thr < 0.08: 85 | points_validate = [] 86 | idx_remove = [] 87 | for p in range(len(points)): 88 | index = list(range(len(points))) 89 | index.remove(p) 90 | for k in idx_remove: 91 | index.remove(k) 92 | area = cv2.contourArea(points[index]) 93 | if np.abs(roi_area - area) / roi_area > thr: 94 | points_validate.append(points[p]) 95 | else: 96 | idx_remove.append(p) 97 | if len(points_validate) == self.config['points_len']: 98 | return np.array(points_validate) 99 | 100 | thr += 0.01 101 | 102 | # return minAreaRect 103 | rect = cv2.minAreaRect(points) 104 | box = cv2.boxPoints(rect) 105 | box[box[:, 0] < bbox[0], 0] = bbox[0] 106 | box[box[:, 1] < bbox[1], 1] = bbox[1] 107 | box[box[:, 0] > bbox[0] + bbox[2], 0] = bbox[0] + bbox[2] 108 | box[box[:, 1] > bbox[1] + bbox[3], 1] = bbox[1] + bbox[3] 109 | 110 | return box.astype(np.int) 111 | 112 | 113 | def format(self, save_name='./data/lsvt_submission.json'): 114 | 115 | save_dir = os.path.dirname(save_name) 116 | if not os.path.exists(save_dir): 117 | os.makedirs(save_dir) 118 | filename = os.path.basename(save_name) 119 | filename = filename.split('.') 120 | filename[-1] = self.config['file_suffix'] 121 | filename = '.'.join(filename) 122 | save_name = os.path.join(save_dir, filename) 123 | 124 | submission_out = {} 125 | for itm in tqdm(self.results): 126 | fileid = self.config['id_prefix'] + str(itm['image_id']) + self.config['id_suffix'] 127 | if fileid not in submission_out: 128 | submission_out[fileid] = [] 129 | 130 | _mask = maskUtils.decode(itm['segmentation']).astype(np.bool) 131 | polygons = maskToPolygon(_mask).polygons() 132 | roi_areas = [cv2.contourArea(points) for points in polygons.points] 133 | if max(roi_areas) < 1: 134 | continue 135 | idx = roi_areas.index(max(roi_areas)) 136 | points = polygons.points[idx] 137 | roi_area = roi_areas[idx] 138 | 139 | # eliminate unnecessarily points 140 | points_validate = [] 141 | idx_remove = [] 142 | for p in range(len(points)): 143 | index = list(range(len(points))) 144 | index.remove(p) 145 | for k in idx_remove: 146 | index.remove(k) 147 | area = cv2.contourArea(points[index]) 148 | if np.abs(roi_area - area) / roi_area > 0.02: 149 | points_validate.append(points[p]) 150 | else: 151 | idx_remove.append(p) 152 | points_validate = np.array(points_validate) 153 | 154 | if self.config['points_len'] is not None: 155 | points_validate = self.check_points_len(points_validate, itm['bbox']) 156 | 157 | points_validate = self.check_clockwise(points_validate.tolist()) 158 | 159 | info = {} 160 | info['points'] = points_validate 161 | info['confidence'] = float("{:.3f}".format(itm['score'])) 162 | submission_out[fileid].append(info) 163 | 164 | # validate all files 165 | if self.config['name'] == 'art': 166 | # assert len(submission_out) == self.config['TOTAL_NUM'] 167 | with open('tmp_submission.pkl', 'wb') as w_obj: 168 | pickle.dump(submission_out, w_obj) 169 | with open('data/ids-art.pkl', 'rb') as r_obj: 170 | ids = pickle.load(r_obj) 171 | for idx in ids: 172 | id_ = self.config['id_prefix'] + self.config['id_form'].format(idx) + self.config['id_suffix'] 173 | if id_ not in submission_out: 174 | submission_out[id_] = [] 175 | else: 176 | for i in range(self.config['START'], self.config['START'] + self.config['TOTAL_NUM']): 177 | id_ = self.config['id_prefix'] + self.config['id_form'].format(i) + self.config['id_suffix'] 178 | if id_ not in submission_out: 179 | submission_out[id_] = [] 180 | 181 | # save file 182 | with open(save_name, 'w') as w_obj: 183 | if self.config['file_suffix'] == 'json': 184 | json.dump(submission_out, w_obj) 185 | else: 186 | files = sorted(submission_out.keys()) 187 | for file in files: 188 | w_obj.write("{}\n".format(file)) 189 | for det_res in submission_out[file]: 190 | points = [] 191 | for tmp_point in det_res['points']: 192 | points.append(str(int(tmp_point[0]))) 193 | points.append(str(int(tmp_point[1]))) 194 | points = ','.join(points) 195 | w_obj.write(points + '\n') 196 | 197 | print("Results have been saved to {}".format(save_name)) 198 | 199 | if __name__ == "__main__": 200 | 201 | parser = argparse.ArgumentParser(description="Tranfer the format to submission style.") 202 | parser.add_argument('--dt-file', default=None, help='coco style detection file.') 203 | parser.add_argument('--mode', default='lsvt', help='choose the format you want transfer to, [lsvt | rects | art].') 204 | parser.add_argument('--save', default='data/lsvt_task1.json', help='filepath to save the transfered results.') 205 | args = parser.parse_args() 206 | 207 | formatObj = FORMAT(args.dt_file, mode=args.mode) 208 | formatObj.format(args.save) 209 | -------------------------------------------------------------------------------- /format_transfer_art_dilated.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import cv2 4 | import numpy as np 5 | import json 6 | from pycocotools.coco import COCO 7 | from pycocotools.cocoeval import COCOeval 8 | import pycocotools.mask as mask_util 9 | from tqdm import tqdm 10 | 11 | if __name__ == '__main__': 12 | 13 | det_json = 'data/results_art_finetune_high_level.pkl.json' 14 | submit_json = 'submit_art_finetune_high_level.json' 15 | submit_dir = 'data' 16 | image_folder = 'tmp/art_test_full' 17 | visual_dir = './art_full_test_det_result' 18 | 19 | if not os.path.exists(visual_dir): 20 | os.makedirs(visual_dir) 21 | if not os.path.exists(submit_dir): 22 | os.makedirs(submit_dir) 23 | 24 | # get val image list 25 | with open(det_json, "r")as f_json: 26 | det_infos = json.loads(f_json.read()) 27 | print('num_det:', len(det_infos)) 28 | imgIds = [] 29 | for info in det_infos: 30 | imgIds.append(info['image_id']) 31 | imgIds = list(set(imgIds)) 32 | print('num_imgId:', len(imgIds)) 33 | kernel_size = (15, 15) 34 | sigma = 1 35 | kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5)) 36 | submit_result = {} 37 | for i in tqdm(range(len(imgIds))): 38 | 39 | imgid = imgIds[i] 40 | img_path = os.path.join(image_folder, 'gt_'+str(imgid)+'.jpg') 41 | image = cv2.imread(img_path) 42 | img_h = image.shape[0] 43 | img_w = image.shape[1] 44 | 45 | mr_segs = [] 46 | mr_score = [] 47 | h, w = 0, 0 48 | 49 | key = 'res_' + str(imgid) 50 | img_result = [] 51 | 52 | for info in det_infos: 53 | if info['image_id'] == imgid and info['score'] > 0.45: 54 | mask = mask_util.decode(info['segmentation']) 55 | h, w = mask.shape[0], mask.shape[1] 56 | mr_segs.append(mask) 57 | mr_score.append(info['score']) 58 | 59 | for m in range(len(mr_segs)): 60 | if mr_score[m] > 0.45: 61 | seg_mask = mr_segs[m].astype(np.uint8) * 255 62 | seg_mask = cv2.GaussianBlur(seg_mask, kernel_size, sigma) 63 | seg_mask = cv2.dilate(seg_mask, kernel) 64 | _, seg_mask = cv2.threshold( 65 | seg_mask, 80, 255, cv2.THRESH_BINARY) 66 | contours, _ = cv2.findContours( 67 | seg_mask, 1, cv2.CHAIN_APPROX_SIMPLE) 68 | maxc, maxc_idx = 0, 0 69 | if len(contours) < 1: 70 | continue 71 | for c in range(len(contours)): 72 | if len(contours[c]) > maxc: 73 | maxc = len(contours[c]) 74 | maxc_idx = c 75 | cnt = contours[maxc_idx] 76 | if cnt.shape[0] < 4: 77 | continue 78 | 79 | #image = cv2.drawContours(image, [cnt], 0, (0, 255, 0), 1) 80 | single_box = {} 81 | epsilon = 0.005 * cv2.arcLength(cnt, True) 82 | cnt = cv2.approxPolyDP(cnt, epsilon, True) 83 | cnt = cnt.reshape(-1, 2) 84 | num_p = cnt.shape[0] 85 | cnt = cnt[::-1, :] 86 | flag = True 87 | xlist = cnt[:, 0] 88 | ylist = cnt[:, 1] 89 | num_p = cnt.shape[0] 90 | for c in range(num_p): 91 | x = int(xlist[c]) 92 | y = int(ylist[c]) 93 | if not(x >= 0 and x < img_w and y >= 0 and y < img_h): 94 | flag = False 95 | if flag: 96 | if cnt.shape[0] > 20: 97 | single_box['points'] = cnt[::2, :].tolist() 98 | else: 99 | single_box['points'] = cnt.tolist() 100 | single_box['confidence'] = mr_score[m] 101 | img_result.append(single_box) 102 | image = cv2.drawContours( 103 | image, [cnt.reshape(-1, 1, 2)], 0, (0, 255, 0), 2) 104 | 105 | if len(img_result) == 0: 106 | single_box = {} 107 | single_box['points'] = [[1, 1], [10, 1], [10, 10], [2, 10]] 108 | single_box['confidence'] = 0.05 109 | img_result.append(single_box) 110 | submit_result[key] = img_result 111 | save_path = os.path.join(visual_dir, 'test_'+str(imgid)+'.jpg') 112 | cv2.imwrite(save_path, image) 113 | 114 | with open(os.path.join(submit_dir, submit_json), 'wb') as outfile: 115 | outfile.write(json.dumps(submit_result).encode("utf-8")) 116 | -------------------------------------------------------------------------------- /loss_visualize.py: -------------------------------------------------------------------------------- 1 | """This is helper function for visualizing 2 | ==== 3 | Features: 4 | > training loss 5 | > validation mAP 6 | """ 7 | 8 | import os 9 | import argparse 10 | import matplotlib.pyplot as plt 11 | 12 | 13 | def parse_args(): 14 | parser = argparse.ArgumentParser(description="Visualize training loss!") 15 | parser.add_argument('--log-file', default=None) 16 | parser.add_argument('--save-name', default=None) 17 | return parser.parse_args() 18 | 19 | 20 | def parse_log(args): 21 | 22 | with open(args.log_file) as r_obj: 23 | epochs = [] 24 | iters = [] 25 | losses = [] 26 | segm_map = [] 27 | for line in r_obj: 28 | line = line.strip() 29 | if line and 'eta' in line: 30 | iters_str = line.split(' ')[6].split(']') 31 | epoch = int(iters_str[0][1:]) 32 | max_iters = int(iters_str[1].split('/')[-1]) 33 | itr = (epoch - 1) * max_iters + int(iters_str[1].split('/')[0][1:]) 34 | iters.append(itr) 35 | losses.append(float(line.split(' ')[-1])) 36 | if line and 'segm_mAP' in line: 37 | iters_str = line.split(' ')[6].split(']') 38 | epoch = int(iters_str[0][1:]) 39 | epochs.append(epoch) 40 | segm_map.append(float(line.split(' ')[-1])) 41 | 42 | save_dir = os.path.dirname(args.save_name) 43 | if not os.path.exists(save_dir): 44 | os.makedirs(save_dir) 45 | plt.clf() 46 | plt.plot(iters, losses) 47 | plt.xlim(0, max(iters)) 48 | plt.ylim(0, max(losses)) 49 | plt.title('Training Losses') 50 | plt.grid() 51 | plt.xlabel('iteration') 52 | plt.ylabel('loss') 53 | plt.savefig(args.save_name + '.loss.png', dpi=400) 54 | 55 | plt.clf() 56 | plt.plot(epochs, segm_map) 57 | plt.xlim(0, max(epochs)) 58 | plt.ylim(0, max(segm_map)) 59 | plt.title('Validation mAP') 60 | plt.grid() 61 | plt.xlabel('epochs') 62 | plt.ylabel('mAP') 63 | plt.savefig(args.save_name + '.mAP.png', dpi=400) 64 | 65 | 66 | if __name__ == "__main__": 67 | 68 | args = parse_args() 69 | parse_log(args) 70 | 71 | 72 | """Notes about validation / rects 73 | Epoch | mAP(bbox) | mAP(segm) 74 | --- | --- | --- 75 | 1 | 0.554 | 0.536 76 | 2 | 0.596 | 0.572 77 | 3 | 0.605 | 0.586 78 | 4 | 0.610 | 0.592 79 | 5 | 0.620 | 0.601 80 | 6 | 0.612 | 0.588 81 | 7 | 0.624 | 0.599 82 | 8 | 0.616 | 0.596 83 | 9 | 0.619 | 0.597 84 | """ 85 | -------------------------------------------------------------------------------- /utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tkianai/ICDAR2019-tools/519036e44fa5dba87ded51fd3875df11888f76e1/utils/__init__.py -------------------------------------------------------------------------------- /utils/iou.py: -------------------------------------------------------------------------------- 1 | """THis is helper function about IOU computation 2 | ==== 3 | Features: 4 | > compute iou over two polygons 5 | """ 6 | 7 | import numpy as np 8 | from skimage.draw import polygon 9 | 10 | def compute_polygons_iou(polygon1, polygon2): 11 | """ 12 | Intersection over union between two shapely polygons. 13 | """ 14 | x1 = np.array(polygon1[0::2]) 15 | y1 = np.array(polygon1[1::2]) 16 | x2 = np.array(polygon2[0::2]) 17 | y2 = np.array(polygon2[1::2]) 18 | IoU = iou(x1, y1, x2, y2) 19 | return IoU 20 | 21 | def approx_area_of_intersection(dt_x, dt_y, gt_x, gt_y): 22 | """ 23 | This helper determine if both polygons are intersecting with each others with an approximation method. 24 | Area of intersection represented by the minimum bounding rectangular [xmin, ymin, xmax, ymax] 25 | """ 26 | dt_ymax = np.max(dt_y) 27 | dt_xmax = np.max(dt_x) 28 | dt_ymin = np.min(dt_y) 29 | dt_xmin = np.min(dt_x) 30 | 31 | gt_ymax = np.max(gt_y) 32 | gt_xmax = np.max(gt_x) 33 | gt_ymin = np.min(gt_y) 34 | gt_xmin = np.min(gt_x) 35 | 36 | all_min_ymax = np.minimum(dt_ymax, gt_ymax) 37 | all_max_ymin = np.maximum(dt_ymin, gt_ymin) 38 | 39 | intersect_heights = np.maximum(0.0, (all_min_ymax - all_max_ymin)) 40 | 41 | all_min_xmax = np.minimum(dt_xmax, gt_xmax) 42 | all_max_xmin = np.maximum(dt_xmin, gt_xmin) 43 | intersect_widths = np.maximum(0.0, (all_min_xmax - all_max_xmin)) 44 | 45 | return intersect_heights * intersect_widths 46 | 47 | def iou(dt_x, dt_y, gt_x, gt_y): 48 | """ 49 | This helper determine the intersection over union of two polygons. 50 | """ 51 | 52 | if approx_area_of_intersection(dt_x, dt_y, gt_x, gt_y) > 1: # only proceed if it passes the approximation test 53 | ymax = np.maximum(np.max(dt_y), np.max(gt_y)) + 1 54 | xmax = np.maximum(np.max(dt_x), np.max(gt_x)) + 1 55 | bin_mask = np.zeros((ymax, xmax)) 56 | dt_bin_mask = np.zeros_like(bin_mask) 57 | gt_bin_mask = np.zeros_like(bin_mask) 58 | 59 | rr, cc = polygon(dt_y, dt_x) 60 | dt_bin_mask[rr, cc] = 1 61 | 62 | rr, cc = polygon(gt_y, gt_x) 63 | gt_bin_mask[rr, cc] = 1 64 | 65 | final_bin_mask = dt_bin_mask + gt_bin_mask 66 | 67 | inter_map = np.where(final_bin_mask == 2, 1, 0) 68 | inter = np.sum(inter_map) 69 | 70 | union_map = np.where(final_bin_mask > 0, 1, 0) 71 | union = np.sum(union_map) 72 | return inter / float(union + 1.0) 73 | else: 74 | return 0 --------------------------------------------------------------------------------