From 2b8f904e9b89d768ea0101f0828899829e3c15eb Mon Sep 17 00:00:00 2001
From: chengjuntao <18222160892@163.com>
Date: Tue, 14 Jan 2020 15:57:20 +0800
Subject: [PATCH] Add RRPN models for PaddleCV (#4148)
* add rrpn for models
---
PaddleCV/rrpn/README.md | 168 ++++
PaddleCV/rrpn/__init__.py | 0
PaddleCV/rrpn/checkpoint.py | 186 +++++
PaddleCV/rrpn/config.py | 226 ++++++
PaddleCV/rrpn/data_utils.py | 249 ++++++
PaddleCV/rrpn/edict.py | 37 +
PaddleCV/rrpn/eval.py | 91 +++
PaddleCV/rrpn/eval_helper.py | 379 +++++++++
PaddleCV/rrpn/image/img_119.jpg | Bin 0 -> 170138 bytes
PaddleCV/rrpn/image/img_120.jpg | Bin 0 -> 120354 bytes
PaddleCV/rrpn/infer.py | 81 ++
PaddleCV/rrpn/models/__init__.py | 0
PaddleCV/rrpn/models/ext_op/rrpn_lib.py | 549 +++++++++++++
PaddleCV/rrpn/models/ext_op/src/README.md | 68 ++
PaddleCV/rrpn/models/ext_op/src/bbox_util.h | 360 +++++++++
PaddleCV/rrpn/models/ext_op/src/blas.h | 487 ++++++++++++
.../models/ext_op/src/concat_and_split.cc | 76 ++
.../rrpn/models/ext_op/src/concat_and_split.h | 59 ++
PaddleCV/rrpn/models/ext_op/src/gather.cu.h | 125 +++
PaddleCV/rrpn/models/ext_op/src/gather.h | 74 ++
PaddleCV/rrpn/models/ext_op/src/make.sh | 73 ++
.../rrpn/models/ext_op/src/math_function.cc | 73 ++
.../rrpn/models/ext_op/src/math_function.h | 43 +
.../ext_op/src/rotated_anchor_generator_op.cc | 172 ++++
.../ext_op/src/rotated_anchor_generator_op.cu | 153 ++++
.../ext_op/src/rotated_anchor_generator_op.h | 111 +++
.../models/ext_op/src/rrpn_box_coder_op.cc | 128 +++
.../models/ext_op/src/rrpn_box_coder_op.cu | 198 +++++
.../src/rrpn_generate_proposal_labels_op.cc | 638 +++++++++++++++
.../ext_op/src/rrpn_generate_proposals_op.cc | 694 ++++++++++++++++
.../ext_op/src/rrpn_generate_proposals_op.cu | 747 ++++++++++++++++++
.../ext_op/src/rrpn_rotated_roi_align_op.cc | 197 +++++
.../ext_op/src/rrpn_rotated_roi_align_op.cu | 442 +++++++++++
.../ext_op/src/rrpn_target_assign_op.cc | 544 +++++++++++++
PaddleCV/rrpn/models/ext_op/src/safe_ref.h | 35 +
PaddleCV/rrpn/models/model_builder.py | 379 +++++++++
PaddleCV/rrpn/models/name_adapter.py | 71 ++
PaddleCV/rrpn/models/resnet.py | 358 +++++++++
PaddleCV/rrpn/pretrained/download.sh | 5 +
PaddleCV/rrpn/reader.py | 180 +++++
PaddleCV/rrpn/roidbs.py | 364 +++++++++
PaddleCV/rrpn/train.py | 224 ++++++
PaddleCV/rrpn/utility.py | 188 +++++
43 files changed, 9232 insertions(+)
create mode 100644 PaddleCV/rrpn/README.md
create mode 100755 PaddleCV/rrpn/__init__.py
create mode 100644 PaddleCV/rrpn/checkpoint.py
create mode 100755 PaddleCV/rrpn/config.py
create mode 100755 PaddleCV/rrpn/data_utils.py
create mode 100755 PaddleCV/rrpn/edict.py
create mode 100755 PaddleCV/rrpn/eval.py
create mode 100755 PaddleCV/rrpn/eval_helper.py
create mode 100644 PaddleCV/rrpn/image/img_119.jpg
create mode 100644 PaddleCV/rrpn/image/img_120.jpg
create mode 100755 PaddleCV/rrpn/infer.py
create mode 100755 PaddleCV/rrpn/models/__init__.py
create mode 100644 PaddleCV/rrpn/models/ext_op/rrpn_lib.py
create mode 100644 PaddleCV/rrpn/models/ext_op/src/README.md
create mode 100644 PaddleCV/rrpn/models/ext_op/src/bbox_util.h
create mode 100644 PaddleCV/rrpn/models/ext_op/src/blas.h
create mode 100644 PaddleCV/rrpn/models/ext_op/src/concat_and_split.cc
create mode 100644 PaddleCV/rrpn/models/ext_op/src/concat_and_split.h
create mode 100644 PaddleCV/rrpn/models/ext_op/src/gather.cu.h
create mode 100644 PaddleCV/rrpn/models/ext_op/src/gather.h
create mode 100644 PaddleCV/rrpn/models/ext_op/src/make.sh
create mode 100644 PaddleCV/rrpn/models/ext_op/src/math_function.cc
create mode 100644 PaddleCV/rrpn/models/ext_op/src/math_function.h
create mode 100644 PaddleCV/rrpn/models/ext_op/src/rotated_anchor_generator_op.cc
create mode 100644 PaddleCV/rrpn/models/ext_op/src/rotated_anchor_generator_op.cu
create mode 100644 PaddleCV/rrpn/models/ext_op/src/rotated_anchor_generator_op.h
create mode 100644 PaddleCV/rrpn/models/ext_op/src/rrpn_box_coder_op.cc
create mode 100644 PaddleCV/rrpn/models/ext_op/src/rrpn_box_coder_op.cu
create mode 100644 PaddleCV/rrpn/models/ext_op/src/rrpn_generate_proposal_labels_op.cc
create mode 100644 PaddleCV/rrpn/models/ext_op/src/rrpn_generate_proposals_op.cc
create mode 100644 PaddleCV/rrpn/models/ext_op/src/rrpn_generate_proposals_op.cu
create mode 100644 PaddleCV/rrpn/models/ext_op/src/rrpn_rotated_roi_align_op.cc
create mode 100644 PaddleCV/rrpn/models/ext_op/src/rrpn_rotated_roi_align_op.cu
create mode 100644 PaddleCV/rrpn/models/ext_op/src/rrpn_target_assign_op.cc
create mode 100755 PaddleCV/rrpn/models/ext_op/src/safe_ref.h
create mode 100755 PaddleCV/rrpn/models/model_builder.py
create mode 100644 PaddleCV/rrpn/models/name_adapter.py
create mode 100644 PaddleCV/rrpn/models/resnet.py
create mode 100755 PaddleCV/rrpn/pretrained/download.sh
create mode 100755 PaddleCV/rrpn/reader.py
create mode 100755 PaddleCV/rrpn/roidbs.py
create mode 100755 PaddleCV/rrpn/train.py
create mode 100755 PaddleCV/rrpn/utility.py
diff --git a/PaddleCV/rrpn/README.md b/PaddleCV/rrpn/README.md
new file mode 100644
index 00000000..d9e6fc34
--- /dev/null
+++ b/PaddleCV/rrpn/README.md
@@ -0,0 +1,168 @@
+# RRPN 旋转物体检测
+
+---
+## 内容
+
+- [安装](#安装)
+- [简介](#简介)
+- [数据准备](#数据准备)
+- [模型训练](#模型训练)
+- [模型评估](#模型评估)
+- [模型推断及可视化](#模型推断及可视化)
+
+## 安装
+
+在当前目录下运行样例代码需要PadddlePaddle Fluid的develop或以上的版本。如果你的运行环境中的PaddlePaddle低于此版本,请根据[安装文档](http://www.paddlepaddle.org/)中的说明来更新PaddlePaddle。
+
+
+## 简介
+RRPN是在Faster RCNN基础上拓展出的两阶段目标检测器,可用于文字检测和旋转物体检测。通过对图像生成候选区域,提取特征,判别特征类别并修正候选框位置。
+
+[RRPN](https://arxiv.org/abs/1703.01086) 整体网络可以分为4个主要内容:
+
+1. 基础卷积层。作为一种卷积神经网络目标检测方法,RRPN首先使用一组基础的卷积网络提取图像的特征图。特征图被后续RPN层和全连接层共享。本示例采用[ResNet-50](https://arxiv.org/abs/1512.03385)作为基础卷积层。
+2. 区域生成网络(RPN)。RPN网络用于生成候选区域(proposals)。该层通过一组固定的尺寸、比例和角度得到一组带方向锚点(anchors), 通过softmax判断旋转的锚点属于前景或者背景,再利用区域回归修正锚点从而获得精确的候选区域。
+3. Rotated RoI Align。该层收集输入的特征图和带方向的候选区域,将带方向的候选区域映射到特征图中进行并池化为统一大小的区域特征图,送入全连接层判定目标类别。
+4. 检测层。利用区域特征图计算候选区域的类别,同时再次通过区域回归获得检测框最终的精确位置。
+
+### 编译自定义OP
+
+自定义OP编译方式如下:
+
+ 进入 `ext_op/src` 目录,执行编译脚本
+ ```
+ cd ext_op/src
+ sh make.sh ${cuda_path} ${cudnn_path} ${nccl_path}
+ '''
+ 其中${cuda_path}、$cudnn_path}和{nccl_path}分别为cuda、cudnn、nccl的安装路径,需通过命令行进行指定
+ 成功编译后,`ext_op/src` 目录下将会生成 `rrpn_lib.so`
+
+## 数据准备
+### 公开数据集
+在[ICDAR2015数据集](https://rrc.cvc.uab.es/?ch=4&com=downloads)上进行训练,数据集需进入官网进行注册后方可下载。
+
+数据目录结构如下:
+
+```
+dataset/icdar2015/
+├── ch4_training_images
+│ ├── img_143.jpg
+│ ├── img_144.jpg
+| ...
+├── ch4_training_localization_transcription_gt
+│ ├── gt_img_143.txt
+│ ├── gt_img_144.txt
+| ...
+├── ch4_test_images
+│ ├── img_111.jpg
+│ ├── img_112.jpg
+| ...
+├── ch4_test_localization_transcription_gt
+│ ├── img_111.jpg
+│ ├── img_112.jpg
+| ...
+```
+### 自定义数据
+原始的RRPN只提供了二分类,若要使用自己数据进行训练多分类,需在utility.py中将dataset改为icdar2017,然后将class_num改为需求类别数,其中0为背景类。
+
+训练自定义数据时,数据目录结构和ICDAR2015一致,标注数据格式如下:
+```
+x1, y1, x2, y2, x3, y3, x4, y4, class_name
+x1, y1, x2, y2, x3, y3, x4, y4, class_name
+```
+
+## 模型训练
+
+**下载预训练模型:** 本示例提供Resnet-50预训练模型,采用如下命令下载预训练模型:
+
+ sh ./pretrained/download.sh
+
+
+通过初始化`pretrained_model` 加载预训练模型。同时在参数微调时也采用该设置加载已训练模型。
+请在训练前确认预训练模型下载与加载正确,否则训练过程中损失可能会出现NAN。
+
+
+- RRPN
+
+ ```
+ python train.py \
+ --model_save_dir=output/ \
+ --pretrained_model=${path_to_pretrain_model} \
+ --data_dir=${path_to_data} \
+ ```
+
+
+
+ - 通过设置export CUDA_VISIBLE_DEVICES=0,1,2,3,4,5,6,7指定8卡GPU训练。
+
+ - 可选参数见:
+
+ python train.py --help
+
+**数据读取器说明:** 数据读取器定义在reader.py中。所有图像将短边等比例缩放至`scales`,若长边大于`max_size`, 则再次将长边等比例缩放至`max_size`。在训练阶段,对图像采用随机旋转。
+
+**模型设置:**
+
+* 使用RotatedRoIAlign方法。
+* 训练过程pre\_nms=12000, post\_nms=2000,测试过程pre\_nms=6000, post\_nms=1000。nms阈值为0.7。
+* RPN网络得到labels的过程中,fg\_fraction=0.25,fg\_thresh=0.5,bg\_thresh_hi=0.5,bg\_thresh\_lo=0.0
+* RPN选择anchor时,rpn\_fg\_fraction=0.5,rpn\_positive\_overlap=0.7,rpn\_negative\_overlap=0.3
+
+
+**训练策略:**
+* 默认配置采用8卡,每卡batch size=1
+* 采用momentum优化算法训练,momentum=0.9。
+* 权重衰减系数为0.02,前500轮学习率从0.00333线性增加至0.01。在6250,12500轮时使用0.1,0.01乘子进行学习率衰减,最大训练17500轮。训练最大轮数和学习率策略可以在config.py中对max_iter和lr_steps进行设置。
+* 非基础卷积层卷积bias学习率为整体学习率2倍。
+* 基础卷积层中,affine_layers参数不更新,res2层参数不更新。
+
+## 模型评估
+
+模型评估是指对训练完毕的模型评估各类性能指标。本示例采用[ICDAR2015官方评估](https://rrc.cvc.uab.es/?com=contestant)
+
+`eval.py`是评估模块的主要执行程序,调用示例如下:
+
+- RRPN
+
+ ```
+ python eval.py \
+ --dataset=icdar2015 \
+ --pretrained_model=${path_to_trained_model}
+ ```
+
+ - 通过设置`--pretrained_model=${path_to_trained_model}`指定训练好的模型,注意不是初始化的模型。
+ - 通过设置`export CUDA\_VISIBLE\_DEVICES=0`指定单卡GPU评估。
+
+
+下表为模型评估结果:
+
+RRPN
+
+| 模型 | 批量大小 | 迭代次数 | F1 |
+| :--------------- | :------------: | :------------------: |------: |
+| [RRPN](https://paddleseg.bj.bcebos.com/deploy/temp/model_final.tar) |8 | 17500 | 0.8048 |
+
+
+
+
+
+
+## 模型推断及可视化
+
+模型推断可以获取图像中的物体及其对应的类别,`infer.py`是主要执行程序,调用示例如下:
+
+```
+python infer.py \
+ --pretrained_model=${path_to_trained_model} \
+ --image_path=dataset/icdar2015 \
+ --draw_threshold=0.6
+```
+
+注意,请正确设置模型路径`${path_to_trained_model}`和预测图片路径。默认使用GPU设备,也可通过设置`--use_gpu=False`使用CPU设备。可通过设置`draw_threshold`调节得分阈值控制检测框的个数。
+
+下图为模型可视化预测结果:
+
+
+
+RRPN 预测可视化
+
diff --git a/PaddleCV/rrpn/__init__.py b/PaddleCV/rrpn/__init__.py
new file mode 100755
index 00000000..e69de29b
diff --git a/PaddleCV/rrpn/checkpoint.py b/PaddleCV/rrpn/checkpoint.py
new file mode 100644
index 00000000..7062199e
--- /dev/null
+++ b/PaddleCV/rrpn/checkpoint.py
@@ -0,0 +1,186 @@
+# Copyright (c) 2019 PaddlePaddle Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+from __future__ import unicode_literals
+
+import errno
+import os
+import shutil
+import time
+import numpy as np
+import re
+import paddle.fluid as fluid
+import logging
+logger = logging.getLogger(__name__)
+
+
+def load_params(exe, prog, path):
+ """
+ Load model from the given path.
+ Args:
+ exe (fluid.Executor): The fluid.Executor object.
+ prog (fluid.Program): load weight to which Program object.
+ path (string): URL string or loca model path.
+ """
+
+ if not os.path.exists(path):
+ raise ValueError("Model pretrain path {} does not "
+ "exists.".format(path))
+
+ logger.info('Loading parameters from {}...'.format(path))
+
+ def _if_exist(var):
+ param_exist = os.path.exists(os.path.join(path, var.name))
+ do_load = param_exist
+ if do_load:
+ logger.debug('load weight {}'.format(var.name))
+ return do_load
+
+ fluid.io.load_vars(exe, path, prog, predicate=_if_exist)
+
+
+def save(exe, prog, path):
+ """
+ Load model from the given path.
+ Args:
+ exe (fluid.Executor): The fluid.Executor object.
+ prog (fluid.Program): save weight from which Program object.
+ path (string): the path to save model.
+ """
+ if os.path.isdir(path):
+ shutil.rmtree(path)
+ logger.info('Save model to {}.'.format(path))
+ fluid.io.save_persistables(exe, path, prog)
+
+
+def load_and_fusebn(exe, prog, path):
+ """
+ Fuse params of batch norm to scale and bias.
+ Args:
+ exe (fluid.Executor): The fluid.Executor object.
+ prog (fluid.Program): save weight from which Program object.
+ path (string): the path to save model.
+ """
+ logger.info('Load model and fuse batch norm if have from {}...'.format(
+ path))
+
+ if not os.path.exists(path):
+ raise ValueError("Model path {} does not exists.".format(path))
+
+ def _if_exist(var):
+ b = os.path.exists(os.path.join(path, var.name))
+
+ if b:
+ logger.debug('load weight {}'.format(var.name))
+ return b
+
+ all_vars = list(filter(_if_exist, prog.list_vars()))
+
+ # Since the program uses affine-channel, there is no running mean and var
+ # in the program, here append running mean and var.
+ # NOTE, the params of batch norm should be like:
+ # x_scale
+ # x_offset
+ # x_mean
+ # x_variance
+ # x is any prefix
+ mean_variances = set()
+ bn_vars = []
+
+ bn_in_path = True
+
+ inner_prog = fluid.Program()
+ inner_start_prog = fluid.Program()
+ inner_block = inner_prog.global_block()
+ with fluid.program_guard(inner_prog, inner_start_prog):
+ for block in prog.blocks:
+ ops = list(block.ops)
+ if not bn_in_path:
+ break
+ for op in ops:
+ if op.type == 'affine_channel':
+ # remove 'scale' as prefix
+ scale_name = op.input('Scale')[0] # _scale
+ bias_name = op.input('Bias')[0] # _offset
+ prefix = scale_name[:-5]
+ mean_name = prefix + 'mean'
+ variance_name = prefix + 'variance'
+
+ if not os.path.exists(os.path.join(path, mean_name)):
+ bn_in_path = False
+ break
+ if not os.path.exists(os.path.join(path, variance_name)):
+ bn_in_path = False
+ break
+
+ bias = block.var(bias_name)
+
+ mean_vb = inner_block.create_var(
+ name=mean_name,
+ type=bias.type,
+ shape=bias.shape,
+ dtype=bias.dtype,
+ persistable=True)
+ variance_vb = inner_block.create_var(
+ name=variance_name,
+ type=bias.type,
+ shape=bias.shape,
+ dtype=bias.dtype,
+ persistable=True)
+
+ mean_variances.add(mean_vb)
+ mean_variances.add(variance_vb)
+
+ bn_vars.append(
+ [scale_name, bias_name, mean_name, variance_name])
+
+ if not bn_in_path:
+ fluid.io.load_vars(exe, path, prog, vars=all_vars)
+ logger.warning(
+ "There is no paramters of batch norm in model {}. "
+ "Skip to fuse batch norm. And load paramters done.".format(path))
+ return
+
+ # load running mean and running variance on cpu place into global scope.
+ place = fluid.CPUPlace()
+ exe_cpu = fluid.Executor(place)
+ fluid.io.load_vars(exe_cpu, path, vars=[v for v in mean_variances])
+
+ # load params on real place into global scope.
+ fluid.io.load_vars(exe, path, prog, vars=all_vars)
+
+ eps = 1e-5
+ for names in bn_vars:
+ scale_name, bias_name, mean_name, var_name = names
+
+ scale = fluid.global_scope().find_var(scale_name).get_tensor()
+ bias = fluid.global_scope().find_var(bias_name).get_tensor()
+ mean = fluid.global_scope().find_var(mean_name).get_tensor()
+ var = fluid.global_scope().find_var(var_name).get_tensor()
+
+ scale_arr = np.array(scale)
+ bias_arr = np.array(bias)
+ mean_arr = np.array(mean)
+ var_arr = np.array(var)
+
+ bn_std = np.sqrt(np.add(var_arr, eps))
+ new_scale = np.float32(np.divide(scale_arr, bn_std))
+ new_bias = bias_arr - mean_arr * new_scale
+
+ # fuse to scale and bias in affine_channel
+ scale.set(new_scale, exe.place)
+ bias.set(new_bias, exe.place)
diff --git a/PaddleCV/rrpn/config.py b/PaddleCV/rrpn/config.py
new file mode 100755
index 00000000..7cfe7cd5
--- /dev/null
+++ b/PaddleCV/rrpn/config.py
@@ -0,0 +1,226 @@
+# Copyright (c) 2019 PaddlePaddle Authors. All Rights Reserved.
+#Licensed under the Apache License, Version 2.0 (the "License");
+#you may not use this file except in compliance with the License.
+#You may obtain a copy of the License at
+# http://www.apache.org/licenses/LICENSE-2.0
+#Unless required by applicable law or agreed to in writing, software
+#distributed under the License is distributed on an "AS IS" BASIS,
+#WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#See the License for the specific language governing permissions and
+#limitations under the License.
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+from __future__ import unicode_literals
+from edict import AttrDict
+import six
+import numpy as np
+
+_C = AttrDict()
+cfg = _C
+
+#
+# Training options
+#
+_C.TRAIN = AttrDict()
+
+# scales an image's shortest side
+_C.TRAIN.scales = [800]
+
+# max size of longest side
+_C.TRAIN.max_size = 1333
+
+# images per GPU in minibatch
+_C.TRAIN.im_per_batch = 1
+
+# roi minibatch size per image
+_C.TRAIN.batch_size_per_im = 256
+
+# target fraction of foreground roi minibatch
+_C.TRAIN.fg_fractrion = 0.25
+
+# overlap threshold for a foreground roi
+_C.TRAIN.fg_thresh = 0.5
+
+# overlap threshold for a background roi
+_C.TRAIN.bg_thresh_hi = 0.5
+_C.TRAIN.bg_thresh_lo = 0.0
+
+# If False, only resize image and not pad, image shape is different between
+# GPUs in one mini-batch. If True, image shape is the same in one mini-batch.
+_C.TRAIN.padding_minibatch = False
+
+# Snapshot period
+_C.TRAIN.snapshot_iter = 1000
+
+# number of RPN proposals to keep before NMS
+_C.TRAIN.rpn_pre_nms_top_n = 12000
+
+# number of RPN proposals to keep after NMS
+_C.TRAIN.rpn_post_nms_top_n = 2000
+
+# NMS threshold used on RPN proposals
+_C.TRAIN.rpn_nms_thresh = 0.7
+
+# min size in RPN proposals
+_C.TRAIN.rpn_min_size = 0.0
+
+# eta for adaptive NMS in RPN
+_C.TRAIN.rpn_eta = 1.0
+
+# number of RPN examples per image
+_C.TRAIN.rpn_batch_size_per_im = 256
+
+# remove anchors out of the image
+_C.TRAIN.rpn_straddle_thresh = 0.
+
+# target fraction of foreground examples pre RPN minibatch
+_C.TRAIN.rpn_fg_fraction = 0.5
+
+# min overlap between anchor and gt box to be a positive examples
+_C.TRAIN.rpn_positive_overlap = 0.7
+
+# max overlap between anchor and gt box to be a negative examples
+_C.TRAIN.rpn_negative_overlap = 0.3
+
+# stopgrad at a specified stage
+_C.TRAIN.freeze_at = 2
+
+# min area of ground truth box
+_C.TRAIN.gt_min_area = -1
+
+#
+# Inference options
+#
+_C.TEST = AttrDict()
+
+# scales an image's shortest side
+_C.TEST.scales = [800]
+
+# max size of longest side
+_C.TEST.max_size = 1333
+
+# eta for adaptive NMS in RPN
+_C.TEST.rpn_eta = 1.0
+
+# min score threshold to infer
+_C.TEST.score_thresh = 0.01
+
+# overlap threshold used for NMS
+_C.TEST.nms_thresh = 0.3
+
+# number of RPN proposals to keep before NMS
+_C.TEST.rpn_pre_nms_top_n = 6000
+
+# number of RPN proposals to keep after NMS
+_C.TEST.rpn_post_nms_top_n = 1000
+
+# min size in RPN proposals
+_C.TEST.rpn_min_size = 0.0
+
+# max number of detections
+_C.TEST.detections_per_im = 300
+
+# NMS threshold used on RPN proposals
+_C.TEST.rpn_nms_thresh = 0.7
+
+#
+# Model options
+#
+
+# Whether use mask rcnn head
+_C.MASK_ON = True
+
+# weight for bbox regression targets
+_C.bbox_reg_weights = [10.0, 10.0, 5.0, 5.0, 1.0]
+
+# RPN anchor sizes
+_C.anchor_sizes = [128, 256, 512]
+
+# RPN anchor ratio
+_C.aspect_ratio = [0.2, 0.5, 1.0]
+
+# RPN anchor angle
+_C.anchor_angle = [-30.0, 0.0, 30.0, 60.0, 90.0, 120.0]
+
+# variance of anchors
+_C.variances = [1., 1., 1., 1., 1.]
+
+# stride of feature map
+_C.rpn_stride = [16.0, 16.0]
+
+# pooled width and pooled height
+_C.roi_resolution = 14
+
+# spatial scale
+_C.spatial_scale = 1. / 16.
+
+# resolution to represent rotated roi align
+_C.resolution = 14
+
+#
+# SOLVER options
+#
+
+# derived learning rate the to get the final learning rate.
+_C.learning_rate = 0.01
+
+# maximum number of iterations
+_C.max_iter = 140000
+
+# warm up to learning rate
+_C.warm_up_iter = 500
+_C.start_factor = 1. / 3
+
+# lr steps_with_decay
+_C.lr_steps = [6250, 12500]
+_C.lr_gamma = 0.1
+
+# L2 regularization hyperparameter
+_C.weight_decay = 0.0001
+
+# momentum with SGD
+_C.momentum = 0.9
+
+#
+# ENV options
+#
+
+# support both CPU and GPU
+_C.use_gpu = True
+
+# Whether use parallel
+_C.parallel = True
+
+# Class number
+_C.class_num = 81
+
+# support pyreader
+_C.use_pyreader = True
+_C.TRAIN.min_size = 800
+_C.TRAIN.max_size = 1333
+_C.TEST.min_size = 1000
+# pixel mean values
+_C.pixel_means = [0.485, 0.456, 0.406]
+_C.pixel_std = [0.229, 0.224, 0.225]
+# clip box to prevent overflowing
+_C.bbox_clip = np.log(1000. / 16.)
+
+
+def merge_cfg_from_args(args, mode):
+ """Merge config keys, values in args into the global config."""
+ if mode == 'train':
+ sub_d = _C.TRAIN
+ else:
+ sub_d = _C.TEST
+ for k, v in sorted(six.iteritems(vars(args))):
+ d = _C
+ try:
+ value = eval(v)
+ except:
+ value = v
+ if k in sub_d:
+ sub_d[k] = value
+ else:
+ d[k] = value
diff --git a/PaddleCV/rrpn/data_utils.py b/PaddleCV/rrpn/data_utils.py
new file mode 100755
index 00000000..339f7cb3
--- /dev/null
+++ b/PaddleCV/rrpn/data_utils.py
@@ -0,0 +1,249 @@
+# Copyright (c) 2019 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.
+#
+# Based on:
+# --------------------------------------------------------
+# Detectron
+# Copyright (c) 2017-present, Facebook, Inc.
+# Licensed under the Apache License, Version 2.0;
+# Written by Ross Girshick
+# --------------------------------------------------------
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+from __future__ import unicode_literals
+
+import cv2
+import numpy as np
+from config import cfg
+import os
+from PIL import Image
+
+
+class DatasetPath(object):
+ def __init__(self, mode, dataset_name):
+ self.mode = mode
+ self.data_dir = dataset_name
+
+ def get_data_dir(self):
+ if self.mode == 'train':
+ return os.path.join(self.data_dir, 'ch4_training_images')
+ elif self.mode == 'val':
+ return os.path.join(self.data_dir, 'ch4_test_images')
+
+ def get_file_list(self):
+ if self.mode == 'train':
+ return os.path.join(self.data_dir,
+ 'ch4_training_localization_transcription_gt')
+ elif self.mode == 'val':
+ return os.path.join(self.data_dir,
+ 'ch4_test_localization_transcription_gt')
+
+
+def get_image_blob(roidb, mode):
+ """Builds an input blob from the images in the roidb at the specified
+ scales.
+ """
+ if mode == 'train' or mode == 'val':
+ with open(roidb['image'], 'rb') as f:
+ data = f.read()
+ data = np.frombuffer(data, dtype='uint8')
+ img = cv2.imdecode(data, 1)
+ img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
+ gt_boxes = roidb['boxes']
+ gt_label = roidb['gt_classes']
+ # resize
+ if mode == 'train':
+ img, im_scale = _resize(img, target_size=800, max_size=1333)
+ need_gt_boxes = gt_boxes.copy()
+ need_gt_boxes[:, :4] *= im_scale
+ img, need_gt_boxes, need_gt_label = _rotation(
+ img, need_gt_boxes, gt_label, prob=1.0, gt_margin=1.4)
+ else:
+ img, im_scale = _resize(img, target_size=1000, max_size=1778)
+ need_gt_boxes = gt_boxes
+ need_gt_label = gt_label
+ img = img.astype(np.float32, copy=False)
+ img = img / 255.0
+ mean = np.array(cfg.pixel_means)[np.newaxis, np.newaxis, :]
+ std = np.array(cfg.pixel_std)[np.newaxis, np.newaxis, :]
+ img -= mean
+ img /= std
+ img = img.transpose((2, 0, 1))
+ return img, im_scale, need_gt_boxes, need_gt_label
+
+
+def _get_size_scale(w, h, min_size, max_size=None):
+ size = min_size
+ scale = 1.0
+ if max_size is not None:
+ min_original_size = float(min((w, h)))
+ max_original_size = float(max((w, h)))
+ if max_original_size / min_original_size * size > max_size:
+ size = int(round(max_size * min_original_size / max_original_size))
+ if (w <= h and w == size) or (h <= w and h == size):
+ return (h, w), scale
+ if w < h:
+ ow = size
+ oh = int(size * h / w)
+ scale = size / w
+ else:
+ oh = size
+ ow = int(size * w / h)
+ scale = size / h
+ scale = ow / w
+ return (oh, ow), scale
+
+
+def _resize(im, target_size=800, max_size=1333):
+ if not isinstance(im, np.ndarray):
+ raise TypeError("{}: image type is not numpy.")
+ if len(im.shape) != 3:
+ raise ImageError('{}: image is not 3-dimensional.')
+ im_shape = im.shape
+ im_size_min = np.min(im_shape[0:2])
+ im_size_max = np.max(im_shape[0:2])
+ selected_size = target_size
+ if float(im_size_min) == 0:
+ raise ZeroDivisionError('min size of image is 0')
+ if max_size != 0:
+ im_scale = float(selected_size) / float(im_size_min)
+ # Prevent the biggest axis from being more than max_size
+ if np.round(im_scale * im_size_max) > max_size:
+ im_scale = float(max_size) / float(im_size_max)
+ im_scale_x = im_scale
+ im_scale_y = im_scale
+
+ resize_w = np.round(im_scale_x * float(im_shape[1]))
+ resize_h = np.round(im_scale_y * float(im_shape[0]))
+ im_info = [resize_h, resize_w, im_scale]
+ else:
+ im_scale_x = float(selected_size) / float(im_shape[1])
+ im_scale_y = float(selected_size) / float(im_shape[0])
+
+ resize_w = selected_size
+ resize_h = selected_size
+
+ im = Image.fromarray(im)
+ im = im.resize((int(resize_w), int(resize_h)), 2)
+ im = np.array(im)
+ return im, im_scale_x
+
+
+def _rotation(image,
+ gt_boxes,
+ gt_label,
+ prob,
+ fixed_angle=-1,
+ r_range=(360, 0),
+ gt_margin=1.4):
+ rotate_range = r_range[0]
+ shift = r_range[1]
+ angle = np.array([np.max([0, fixed_angle])])
+ if np.random.rand() <= prob:
+ angle = np.array(
+ np.random.rand(1) * rotate_range - shift, dtype=np.int16)
+ '''
+ rotate image
+ '''
+ image = np.array(image)
+ (h, w) = image.shape[:2]
+ scale = 1.0
+ # set the rotation center
+ center = (w / 2, h / 2)
+ # anti-clockwise angle in the function
+ M = cv2.getRotationMatrix2D(center, angle, scale)
+ image = cv2.warpAffine(image, M, (w, h))
+ # back to PIL image
+ im_width, im_height = w, h
+ '''
+ rotate boxes
+ '''
+ need_gt_boxes = gt_boxes.copy()
+ origin_gt_boxes = need_gt_boxes
+ rotated_gt_boxes = np.empty((len(need_gt_boxes), 5), dtype=np.float32)
+ # anti-clockwise to clockwise arc
+ cos_cita = np.cos(np.pi / 180 * angle)
+ sin_cita = np.sin(np.pi / 180 * angle)
+ # clockwise matrix
+ rotation_matrix = np.array([[cos_cita, sin_cita], [-sin_cita, cos_cita]])
+ pts_ctr = origin_gt_boxes[:, 0:2]
+ pts_ctr = pts_ctr - np.tile((im_width / 2, im_height / 2),
+ (gt_boxes.shape[0], 1))
+ pts_ctr = np.array(np.dot(pts_ctr, rotation_matrix), dtype=np.int16)
+ pts_ctr = np.squeeze(
+ pts_ctr, axis=-1) + np.tile((im_width / 2, im_height / 2),
+ (gt_boxes.shape[0], 1))
+ origin_gt_boxes[:, 0:2] = pts_ctr
+ len_of_gt = len(origin_gt_boxes)
+ # rectificate the angle in the range of [-45, 45]
+ for idx in range(len_of_gt):
+ ori_angle = origin_gt_boxes[idx, 4]
+ height = origin_gt_boxes[idx, 3]
+ width = origin_gt_boxes[idx, 2]
+ # step 1: normalize gt (-45,135)
+ if width < height:
+ ori_angle += 90
+ width, height = height, width
+ # step 2: rotate (-45,495)
+ rotated_angle = ori_angle + angle
+ # step 3: normalize rotated_angle (-45,135)
+ while rotated_angle > 135:
+ rotated_angle = rotated_angle - 180
+ rotated_gt_boxes[idx, 0] = origin_gt_boxes[idx, 0]
+ rotated_gt_boxes[idx, 1] = origin_gt_boxes[idx, 1]
+ rotated_gt_boxes[idx, 3] = height * gt_margin
+ rotated_gt_boxes[idx, 2] = width * gt_margin
+ rotated_gt_boxes[idx, 4] = rotated_angle
+ x_inbound = np.logical_and(rotated_gt_boxes[:, 0] >= 0,
+ rotated_gt_boxes[:, 0] < im_width)
+ y_inbound = np.logical_and(rotated_gt_boxes[:, 1] >= 0,
+ rotated_gt_boxes[:, 1] < im_height)
+ inbound = np.logical_and(x_inbound, y_inbound)
+ need_gt_boxes = rotated_gt_boxes[inbound]
+ need_gt_label = gt_label.copy()
+ need_gt_label = need_gt_label[inbound]
+ return image, need_gt_boxes, need_gt_label
+
+
+def prep_im_for_blob(im, pixel_means, target_size, max_size):
+ """Prepare an image for use as a network input blob. Specially:
+ - Subtract per-channel pixel mean
+ - Convert to float32
+ - Rescale to each of the specified target size (capped at max_size)
+ Returns a list of transformed images, one for each target size. Also returns
+ the scale factors that were used to compute each returned image.
+ """
+ im = im.astype(np.float32, copy=False)
+ im -= pixel_means
+
+ im_shape = im.shape
+ im_size_min = np.min(im_shape[0:2])
+ im_size_max = np.max(im_shape[0:2])
+ im_scale = float(target_size) / float(im_size_min)
+ # Prevent the biggest axis from being more than max_size
+ if np.round(im_scale * im_size_max) > max_size:
+ im_scale = float(max_size) / float(im_size_max)
+ im = cv2.resize(
+ im,
+ None,
+ None,
+ fx=im_scale,
+ fy=im_scale,
+ interpolation=cv2.INTER_LINEAR)
+ im_height, im_width, channel = im.shape
+ channel_swap = (2, 0, 1) #(batch, channel, height, width)
+ im = im.transpose(channel_swap)
+ return im, im_scale
diff --git a/PaddleCV/rrpn/edict.py b/PaddleCV/rrpn/edict.py
new file mode 100755
index 00000000..552ede8e
--- /dev/null
+++ b/PaddleCV/rrpn/edict.py
@@ -0,0 +1,37 @@
+# Copyright (c) 2019 PaddlePaddle Authors. All Rights Reserve.
+#
+#Licensed under the Apache License, Version 2.0 (the "License");
+#you may not use this file except in compliance with the License.
+#You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+#Unless required by applicable law or agreed to in writing, software
+#distributed under the License is distributed on an "AS IS" BASIS,
+#WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#See the License for the specific language governing permissions and
+#limitations under the License.
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+from __future__ import unicode_literals
+
+
+class AttrDict(dict):
+ def __init__(self, *args, **kwargs):
+ super(AttrDict, self).__init__(*args, **kwargs)
+
+ def __getattr__(self, name):
+ if name in self.__dict__:
+ return self.__dict__[name]
+ elif name in self:
+ return self[name]
+ else:
+ raise AttributeError(name)
+
+ def __setattr__(self, name, value):
+ if name in self.__dict__:
+ self.__dict__[name] = value
+ else:
+ self[name] = value
diff --git a/PaddleCV/rrpn/eval.py b/PaddleCV/rrpn/eval.py
new file mode 100755
index 00000000..bf773207
--- /dev/null
+++ b/PaddleCV/rrpn/eval.py
@@ -0,0 +1,91 @@
+# Copyright (c) 2019 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 cv2
+import time
+import numpy as np
+import pickle
+import paddle
+import paddle.fluid as fluid
+import reader
+import models.model_builder as model_builder
+import models.resnet as resnet
+import checkpoint as checkpoint
+from config import cfg
+from utility import print_arguments, parse_args, check_gpu
+from data_utils import DatasetPath
+from eval_helper import *
+import logging
+FORMAT = '%(asctime)s-%(levelname)s: %(message)s'
+logging.basicConfig(level=logging.INFO, format=FORMAT)
+logger = logging.getLogger(__name__)
+
+
+def eval():
+
+ place = fluid.CUDAPlace(0) if cfg.use_gpu else fluid.CPUPlace()
+ exe = fluid.Executor(place)
+ image_shape = [3, cfg.TEST.max_size, cfg.TEST.max_size]
+ class_nums = cfg.class_num
+ model = model_builder.RRPN(
+ add_conv_body_func=resnet.ResNet(),
+ add_roi_box_head_func=resnet.ResNetC5(),
+ use_pyreader=False,
+ mode='val')
+
+ startup_prog = fluid.Program()
+ infer_prog = fluid.Program()
+ with fluid.program_guard(infer_prog, startup_prog):
+ with fluid.unique_name.guard():
+ model.build_model(image_shape)
+ pred_boxes = model.eval_bbox_out()
+ infer_prog = infer_prog.clone(True)
+ exe.run(startup_prog)
+
+ # yapf: disable
+ def if_exist(var):
+ return os.path.exists(os.path.join(cfg.pretrained_model, var.name))
+ if cfg.pretrained_model:
+ checkpoint.load_params(exe, infer_prog, cfg.pretrained_model)
+ # yapf: enable
+ test_reader = reader.test(1)
+ feeder = fluid.DataFeeder(place=place, feed_list=model.feeds())
+
+ fetch_list = [pred_boxes]
+ res_list = []
+ keys = [
+ 'bbox', 'gt_box', 'gt_class', 'is_crowed', 'im_info', 'im_id',
+ 'is_difficult'
+ ]
+ for i, data in enumerate(test_reader()):
+ im_info = [data[0][1]]
+ result = exe.run(infer_prog,
+ fetch_list=[v.name for v in fetch_list],
+ feed=feeder.feed(data),
+ return_numpy=False)
+ pred_boxes_v = result[0]
+ nmsed_out = pred_boxes_v
+ outs = np.array(nmsed_out)
+ res = get_key_dict(outs, data[0], keys)
+ res_list.append(res)
+ if i % 50 == 0:
+ logger.info('test_iter {}'.format(i))
+ icdar_eval(res_list)
+
+
+if __name__ == '__main__':
+ args = parse_args()
+ print_arguments(args)
+ check_gpu(args.use_gpu)
+ eval()
diff --git a/PaddleCV/rrpn/eval_helper.py b/PaddleCV/rrpn/eval_helper.py
new file mode 100755
index 00000000..c9e66e67
--- /dev/null
+++ b/PaddleCV/rrpn/eval_helper.py
@@ -0,0 +1,379 @@
+# Copyright (c) 2019 PaddlePaddle Authors. All Rights Reserve.
+#
+#Licensed under the Apache License, Version 2.0 (the "License");
+#you may not use this file except in compliance with the License.
+#You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+#Unless required by applicable law or agreed to in writing, software
+#distributed under the License is distributed on an "AS IS" BASIS,
+#WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#See the License for the specific language governing permissions and
+#limitations under the License.
+
+import os
+import numpy as np
+import paddle.fluid as fluid
+import math
+from config import cfg
+import six
+import numpy as np
+import cv2
+import Polygon as plg
+from PIL import Image
+from PIL import ImageDraw
+from PIL import ImageFont
+from config import cfg
+import logging
+logger = logging.getLogger(__name__)
+
+
+def get_key_dict(out, data, key):
+ res = {}
+ for i in range(len(key)):
+ if i == 0:
+ res[key[i]] = out
+ else:
+ res[key[i]] = data[i]
+ return res
+
+
+def get_labels_maps():
+ default_labels_maps = {1: 'text'}
+ if cfg.dataset == 'icdar2015':
+ return default_labels_maps
+
+ labels_map = {}
+ with open(os.path.join(cfg.data_dir, 'label_list')) as f:
+ lines = f.readlines()
+ for idx, line in enumerate(lines):
+ labels_map[idx + 1] = line.strip()
+ return labels_map
+
+
+def draw_bounding_box_on_image(image_path,
+ image_name,
+ nms_out,
+ im_scale,
+ draw_threshold=0.8):
+ #if image is None:
+ image = Image.open(os.path.join(image_path, image_name))
+ draw = ImageDraw.Draw(image)
+ im_width, im_height = image.size
+
+ labels_map = get_labels_maps()
+ for dt in np.array(nms_out):
+ num_id, score = dt.tolist()[:2]
+ x1, y1, x2, y2, x3, y3, x4, y4 = dt.tolist()[2:] / im_scale
+ if score < draw_threshold:
+ continue
+ draw.line(
+ [(x1, y1), (x2, y2), (x3, y3), (x4, y4), (x1, y1)],
+ width=2,
+ fill='red')
+ if image.mode == 'RGB':
+ draw.text((x1, y1), labels_map[num_id], (255, 255, 0))
+ print("image with bbox drawed saved as {}".format(image_name))
+ image.save(image_name)
+
+
+def polygon_from_points(points):
+ """
+ Returns a Polygon object to use with the Polygon2 class from a list of 8 points: x1,y1,x2,y2,x3,y3,x4,y4
+ """
+ res_boxes = np.empty([1, 8], dtype='int32')
+ res_boxes[0, 0] = int(points[0])
+ res_boxes[0, 4] = int(points[1])
+ res_boxes[0, 1] = int(points[2])
+ res_boxes[0, 5] = int(points[3])
+ res_boxes[0, 2] = int(points[4])
+ res_boxes[0, 6] = int(points[5])
+ res_boxes[0, 3] = int(points[6])
+ res_boxes[0, 7] = int(points[7])
+ point_mat = res_boxes[0].reshape([2, 4]).T
+ return plg.Polygon(point_mat)
+
+
+def clip_box(bbox, im_info):
+
+ h = im_info[0]
+ w = im_info[1]
+ res = []
+ for b in bbox:
+ pts = b.reshape(4, 2)
+ pts[np.where(pts < 0)] = 1
+ pts[np.where(pts[:, 0] > w), 0] = w - 1
+ pts[np.where(pts[:, 1] > h), 1] = h - 1
+ pts = pts.reshape(-1)
+ pts /= im_info[2]
+ res.append(pts)
+
+ return np.array(res)
+
+
+def get_union(det, gt):
+ area_det = det.area()
+ area_gt = gt.area()
+ return area_det + area_gt - get_intersection(det, gt)
+
+
+def get_intersection_over_union(det, gt):
+ try:
+ return get_intersection(det, gt) / get_union(det, gt)
+ except:
+ return 0
+
+
+def get_intersection(det, gt):
+ inter = det & gt
+ if len(inter) == 0:
+ return 0
+ return inter.area()
+
+
+def parse_gt(result, im_id):
+ for res in result:
+ if res['im_id'] == im_id:
+ gt_boxes = list(res['gt_box'])
+ gt_class = res['gt_class']
+ is_difficult = res['is_difficult'].reshape(-1)
+ objects = []
+ for i in range(len(gt_boxes)):
+ object_struct = {}
+ object_struct['bbox'] = gt_boxes[i]
+ object_struct['class'] = gt_class[i]
+ if is_difficult[i] == 1:
+ object_struct['difficult'] = 1
+ else:
+ object_struct['difficult'] = 0
+ object_struct['im_id'] = im_id
+ objects.append(object_struct)
+ return objects
+
+
+def calculate_ap(rec, prec):
+ # 11 point metric
+ ap = 0.
+ for t in np.arange(0., 1.1, 0.1):
+ if np.sum(rec >= t) == 0:
+ p = 0
+ else:
+ p = np.max(prec[rec >= t])
+ ap = ap + p / 11.
+ return ap
+
+
+def icdar_map(result, class_name, ovthresh):
+ im_ids = []
+ for res in result:
+ im_ids.append(res['im_id'])
+ recs = {}
+
+ for i, im_id in enumerate(im_ids):
+ recs[str(im_id)] = parse_gt(result, im_id)
+ class_recs = {}
+ npos = 0
+ for k in im_ids:
+ res = [obj for obj in recs[str(k)] if obj['class'] == class_name]
+ bbox = np.array([x['bbox'] for x in res])
+ difficult = np.array([x['difficult'] for x in res]).astype(np.bool)
+ det = [False] * len(res)
+ npos = npos + sum(~difficult)
+ class_recs[k] = {'bbox': bbox, 'difficult': difficult, 'det': det}
+ image_ids = []
+ confidence = []
+ bbox = []
+ for res in result:
+ im_info = res['im_info']
+ pred_boxes = res['bbox']
+ for box in pred_boxes:
+ if box[0] == class_name:
+ image_ids.append(res['im_id'])
+ confidence.append(box[1])
+ clipd_box = clip_box(box[2:].reshape(-1, 8), im_info)
+ bbox.append(clipd_box[0])
+ confidence = np.array(confidence)
+ sorted_ind = np.argsort(-confidence)
+ sorted_scores = np.sort(-confidence)
+ bbox = np.array(bbox)
+ bbox = bbox[sorted_ind, :]
+ image_ids = [image_ids[x] for x in sorted_ind]
+ nd = len(image_ids)
+ tp = np.zeros(nd)
+ fp = np.zeros(nd)
+ for d in range(nd):
+ res = class_recs[image_ids[d]]
+ bb = bbox[d, :].astype(float)
+ ovmax = -np.inf
+ gt_bbox = res['bbox'].astype(float)
+ if gt_bbox.size > 0:
+ # compute overlaps
+ gt_bbox_xmin = np.min(gt_bbox[:, 0::2], axis=1)
+ gt_bbox_ymin = np.min(gt_bbox[:, 1::2], axis=1)
+ gt_bbox_xmax = np.max(gt_bbox[:, 0::2], axis=1)
+ gt_bbox_ymax = np.max(gt_bbox[:, 1::2], axis=1)
+ bb_xmin = np.min(bb[0::2])
+ bb_ymin = np.min(bb[1::2])
+ bb_xmax = np.max(bb[0::2])
+ bb_ymax = np.max(bb[1::2])
+
+ ixmin = np.maximum(gt_bbox_xmin, bb_xmin)
+ iymin = np.maximum(gt_bbox_ymin, bb_ymin)
+ ixmax = np.minimum(gt_bbox_xmax, bb_xmax)
+ iymax = np.minimum(gt_bbox_ymax, bb_ymax)
+ iw = np.maximum(ixmax - ixmin + 1., 0.)
+ ih = np.maximum(iymax - iymin + 1., 0.)
+ inters = iw * ih
+
+ # union
+ uni = ((bb_xmax - bb_xmin + 1.) * (bb_ymax - bb_ymin + 1.) +
+ (gt_bbox_xmax - gt_bbox_xmin + 1.) *
+ (gt_bbox_ymax - gt_bbox_ymin + 1.) - inters)
+
+ overlaps = inters / uni
+ gt_bbox_keep_mask = overlaps > 0
+ gt_bbox_keep = gt_bbox[gt_bbox_keep_mask, :]
+ gt_bbox_keep_index = np.where(overlaps > 0)[0]
+
+ def calcoverlaps(gt_bbox_keep, bb):
+ overlaps = []
+ for index, _ in enumerate(gt_bbox_keep):
+ p_g = polygon_from_points(gt_bbox_keep[index])
+ p_d = polygon_from_points(bb)
+ overlap = get_intersection_over_union(p_d, p_g)
+ overlaps.append(overlap)
+ return overlaps
+
+ if len(gt_bbox_keep) > 0:
+ overlaps = calcoverlaps(gt_bbox_keep, bb)
+
+ ovmax = np.max(overlaps)
+ jmax = np.argmax(overlaps)
+ jmax = gt_bbox_keep_index[jmax]
+
+ if ovmax > ovthresh:
+ if not res['difficult'][jmax]:
+ if not res['det'][jmax]:
+ tp[d] = 1.
+ res['det'][jmax] = 1
+ else:
+ fp[d] = 1.
+ else:
+ fp[d] = 1.
+ # compute precision recall
+ fp = np.cumsum(fp)
+ tp = np.cumsum(tp)
+
+ rec = tp / float(npos)
+ prec = tp / np.maximum(tp + fp, np.finfo(np.float64).eps)
+ ap = calculate_ap(rec, prec)
+ return rec, prec, ap
+
+
+def icdar_map_eval(result, num_class):
+ map = 0
+ for i in range(num_class - 1):
+ rec, prec, ap = icdar_map(result, i + 1, ovthresh=0.5)
+ map = map + ap
+ map = map / (num_class - 1)
+ logger.info('mAP {}'.format(map))
+
+
+def icdar_box_eval(result, thresh):
+
+ matched_sum = 0
+ num_global_care_gt = 0
+ num_global_care_det = 0
+ for res in result:
+ im_info = res['im_info']
+ h = im_info[1]
+ w = im_info[2]
+ gt_boxes = res['gt_box']
+ pred_boxes = res['bbox']
+ pred_boxes = pred_boxes[np.where(pred_boxes[:, 1] > thresh)]
+ pred_boxes = pred_boxes[:, 2:]
+ pred_boxes = clip_box(pred_boxes, im_info)
+
+ is_difficult = res['is_difficult']
+ det_matched = 0
+
+ iou_mat = np.empty([1, 1])
+
+ gt_pols = []
+ det_pols = []
+
+ gt_pol_points = []
+ det_pol_points = []
+
+ gt_dont_care_pols_num = []
+ det_dont_care_pols_num = []
+
+ det_matched_nums = []
+
+ points_list = list(gt_boxes)
+
+ dony_care = is_difficult.reshape(-1)
+ for i, points in enumerate(points_list):
+ gt_pol = polygon_from_points(list(points))
+ gt_pols.append(gt_pol)
+ gt_pol_points.append(list(points))
+ if dony_care[i] == 1:
+ gt_dont_care_pols_num.append(len(gt_pols) - 1)
+ for i, points in enumerate(pred_boxes):
+ points = list(points.reshape(8).astype(np.int32))
+ det_pol = polygon_from_points(points)
+ det_pols.append(det_pol)
+ det_pol_points.append(points)
+ if len(gt_dont_care_pols_num) > 0:
+ for dont_care_pol in gt_dont_care_pols_num:
+ dont_care_pol = gt_pols[dont_care_pol]
+ intersected_area = get_intersection(dont_care_pol, det_pol)
+ pd_dimensions = det_pol.area()
+ precision = 0 if pd_dimensions == 0 else intersected_area / pd_dimensions
+ if (precision > 0.5):
+ det_dont_care_pols_num.append(len(det_pols) - 1)
+ break
+ if len(gt_pols) > 0 and len(det_pols) > 0:
+ # Calculate IoU and precision matrixs
+ output_shape = [len(gt_pols), len(det_pols)]
+ iou_mat = np.empty(output_shape)
+ gt_rect_mat = np.zeros(len(gt_pols), np.int8)
+ det_rect_mat = np.zeros(len(det_pols), np.int8)
+ for gt_num in range(len(gt_pols)):
+ for det_num in range(len(det_pols)):
+ p_d = gt_pols[gt_num]
+ p_g = det_pols[det_num]
+ iou_mat[gt_num, det_num] = get_intersection_over_union(p_d,
+ p_g)
+
+ for gt_num in range(len(gt_pols)):
+ for det_num in range(len(det_pols)):
+ if gt_rect_mat[gt_num] == 0 and det_rect_mat[
+ det_num] == 0 and gt_num not in gt_dont_care_pols_num and det_num not in det_dont_care_pols_num:
+ if iou_mat[gt_num, det_num] > 0.5:
+ gt_rect_mat[gt_num] = 1
+ det_rect_mat[det_num] = 1
+ det_matched += 1
+ det_matched_nums.append(det_num)
+ num_gt_care = (len(gt_pols) - len(gt_dont_care_pols_num))
+ num_det_care = (len(det_pols) - len(det_dont_care_pols_num))
+ matched_sum += det_matched
+ num_global_care_gt += num_gt_care
+ num_global_care_det += num_det_care
+ method_recall = 0 if num_global_care_gt == 0 else float(
+ matched_sum) / num_global_care_gt
+ method_precision = 0 if num_global_care_det == 0 else float(
+ matched_sum) / num_global_care_det
+ method_hmean = 0 if method_recall + method_precision == 0 else 2 * method_recall * method_precision / (
+ method_recall + method_precision)
+ logger.info('Recall {}'.format(method_recall))
+ logger.info('Precision {}'.format(method_precision))
+ logger.info('F1 {}'.format(method_hmean))
+
+
+def icdar_eval(result):
+ if cfg.dataset == 'icdar2015':
+ icdar_box_eval(result, 0.8)
+ else:
+ icdar_map_eval(result, cfg.class_num)
diff --git a/PaddleCV/rrpn/image/img_119.jpg b/PaddleCV/rrpn/image/img_119.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..01feb6de6bb67ecc39db6dfc041c01306e46a50a
GIT binary patch
literal 170138
zcmbT7Wl&r}uJv1pq+(djNko01^O1cz6VOI79>l1SBLxWK>)<
zR1_3cLTnriToNKOQW7E{ker&8j+~O23J9dlZd00Tt{fX0G?!Giib1R(o2PB^IlG{FBdC}uqoKZ;Biz<5hz`7If9c45vjx*
z`|;G~Zm2oUTtkqMKj9M)64B7o(K9e|ar5x<@e4>uN`a(hWaZQ~G_|yKboI7odbhI!y}_(
z;}i1>i%ZKZt843f`v-?d$0xr|&u;JTA0D5cUtZt-gA3{(=l{0<3HJYRVg2KRhJ}TJ
zMfeXcC}^*L4ub^?N5Kw{EvAZK>ViYb5sZi{o>bV_k3_|(c7tc;I*0s;nrn~d_CL`6
zlkC3+7V`f^_J6?sFV{K%6$a|x&x64Nhyo6AvNms?lKtxJRM&I~mbGB7+`<7Kv%dXW
zb=mz{7kcPjB!_e9ahj
zEI%c_;NV9?S-L7G*44?PBc4Uq`j2h5zG~;;lKxgFCtBAvO3A?VJ7DzWGWX|8rjZOu
zEX09bw}e37sw`bg^n2LZn`HepotW%vk{o1b9g;U$kFUwb2+?&o~
zqpktD_Q-<+%g@M!PNk@WiNypJk3Uek+fs;7%92t;QcVD(dup$S$)u$fkAhu@v=BNG
zeT2BxJ8eEH(#%>#kv~ik?I|4JhKjHeIP^)2iA7nYVf
z+EY~TtEUFJkDYpY$J$fPMSQa~%KUhXFTSgKHfiUvoC)kb<0r3XMSgfVTG7WFLsjcT
z%g&M(8gNj50bfa#+y4TPt33_~zphHNX7k*v3Z@iLtzu=R!cTOpXhVG?bgH9Nns4}R
zxAi>^ES*$WoY=5O6%ImJOxAVVbfP@`sHND{UJ!Y7iBVRub|_?bAl;#(hQ<&%t{|aIZOP9}lZM`c7w;f9;~_
zSYN9NMq+13xm+#aB`sfX4_o3UD0X-TSAhyD++v^lrtP*iZ4+&vjrnBV&k!xmbVTc0
zxp9@*AnOml?$11(mfbc<5k5Mx-#+~33=FbD8o)NYasDc?p?dbe^V};Bdg5Iw)
z12V)x9O&$i(5=&!OFi(4TI}v_g2z;p
z%WMqBxA*6&Ik?}|DVY_SOfXXUcm0spe?FYnTiaSL1q{QQaE9#k!YP4-tH4_Zwv^|}
zci*bPo^n&JDxo9ujA3y;oUgMS>2cmHiwWmS*X@+g8_Q?=`%GPxn^SuMb5w~uFmY1|
z4W~xIrFL0dNHlGE1KZp9a9b&fhwu62>PE)qT;pCOhdQ?89_jY=fY3Z&7yZuq*w5Dm
zJXa2>K+ENIj^Tt}*&^j^kgm1b;>GaNR9fuv?F3JTOK6aQFX{>n$*-`c6mg_o(k0=V
zeCwQTMH5FBb@JjIw-HV)E58miv!8nFLkE{z&AZF@xLSpbQ|V%J(porl1(BNscnF}*MQt0`(duaKmUR?1+2t0z>(`@B)xjm6k9$88*>)rE(TLYF1IE01|
z_V=cBWetCg2ja$W8f)prDB7Y(_;jf$?F8Y
zQU`+62|~GcVGIS0^tL(bpZzw+wrdR&f&pB~3TQIiVNm$qkrg9H!nRubBR;p()sTh7
zWtTZCm1%cLE?)N{otGJ1f)uM>B}RN5q8RQrK=NX&CtF4ut}d5Lz0L&K>QdfQneh0u~~s5Lb5nD)@2-)Th<&g5A|@$uw`y+Bd6bY}Z-z5czPg)s(*=2Bx^ZVly4k
zNJCZKcBV*mA4|}Eb#AqL`P~DJS%s5xfN)>4TH4hBv9|YO<1~>O^ZWxJQ~4F2>+*
z$t%+|Dl=lPqt`TFOapDOW=}4~1hi}0V8=*1SwDd=nKx3$^$8+Vi$b5`)}7yND)skI*TxHmM5qPRij7e5?we-c(q@k?1fO)Ah>v=uP>h$6(rF3qUB!KL9>e
z3yg+2an*8vKA~^?gm}y8=rJpiH0;43h*-h8PWbUkWAU!>?bQPH`DDFX-t+V)wDcCY
z?zXcLNX;A>7n6@Ph)j=pj;KW9_OG
z7F+4@Ou3?F0|MUn%EQ>C{i2%Z&-t>zGD0O^DI<D;mB@!cJvgz#LElXKD62_Z{4X{D&$F1aifjom!8Ge
zTT`uV-Vo|var)kP*Nk0Yn-HWECmg5}vbsN~kC`&dxn4-iZLGIxQ<%NKYfE%T5XZ!g
zYz-xj6FYv{1SX7GpBgHEFe7oJF^~^zY$ion0O9`vycW18rGy~Pfh8QR&ENVw_@8Rp
z$#J^JyNf~29oLEz@8LC0GV@y#Gkt=2VPvUNB7zzqK35Pw-;+OC;vF56w3A&HzsxS9wB`zdJ}Snk8e
zC{0+m?C0i1?Oeif<%x_Ni|t=A&xK>YUpB|8Dv6!U46hpZ;f~N_U~$0&WR9?Rd;gUQ
za%qoesB?vyak(}s-M6_E615_IT3+8L%qSWXIJvzvvgR`T3sBpzgdU-F0%LAsjI=4Y
ze_uD$FL2^zqS(`Ks7u=)V4x7gsrYX58hmcx|Jc&TnL#H3j9GHaUj{E<>3hQS@{tWF
z!fjN7hG#viLbPT@dTJ*a(vI57g0{J%pDY$X!p(F)
z)(Aiv4_Bh>FVXEu#xFmHq#h3M+`W;++$e99Jym=mF$I-%z+f#7U4cp7tp1iiS70Jf9@TChJDF$5B|&
z@9QD2YeV6@dh@-gfZ5M!ZKnjvtT^q~iwSy%t>=FMmwu<$1wr})h{%4B>vExqceI
zoNuR39_sCfId~7{1E|d)Xs|$z%HDp@4yBV)v6qBa?U9COmf|S?X`iSZgatLK$ihB>
z=aL$XD}o8eMll=u$JLPQm?qJwzppSm
zD?2i_{#d8Ub?E&`6)zFn_!>x5PaOuXQu^s#5nHNcoTN{?T=Dhs2fU5vy2ImodDLFk
zIv4ij5$oXf_!z*e>bu6G6ZmR-+g>-Ym6}6%bH|60aJ9r8NSb-TeT*b>hfWCAo!VJYgMseb#=OuYsJ%MTc1FvR0x1;UQh0(=1m6bYH9hd
zz3`|h{~mh8QY|jW%Qpyt=uI`mB+qY&3wD&0AoHlhMkdLIM2;yJgQB`UPr?PDKiAn^9QN=D%aI#yO
z=|)k5UG1hjtok4N%1Kvb);tFK!ABOk%Aqi9_>6x!0Jl
zI$$OLw`Jb&4-(7LVYH(~B$`*i)uyg`z0%sr2UBs3W<%zo%r!X+;UL;Ubc=zCoz|zf
z+OTCW`g&lM_Az1qBk6v3;D`uj&(G-1sJwG!>d%>wjHrvjg`a-`kJYQ%>Wx0;QLKAj
ziF=wroMVUP%H?6+aedEJ<%f
z&5QLA!`^T6%UOhM(t(bew1qAnz-W0SXYt?Ya7b*lOXb0>NQDbqdeeqs1pKd@
zqufbMO=BH!F*mrri&{sLeppSuV8$W!X|$cJJJ1m8(yNc27ooXQn=LnDp?6Vdo-C}(
z22R}2?f9ipZrO*x{PE8`G^Npr?$oAvX;xi;5S%dJ>1`!reP(&Z#c;@Tk~j+QyBWIu
z=I$+Pl5mTgfA1Ddg`h$xqd#I9SJFLEQr77QVWHSQi7yjVCJ6=O)G&Mdt~Ctz1hg1l
zbKyC!0BtGWV{v#U2fQyZR@?55dc1w9W&*7g
z%;yl9>r;dJ@ay7Rgnx&!^9a9~prgeck*(3BOG8N|lx|ZQtX*ioTZ5&~{$AV(Ny50Q
zDk`c{xPbqz&x=>m6&x<3m-|)pv?Mq|LBf^aM)}+xNU95J9Ly)QWSvs=rbRAHz=rP@
z?jxLvNI6$YwzS1QG>`9Bt&vx@B)zvJ2=kTa>*r_B)oAUM^E`cg@1rZHTCMLt+Qght
z(P16Myj2`D$yTDv9f)<;xW_c_k|vJ2vep^jnux6&hVnG_B>ZQ3k88xI5VBbn{BP`9
zTxy!eh586aWa&!zw@f34Arya1)-F%=D8G8X1yq}x+e33=QJ);anw3SIt)Ht!P`-od
zy;%QbHz%CGX#>yJdp?XUqC9K-Ih@;p$)}GM0V|cOcJ{cuaAkXHT!ur*>)(}lENOjR
z@~{|(EXAl$!E3Z=%A_+(P}iOS
zdG_i9TD2Y}o6Uq;wyXOAF9$2zP0_I)9SSGh!s7crh>3f}<_S0Tpb%5PQprw>Y}^7>
zD%2F1ShA<@r9J^wI!2oj2-9&BB7TX^ioUeDZVsitwCtS(Hv6c6S5PxbvZMRnMRvQf
zJraYMrwufl_+AbI?aW(Vq$gBY&t+%N>C@%%z@nGrw)MlfH<`h0{V%JcX7%!*Wbi3{cShtJ|@en9r1z1?I!MvA=4#5d3X7flyFvn_#>hl&P^S0z_9gkc|pc=WP0Op$E#!2`^
z)m82Cm^(Yo?T=&~tDGnLEb?9XM8hp7(?!2koXMO^j9r>WK?_Oq*&^Qy
zDnz_zAFWkO^YAZ#CiGj<_KbBD3m2}!p-+kDFSUx~B03a+VK6&?_FHUN&y}I}lv48$
zzqgD!{Uti9H!bVBhj`}uhTLrJp7uP2?|BFdT^kZegzqe%Kdmm)G)Jr$*^%0)-#4JuUZFdB
zMu#%O!{p#O{0&>AhcR^(wI|ltpm@umb?IUPSKy=2WkJYIli6Wk(QiDB(}NNy4a<_x
z-evJHTI=rBhGmZx<-UOsH*c_Im3lDM
zN+_l6KWm4Ka2b|yjpzeU>U>Uz-H2wIOE=|F^bHMCqChC9UA&dg&xN)f=+8eh3?kh(
zT)~5fghOBnHo{GGCE;o^!zIh*R%Ul5-q38)do`v8E-v|
zRg90{$(Ps=KDzA|At^O-E635iuP%C4YVi>s>>vaRfgSTcK72>@sxfXJgte)=sHfX@
z>{Eq1Ty7$dINBrby=xOCN_m8-k(X|0vDb_~x?-hZ961(_EQqfXJB)7Ko802DciEek
z+oQAaVB=^+{_x+L@D0sFy1q0xm$`PWS@BW>@Jen#P@m7&+rH7cIv_d7ZLcGe45i&q
z1Bl~&79bROP-;G}he|dO(tRjlYboF2`?{Rzb#`>1|
zrBC#??@ssP>{=b|!lR(B9yi^Df9-6+y4agFf4$JsH`Qu8Sf-hnAPrDu!t8d(5&Yw4
z38E*iZm?%$(Ai6zFDiQZZB?UD_#)g;W8^x`K#6%}wOeSJ9%XgF>&zDt*?%S4nU<6|
zQzA{MNsAD{h4{#1MYDVjHxEBr+~2Dmgrxgz7l(|66R$4f&a}pzy~QNsRX3X(>=vmh
zlL^@{1SRqvEL94lRC)w%k+C?O=<7*}^ACBa>>7S~!E@?u~Wr?4t(hQ5J6T|2Vg^=Avw5Mvjre{f0RroEb
z*+v!AiA(WE+d<+uK}TK3R};f0=PP*VS6#C9?q|W_>9j6zMJbgUeF~
zw}bj(HE6&wUAZ0O$2Wr~U&RSoe5%q54~a9nWFevy>Bwzg-u!1q8%hG$LC2CjI-mOh8@5Ohz3siP3&S4EzV1zU`B;G(gB?XO
zw$6r6az>E+b)SddQ4~k(dYAI}Gke4-yrHz9J-=*hKzWT+k0fu|~G+n9JYTlDE+MZEGZLuAO@1LY#Q^xKQ#aVpNDYCo
zo+TYR5*9Tt)U6zHntcM)DyMwb1^3Gq_VuT=1g)(Se6Clxu{wF4vBi)ZkW%^2RVFJr
zh-J+|_&ito0U|fWt{5O6b>-%Nv)yqeEoH1+S-U00)#E=M1!=aVCppT5zaYDMp7wdj
zqX@Z|_olSP%Fo2C*LqtXPLKLZ-WI}Cjh_Y^YS+QHMd#|bSY7nMKdnBP<*toWgDp=w
z;2E?AyTY5D%W-5-sXZbsEajfSG3!pFg#Z;(Aryd@%l2-;fx&WU;>;En{PI**GR{+c
zE9CX$xjd&YjWZ;2Vvw)5(zIAc^AqnYYG(}ihEXq7q+X9
zY&owL9p-tOGZ=ZsATmvUk@plm>Cqgw5UBm|>KyCx{yx=ilT~G;SCGTq?a--$7{@>C
zl}?J^x-hvePgDaUnHJe>yLx%9Gi~`TFO^|OWH|;V)uaJ9!fbk$yMJQCn^JQn1Uicm
zPeKDAIH$Ksq@C%D10iFAYTp|Lp+?pVk*I@di0RsZv?0lZ^d&zANMST^CjV
zawgfmd_~=|03zapzTCOOUmzRud8obJLm|NjrJ5u|^KgE;3w9Jlo)aP0J<1|>jrj7d
zeUlfi$3>Pelgj3rCQX)G}`Xq59>92TdKK*|I
z!qs^l@ptPpC|#LqI@a$6r1^T+kN&h9N)6z3gr3Zj{w_@a&G<~=>V(cFh`o+E*!rEq
zl_Csa>=lrjPcL_jA`85#`f~VtUqIC}u_ii+KLy<_*5b{qvMHsV{qqzI)?@XPumAMC
zBD3o#s