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 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

Nug~EMa-7|TM^ekdZ4+Cgk~$1J+oFKxO<1j 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 z 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 HtPlU$SPvG9 literal 0 HcmV?d00001 diff --git a/PaddleCV/rrpn/image/img_120.jpg b/PaddleCV/rrpn/image/img_120.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8a318613b7275415599da6b79e33314b174e2172 GIT binary patch literal 120354 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

fnF) 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

@Am5QTA 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 bhCI%vqVwrTpOxi$9+{}U2YPO33m^a4S!WTk literal 0 HcmV?d00001 diff --git a/PaddleCV/rrpn/infer.py b/PaddleCV/rrpn/infer.py new file mode 100755 index 00000000..3af9d21c --- /dev/null +++ b/PaddleCV/rrpn/infer.py @@ -0,0 +1,81 @@ +# 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 data_utils import DatasetPath +from eval_helper import * +from utility import print_arguments, parse_args, check_gpu + + +def infer(): + 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='infer') + 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 + infer_reader = reader.infer(cfg.image_path) + feeder = fluid.DataFeeder(place=place, feed_list=model.feeds()) + + fetch_list = [pred_boxes] + imgs = os.listdir(cfg.image_path) + imgs.sort() + + for i, data in enumerate(infer_reader()): + result = exe.run(infer_prog, + fetch_list=[v.name for v in fetch_list], + feed=feeder.feed(data), + return_numpy=False) + nmsed_out = result[0] + im_info = data[0][1] + im_scale = im_info[2] + outs = np.array(nmsed_out) + draw_bounding_box_on_image(cfg.image_path, imgs[i], outs, im_scale, + cfg.draw_threshold) + + +if __name__ == '__main__': + args = parse_args() + print_arguments(args) + check_gpu(args.use_gpu) + infer() diff --git a/PaddleCV/rrpn/models/__init__.py b/PaddleCV/rrpn/models/__init__.py new file mode 100755 index 00000000..e69de29b diff --git a/PaddleCV/rrpn/models/ext_op/rrpn_lib.py b/PaddleCV/rrpn/models/ext_op/rrpn_lib.py new file mode 100644 index 00000000..04c11486 --- /dev/null +++ b/PaddleCV/rrpn/models/ext_op/rrpn_lib.py @@ -0,0 +1,549 @@ +# 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 paddle.fluid as fluid +from paddle.fluid.layer_helper import LayerHelper +from paddle.fluid.framework import Variable +fluid.load_op_library('models/ext_op/src/rrpn_lib.so') + + +def rrpn_target_assign(bbox_pred, + cls_logits, + anchor_box, + gt_boxes, + im_info, + rpn_batch_size_per_im=256, + rpn_straddle_thresh=0.0, + rpn_fg_fraction=0.5, + rpn_positive_overlap=0.7, + rpn_negative_overlap=0.3, + use_random=True): + """ + **Target Assign Layer for rotated region proposal network (RRPN).** + This layer can be, for given the Intersection-over-Union (IoU) overlap + between anchors and ground truth boxes, to assign classification and + regression targets to each each anchor, these target labels are used for + train RPN. The classification targets is a binary class label (of being + an object or not). Following the paper of RRPN, the positive labels + are two kinds of anchors: (i) the anchor/anchors with the highest IoU + overlap with a ground-truth box, or (ii) an anchor that has an IoU overlap + higher than rpn_positive_overlap(0.7) with any ground-truth box. Note + that a single ground-truth box may assign positive labels to multiple + anchors. A non-positive anchor is when its IoU ratio is lower than + rpn_negative_overlap (0.3) for all ground-truth boxes. Anchors that are + neither positive nor negative do not contribute to the training objective. + The regression targets are the encoded ground-truth boxes associated with + the positive anchors. + Args: + bbox_pred(Variable): A 3-D Tensor with shape [N, M, 5] represents the + predicted locations of M bounding bboxes. N is the batch size, + and each bounding box has five coordinate values and the layout + is [x, y, w, h, angle]. The data type can be float32 or float64. + cls_logits(Variable): A 3-D Tensor with shape [N, M, 1] represents the + predicted confidence predictions. N is the batch size, 1 is the + frontground and background sigmoid, M is number of bounding boxes. + The data type can be float32 or float64. + anchor_box(Variable): A 2-D Tensor with shape [M, 5] holds M boxes, + each box is represented as [x, y, w, h, angle], + [x, y] is the left top coordinate of the anchor box, + if the input is image feature map, they are close to the origin + of the coordinate system. [w, h] is the right bottom + coordinate of the anchor box, angle is the rotation angle of box. + The data type can be float32 or float64. + gt_boxes (Variable): The ground-truth bounding boxes (bboxes) are a 2D + LoDTensor with shape [Ng, 5], Ng is the total number of ground-truth + bboxes of mini-batch input. The data type can be float32 or float64. + im_info (Variable): A 2-D LoDTensor with shape [N, 3]. N is the batch size, + 3 is the height, width and scale. + rpn_batch_size_per_im(int): Total number of RPN examples per image. + The data type must be int32. + rpn_straddle_thresh(float): Remove RPN anchors that go outside the image + by straddle_thresh pixels. The data type must be float32. + rpn_fg_fraction(float): Target fraction of RoI minibatch that is labeled + foreground (i.e. class > 0), 0-th class is background. The data type must be float32. + rpn_positive_overlap(float): Minimum overlap required between an anchor + and ground-truth box for the (anchor, gt box) pair to be a positive + example. The data type must be float32. + rpn_negative_overlap(float): Maximum overlap allowed between an anchor + and ground-truth box for the (anchor, gt box) pair to be a negative + examples. The data type must be float32. + use_random(bool): Whether to sample randomly when sampling. + Returns: + tuple: + A tuple(predicted_scores, predicted_location, target_label, + target_bbox) is returned. The predicted_scores + and predicted_location is the predicted result of the RPN. + The target_label and target_bbox is the ground truth, + respectively. The predicted_location is a 2D Tensor with shape + [F, 5], and the shape of target_bbox is same as the shape of + the predicted_location, F is the number of the foreground + anchors. The predicted_scores is a 2D Tensor with shape + [F + B, 1], and the shape of target_label is same as the shape + of the predicted_scores, B is the number of the background + anchors, the F and B is depends on the input of this operator. + Bbox_inside_weight represents whether the predicted loc is fake_fg + or not and the shape is [F, 5]. + Examples: + .. code-block:: python + import paddle.fluid as fluid + bbox_pred = fluid.data(name='bbox_pred', shape=[None, 5], dtype='float32') + cls_logits = fluid.data(name='cls_logits', shape=[None, 1], dtype='float32') + anchor_box = fluid.data(name='anchor_box', shape=[None, 5], dtype='float32') + gt_boxes = fluid.data(name='gt_boxes', shape=[None, 5], dtype='float32') + im_info = fluid.data(name='im_infoss', shape=[None, 3], dtype='float32') + loc, score, loc_target, score_target = rrpn_target_assign( + bbox_pred, cls_logits, anchor_box, gt_boxes, im_info) + """ + + helper = LayerHelper('rrpn_target_assign', **locals()) + # Assign target label to anchors + loc_index = helper.create_variable_for_type_inference(dtype='int32') + score_index = helper.create_variable_for_type_inference(dtype='int32') + target_label = helper.create_variable_for_type_inference(dtype='int32') + target_bbox = helper.create_variable_for_type_inference( + dtype=anchor_box.dtype) + helper.append_op( + type="rrpn_target_assign", + inputs={'Anchor': anchor_box, + 'GtBoxes': gt_boxes, + 'ImInfo': im_info}, + outputs={ + 'LocationIndex': loc_index, + 'ScoreIndex': score_index, + 'TargetLabel': target_label, + 'TargetBBox': target_bbox + }, + attrs={ + 'rpn_batch_size_per_im': rpn_batch_size_per_im, + 'rpn_straddle_thresh': rpn_straddle_thresh, + 'rpn_positive_overlap': rpn_positive_overlap, + 'rpn_negative_overlap': rpn_negative_overlap, + 'rpn_fg_fraction': rpn_fg_fraction, + 'use_random': use_random + }) + + loc_index.stop_gradient = True + score_index.stop_gradient = True + target_label.stop_gradient = True + target_bbox.stop_gradient = True + + cls_logits = fluid.layers.reshape(x=cls_logits, shape=(-1, 1)) + bbox_pred = fluid.layers.reshape(x=bbox_pred, shape=(-1, 5)) + predicted_cls_logits = fluid.layers.gather(cls_logits, score_index) + predicted_bbox_pred = fluid.layers.gather(bbox_pred, loc_index) + + return predicted_cls_logits, predicted_bbox_pred, target_label, target_bbox + + +def rotated_anchor_generator(input, + anchor_sizes=None, + aspect_ratios=None, + angles=None, + variance=[1.0, 1.0, 1.0, 1.0, 1.0], + stride=None, + offset=0.5, + name=None): + """ + **Rotated Anchor generator operator** + Generate anchors for RRPN algorithm. + Each position of the input produce N anchors, N = + size(anchor_sizes) * size(aspect_ratios) * size(angles). + The order of generated anchors is firstly aspect_ratios + loop then anchor_sizes loop. + Args: + input(Variable): 4-D Tensor with shape [N,C,H,W]. The input feature map. + anchor_sizes(float32|list|tuple): The anchor sizes of generated + anchors, given in absolute pixels e.g. [64., 128., 256., 512.]. + For instance, the anchor size of 64 means the area of this anchor + equals to 64**2. None by default. + aspect_ratios(float32|list|tuple): The height / width ratios + of generated anchors, e.g. [0.5, 1.0, 2.0]. None by default. + angle(list|tuple): Rotated angle of prior boxes. The data type is float32. + variance(list|tuple): The variances to be used in box + regression deltas. The data type is float32, [1.0, 1.0, 1.0, 1.0, 1.0] by + default. + stride(list|tuple): The anchors stride across width and height. + The data type is float32. e.g. [16.0, 16.0]. None by default. + offset(float32): Prior boxes center offset. 0.5 by default. + name(str): Name of this layer. None by default. + Returns: + Anchors(Variable): The output anchors with a layout of [H, W, num_anchors, 5]. + H is the height of input, W is the width of input, + num_anchors is the box count of each position. Each anchor is + in (x, y, w, h, angle) format. + Variances(Variable): The expanded variances of anchors with a layout of + [H, W, num_priors, 5]. H is the height of input, + W is the width of input num_anchors is the box count + of each position. Each variance is in (x, y, w, h, angle) format. + Examples: + .. code-block:: python + import paddle.fluid as fluid + conv1 = fluid.data(name='conv1', shape=[None, 48, 16, 16], dtype='float32') + anchor, var = rotated_anchor_generator( + input=conv1, + anchor_sizes=[128, 256, 512], + aspect_ratios=[0.2, 0.5, 1.0], + variance=[1.0, 1.0, 1.0, 1.0, 1.0], + stride=[16.0, 16.0], + offset=0.5) + """ + helper = LayerHelper("rotated_anchor_generator", **locals()) + dtype = helper.input_dtype() + + def _is_list_or_tuple_(data): + return (isinstance(data, list) or isinstance(data, tuple)) + + if not _is_list_or_tuple_(anchor_sizes): + anchor_sizes = [anchor_sizes] + if not _is_list_or_tuple_(aspect_ratios): + aspect_ratios = [aspect_ratios] + if not _is_list_or_tuple_(angles): + angles = [angles] + if not (_is_list_or_tuple_(stride) and len(stride) == 2): + raise ValueError('stride should be a list or tuple ', + 'with length 2, (stride_width, stride_height).') + + anchor_sizes = list(map(float, anchor_sizes)) + aspect_ratios = list(map(float, aspect_ratios)) + angles = list(map(float, angles)) + stride = list(map(float, stride)) + + attrs = { + 'anchor_sizes': anchor_sizes, + 'aspect_ratios': aspect_ratios, + 'angles': angles, + 'variances': variance, + 'stride': stride, + 'offset': offset + } + + anchor = helper.create_variable_for_type_inference(dtype) + var = helper.create_variable_for_type_inference(dtype) + helper.append_op( + type="rotated_anchor_generator", + inputs={"Input": input}, + outputs={"Anchors": anchor, + "Variances": var}, + attrs=attrs, ) + anchor.stop_gradient = True + var.stop_gradient = True + return anchor, var + + +def rrpn_box_coder(prior_box, prior_box_var, target_box, name=None): + """ + Args: + prior_box(Variable): Box list prior_box is a 2-D Tensor with shape + [M, 5] holds M boxes and data type is float32 or float64. Each box + is represented as [x, y, w, h, angle], [x, y] is the + center coordinate of the anchor box, [w, h] is the width and height + of the anchor box, angle is rotated angle of prior_box. + prior_box_var(List|Variable|None): "prior_box_var is a 2-D Tensor with + shape [M, 5] holds M group of variance." + target_box(Variable): This input can be a 2-D LoDTensor with shape + [M, 5]. Each box is represented as [x, y, w, h, angle]. The data + type is float32 or float64. + name(str): Name of this layer. None by default. + Returns: + Variable: + output_box(Variable): The output tensor of rrpn_box_coder_op with shape [N, 5] representing the + result of N target boxes encoded with N Prior boxes and variances. + N represents the number of box and 5 represents [x, y, w, h ,angle]. + Examples: + + .. code-block:: python + + import paddle.fluid as fluid + prior_box_decode = fluid.data(name='prior_box_decode', + shape=[512, 5], + dtype='float32') + target_box_decode = fluid.data(name='target_box_decode', + shape=[512, 5], + dtype='float32') + output_decode = rrpn_box_coder(prior_box=prior_box_decode, + prior_box_var=[10, 10, 5, 5, 1], + target_box=target_box_decode) + """ + + helper = LayerHelper("rrpn_box_coder", **locals()) + + if name is None: + output_box = helper.create_variable_for_type_inference( + dtype=prior_box.dtype) + else: + output_box = helper.create_variable( + name=name, dtype=prior_box.dtype, persistable=False) + + inputs = {"PriorBox": prior_box, "TargetBox": target_box} + attrs = {} + if isinstance(prior_box_var, Variable): + inputs['PriorBoxVar'] = prior_box_var + elif isinstance(prior_box_var, list): + attrs['variance'] = prior_box_var + else: + raise TypeError( + "Input variance of rrpn_box_coder must be Variable or list") + helper.append_op( + type="rrpn_box_coder", + inputs=inputs, + attrs=attrs, + outputs={"OutputBox": output_box}) + return output_box + + +def rotated_roi_align(input, + rois, + pooled_height=1, + pooled_width=1, + spatial_scale=1.0, + name=None): + """ + **RotatedRoIAlign Operator** + + Rotated Region of interest align (also known as Rotated RoI align) is to perform + bilinear interpolation on inputs of nonuniform sizes to obtain + fixed-size feature maps (e.g. 7*7) + + Dividing each region proposal into equal-sized sections with + the pooled_width and pooled_height. Location remains the origin + result. + + Each ROI bin are transformed to become horizontal by perspective transformation and + values in each ROI bin are computed directly through bilinear interpolation. The output is + the mean of all values. + Thus avoid the misaligned problem. + """ + helper = LayerHelper('rrpn_rotated_roi_align', **locals()) + dtype = helper.input_dtype() + align_out = helper.create_variable_for_type_inference(dtype) + cx = helper.create_variable_for_type_inference('float32') + cy = helper.create_variable_for_type_inference('float32') + helper.append_op( + type="rrpn_rotated_roi_align", + inputs={"X": input, + "ROIs": rois}, + outputs={"Out": align_out, + "ConIdX": cx, + "ConIdY": cy}, + attrs={ + "pooled_height": pooled_height, + "pooled_width": pooled_width, + "spatial_scale": spatial_scale, + }) + return align_out + + +def rotated_generate_proposal_labels(rpn_rois, + gt_classes, + is_crowd, + gt_boxes, + im_info, + batch_size_per_im=256, + fg_fraction=0.25, + fg_thresh=0.25, + bg_thresh_hi=0.5, + bg_thresh_lo=0.0, + bbox_reg_weights=[0.1, 0.1, 0.2, 0.2], + class_nums=None, + use_random=True, + is_cls_agnostic=False): + """ + **Rotated Generate Proposal Labels** + This operator can be, for given the RotatedGenerateProposalOp output bounding boxes and groundtruth, + to sample foreground boxes and background boxes, and compute loss target. + RpnRois is the output boxes of RPN and was processed by rotated_generate_proposal_op, these boxes + were combined with groundtruth boxes and sampled according to batch_size_per_im and fg_fraction, + If an instance with a groundtruth overlap greater than fg_thresh, then it was considered as a foreground sample. + If an instance with a groundtruth overlap greater than bg_thresh_lo and lower than bg_thresh_hi, + then it was considered as a background sample. + After all foreground and background boxes are chosen (so called Rois), + then we apply random sampling to make sure + the number of foreground boxes is no more than batch_size_per_im * fg_fraction. + For each box in Rois, we assign the classification (class label) and regression targets (box label) to it. + Finally BboxInsideWeights and BboxOutsideWeights are used to specify whether it would contribute to training loss. + Args: + rpn_rois(Variable): A 2-D LoDTensor with shape [N, 5]. N is the number of the RotatedGenerateProposalOp's output, each element is a bounding box with [x, y, w, h, angle] format. The data type can be float32 or float64. + gt_classes(Variable): A 2-D LoDTensor with shape [M, 1]. M is the number of groundtruth, each element is a class label of groundtruth. The data type must be int32. + is_crowd(Variable): A 2-D LoDTensor with shape [M, 1]. M is the number of groundtruth, each element is a flag indicates whether a groundtruth is crowd. The data type must be int32. + gt_boxes(Variable): A 2-D LoDTensor with shape [M, 5]. M is the number of groundtruth, each element is a bounding box with [x, y, w, h, angle] format. + im_info(Variable): A 2-D LoDTensor with shape [B, 3]. B is the number of input images, each element consists of im_height, im_width, im_scale. + batch_size_per_im(int): Batch size of rois per images. The data type must be int32. + fg_fraction(float): Foreground fraction in total batch_size_per_im. The data type must be float32. + fg_thresh(float): Overlap threshold which is used to chose foreground sample. The data type must be float32. + bg_thresh_hi(float): Overlap threshold upper bound which is used to chose background sample. The data type must be float32. + bg_thresh_lo(float): Overlap threshold lower bound which is used to chose background sample. The data type must be float32. + bbox_reg_weights(list|tuple): Box regression weights. The data type must be float32. + class_nums(int): Class number. The data type must be int32. + use_random(bool): Use random sampling to choose foreground and background boxes. + is_cls_agnostic(bool): bbox regression use class agnostic simply which only represent fg and bg boxes. + Returns: + tuple: + A tuple with format``(rois, labels_int32, bbox_targets, bbox_inside_weights, bbox_outside_weights)``. + - **rois**: 2-D LoDTensor with shape ``[batch_size_per_im * batch_size, 5]``. The data type is the same as ``rpn_rois``. + - **labels_int32**: 2-D LoDTensor with shape ``[batch_size_per_im * batch_size, 1]``. The data type must be int32. + - **bbox_targets**: 2-D LoDTensor with shape ``[batch_size_per_im * batch_size, 5 * class_num]``. The regression targets of all RoIs. The data type is the same as ``rpn_rois``. + - **bbox_inside_weights**: 2-D LoDTensor with shape ``[batch_size_per_im * batch_size, 5 * class_num]``. The weights of foreground boxes' regression loss. The data type is the same as ``rpn_rois``. + - **bbox_outside_weights**: 2-D LoDTensor with shape ``[batch_size_per_im * batch_size, 5 * class_num]``. The weights of regression loss. The data type is the same as ``rpn_rois``. + Examples: + .. code-block:: python + import paddle.fluid as fluid + rpn_rois = fluid.data(name='rpn_rois', shape=[None, 5], dtype='float32') + gt_classes = fluid.data(name='gt_classes', shape=[None, 1], dtype='float32') + is_crowd = fluid.data(name='is_crowd', shape=[None, 1], dtype='float32') + gt_boxes = fluid.data(name='gt_boxes', shape=[None, 5], dtype='float32') + im_info = fluid.data(name='im_info', shape=[None, 3], dtype='float32') + rois, labels, bbox, inside_weights, outside_weights = rotated_generate_proposal_labels( + rpn_rois, gt_classes, is_crowd, gt_boxes, im_info, + class_nums=10) + """ + helper = LayerHelper('rrpn_generate_proposal_labels', **locals()) + rois = helper.create_variable_for_type_inference(dtype=rpn_rois.dtype) + labels_int32 = helper.create_variable_for_type_inference( + dtype=gt_classes.dtype) + bbox_targets = helper.create_variable_for_type_inference( + dtype=rpn_rois.dtype) + bbox_inside_weights = helper.create_variable_for_type_inference( + dtype=rpn_rois.dtype) + bbox_outside_weights = helper.create_variable_for_type_inference( + dtype=rpn_rois.dtype) + + helper.append_op( + type="rrpn_generate_proposal_labels", + inputs={ + 'RpnRois': rpn_rois, + 'GtClasses': gt_classes, + 'IsCrowd': is_crowd, + 'GtBoxes': gt_boxes, + 'ImInfo': im_info + }, + outputs={ + 'Rois': rois, + 'LabelsInt32': labels_int32, + 'BboxTargets': bbox_targets, + 'BboxInsideWeights': bbox_inside_weights, + 'BboxOutsideWeights': bbox_outside_weights + }, + attrs={ + 'batch_size_per_im': batch_size_per_im, + 'fg_fraction': fg_fraction, + 'fg_thresh': fg_thresh, + 'bg_thresh_hi': bg_thresh_hi, + 'bg_thresh_lo': bg_thresh_lo, + 'bbox_reg_weights': bbox_reg_weights, + 'class_nums': class_nums, + 'use_random': use_random, + 'is_cls_agnostic': is_cls_agnostic + }) + + rois.stop_gradient = True + labels_int32.stop_gradient = True + bbox_targets.stop_gradient = True + bbox_inside_weights.stop_gradient = True + bbox_outside_weights.stop_gradient = True + + return rois, labels_int32, bbox_targets, bbox_inside_weights, bbox_outside_weights + + +def rotated_generate_proposals(scores, + bbox_deltas, + im_info, + anchors, + variances, + pre_nms_top_n=6000, + post_nms_top_n=1000, + nms_thresh=0.5, + min_size=0.1, + name=None): + """ + **Rotated Generate proposal** + This operation proposes Rotated RoIs according to each box with their + probability to be a foreground object and the box can be calculated by anchors. + bbox_deltas and scores are the output of RPN. Final proposals could be used to + train detection net. For generating proposals, this operation performs following steps: + 1. Transposes and resizes scores and bbox_deltas in size of + (H*W*A, 1) and (H*W*A, 5) + 2. Calculate box locations as proposals candidates. + 3. Remove predicted boxes with small area. + 4. Apply NMS to get final proposals as output. + Args: + scores(Variable): A 4-D Tensor with shape [N, A, H, W] represents + the probability for each box to be an object. + N is batch size, A is number of anchors, H and W are height and + width of the feature map. The data type must be float32. + bbox_deltas(Variable): A 4-D Tensor with shape [N, 5*A, H, W] + represents the differece between predicted box locatoin and + anchor location. The data type must be float32. + im_info(Variable): A 2-D Tensor with shape [N, 3] represents origin + image information for N batch. Info contains height, width and scale + between origin image size and the size of feature map. + The data type must be int32. + anchors(Variable): A 4-D Tensor represents the anchors with a layout + of [H, W, A, 5]. H and W are height and width of the feature map, + num_anchors is the box count of each position. Each anchor is + in (x, y, w, h, angle) format. The data type must be float32. + variances(Variable): A 4-D Tensor. The expanded variances of anchors with a layout of + [H, W, num_priors, 5]. Each variance is in + (xcenter, ycenter, w, h) format. The data type must be float32. + pre_nms_top_n(float): Number of total bboxes to be kept per + image before NMS. The data type must be float32. `6000` by default. + post_nms_top_n(float): Number of total bboxes to be kept per + image after NMS. The data type must be float32. `1000` by default. + nms_thresh(float): Threshold in NMS. The data type must be float32. `0.5` by default. + min_size(float): Remove predicted boxes with either height or + width < min_size. The data type must be float32. `0.1` by default. + Returns: + tuple: + A tuple with format ``(rrpn_rois, rrpn_roi_probs)``. + - **rpn_rois**: The generated RoIs. 2-D Tensor with shape ``[N, 5]`` while ``N`` is the number of RoIs. The data type is the same as ``scores``. + - **rpn_roi_probs**: The scores of generated RoIs. 2-D Tensor with shape ``[N, 1]`` while ``N`` is the number of RoIs. The data type is the same as ``scores``. + Examples: + .. code-block:: python + + import paddle.fluid as fluid + scores = fluid.data(name='scores', shape=[None, 4, 5, 5], dtype='float32') + bbox_deltas = fluid.data(name='bbox_deltas', shape=[None, 20, 5, 5], dtype='float32') + im_info = fluid.data(name='im_info', shape=[None, 3], dtype='float32') + anchors = fluid.data(name='anchors', shape=[None, 5, 4, 5], dtype='float32') + variances = fluid.data(name='variances', shape=[None, 5, 10, 5], dtype='float32') + rrois, rroi_probs = fluid.layers.rotated_generate_proposals(scores, bbox_deltas, + im_info, anchors, variances) + """ + + helper = LayerHelper('rrpn_generate_proposals', **locals()) + + rpn_rois = helper.create_variable_for_type_inference( + dtype=bbox_deltas.dtype) + rpn_roi_probs = helper.create_variable_for_type_inference( + dtype=scores.dtype) + helper.append_op( + type="rrpn_generate_proposals", + inputs={ + 'Scores': scores, + 'BboxDeltas': bbox_deltas, + 'ImInfo': im_info, + 'Anchors': anchors, + 'Variances': variances + }, + attrs={ + 'pre_nms_topN': pre_nms_top_n, + 'post_nms_topN': post_nms_top_n, + 'nms_thresh': nms_thresh, + 'min_size': min_size + }, + outputs={'RpnRois': rpn_rois, + 'RpnRoiProbs': rpn_roi_probs}) + rpn_rois.stop_gradient = True + rpn_roi_probs.stop_gradient = True + + return rpn_rois, rpn_roi_probs diff --git a/PaddleCV/rrpn/models/ext_op/src/README.md b/PaddleCV/rrpn/models/ext_op/src/README.md new file mode 100644 index 00000000..cbec1854 --- /dev/null +++ b/PaddleCV/rrpn/models/ext_op/src/README.md @@ -0,0 +1,68 @@ +# 自定义OP的编译过程 + +## 代码结构 + + - src: 扩展OP C++/CUDA 源码 + - rrpn_lib.py: Python封装 + +## 安装PaddlePaddle + +请通过如下方式安装PaddlePaddle: + +- 通过[Paddle develop分支](https://github.com/PaddlePaddle/Paddle/tree/develop)源码编译安装,编译方法如下: + + 1. [Ubuntu](https://www.paddlepaddle.org.cn/install/doc/source/ubuntu) + 1. [CentOS](https://www.paddlepaddle.org.cn/install/doc/source/centos) + 1. [MasOS](https://www.paddlepaddle.org.cn/install/doc/source/macos) + 1. [Windows](https://www.paddlepaddle.org.cn/install/doc/source/windows) + + **说明:** 推荐使用docker编译 + +- 安装Paddle develop[每日版本whl包](https://www.paddlepaddle.org.cn/install/doc/tables#多版本whl包列表-dev-11) + + **注意:** 编译自定义OP使用的gcc版本须与Paddle编译使用gcc版本一致,Paddle develop每日版本目前采用**gcc 4.8.2**版本编译,若使用每日版本,请使用**gcc 4.8.2**版本编译自定义OP,否则可能出现兼容性问题。 + +## 编译自定义OP + +自定义op需要将实现的C++、CUDA代码编译成动态库,mask.sh中通过g++/nvcc编译,当然您也可以写Makefile或者CMake。 + +编译需要include PaddlePaddle的相关头文件,链接PaddlePaddle的lib库。 头文件和lib库可通过下面命令获取到: + +``` +# python +>>> import paddle +>>> print(paddle.sysconfig.get_include()) +/paddle/pyenv/local/lib/python2.7/site-packages/paddle/include +>>> print(paddle.sysconfig.get_lib()) +/paddle/pyenv/local/lib/python2.7/site-packages/paddle/libs +``` + +我们提供动态库编译脚本如下: + +``` +cd src +sh make.sh +``` + +最终编译会产出`rrpn_lib.so` + +**说明:** 若使用源码编译安装PaddlePaddle的方式,编译过程中`cmake`未设置`WITH_MKLDNN`的方式, +编译自定义OP时会报错找不到`mkldnn.h`等文件,可在`make.sh`中删除编译命令中的`-DPADDLE_WITH_MKLDNN`选项。 + +## 设置环境变量 + +需要将Paddle的核心库设置到`LD_LIBRARY_PATH`里, 先运行下面程序获取路径: + +``` +import paddle +print(paddle.sysconfig.get_lib()) +``` + +可通过如下方式添加动态库路径: + +``` +export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:`python -c 'import paddle; print(paddle.sysconfig.get_lib())'` +``` + + +更多关于如何在框架外部自定义 C++ OP,可阅读[官网说明文档](https://www.paddlepaddle.org.cn/documentation/docs/zh/advanced_usage/index_cn.html) diff --git a/PaddleCV/rrpn/models/ext_op/src/bbox_util.h b/PaddleCV/rrpn/models/ext_op/src/bbox_util.h new file mode 100644 index 00000000..dc978e2c --- /dev/null +++ b/PaddleCV/rrpn/models/ext_op/src/bbox_util.h @@ -0,0 +1,360 @@ +/* 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 +-------------------------------------------------------- +@misc{ma2019rrpn, + author = {Jianqi Ma}, + title = {{RRPN in pytorch}}, + year = {2019}, + howpublished = {\url{https://github.com/mjq11302010044/RRPN_pytorch}}, +} +@article{Jianqi17RRPN, + Author = {Jianqi Ma and Weiyuan Shao and Hao Ye and Li Wang and Hong Wang +and Yingbin Zheng and Xiangyang Xue}, + Title = {Arbitrary-Oriented Scene Text Detection via Rotation Proposals}, + journal = {IEEE Transactions on Multimedia}, + volume={20}, + number={11}, + pages={3111-3122}, + year={2018} +} +-------------------------------------------------------- +*/ + +#pragma once +#include +#include "paddle/fluid/framework/eigen.h" +#include "paddle/fluid/framework/op_registry.h" +#include "paddle/fluid/framework/tensor.h" + +namespace paddle { +namespace operators { + +#define PI 3.141592654 + +struct RangeInitFunctor { + int start; + int delta; + int* out; + HOSTDEVICE void operator()(size_t i) { out[i] = start + i * delta; } +}; + + +// get trangle area after decompose intersecting polygons into triangles +template +inline T trangle_area(T* a, T* b, T* c) { + return ((a[0] - c[0]) * (b[1] - c[1]) - (a[1] - c[1]) * (b[0] - c[0])) / 2.0; +} + +// get area of intersecting +template +inline T get_area(T* int_pts, int num_of_inter) { + T area = 0.0; + for (int i = 0; i < num_of_inter - 2; i++) { + area += fabs( + trangle_area(int_pts, int_pts + 2 * i + 2, int_pts + 2 * i + 4)); + } + return area; +} + +// sort points to decompose intersecting polygons into triangles +template +inline void reorder_pts(T* int_pts, int num_of_inter) { + if (num_of_inter > 0) { + T center[2] = {0.0, 0.0}; + + for (int i = 0; i < num_of_inter; i++) { + center[0] += int_pts[2 * i]; + center[1] += int_pts[2 * i + 1]; + } + center[0] /= num_of_inter; + center[1] /= num_of_inter; + + T vs[16]; + T v[2]; + T d; + for (int i = 0; i < num_of_inter; i++) { + v[0] = int_pts[2 * i] - center[0]; + v[1] = int_pts[2 * i + 1] - center[1]; + d = sqrt(v[0] * v[0] + v[1] * v[1]); + v[0] = v[0] / d; + v[1] = v[1] / d; + if (v[1] < 0) { + v[0] = -2 - v[0]; + } + vs[i] = v[0]; + } + + float temp, tx, ty; + int j; + for (int i = 1; i < num_of_inter; ++i) { + if (vs[i - 1] > vs[i]) { + temp = vs[i]; + tx = int_pts[2 * i]; + ty = int_pts[2 * i + 1]; + j = i; + while (j > 0 && vs[j - 1] > temp) { + vs[j] = vs[j - 1]; + int_pts[j * 2] = int_pts[j * 2 - 2]; + int_pts[j * 2 + 1] = int_pts[j * 2 - 1]; + j--; + } + vs[j] = temp; + int_pts[j * 2] = tx; + int_pts[j * 2 + 1] = ty; + } + } + } +} + +// determine if points intersect +template +inline bool inter2line(T* pts1, T* pts2, int i, int j, T* temp_pts) { + T a[2] = {pts1[2 * i], pts1[2 * i + 1]}; + T b[2] = {pts1[2 * ((i + 1) % 4)], pts1[2 * ((i + 1) % 4) + 1]}; + T c[2] = {pts2[2 * j], pts2[2 * j + 1]}; + T d[2] = {pts2[2 * ((j + 1) % 4)], pts2[2 * ((j + 1) % 4) + 1]}; + + T area_abc, area_abd, area_cda, area_cdb; + + area_abc = trangle_area(a, b, c); + area_abd = trangle_area(a, b, d); + + if (area_abc * area_abd >= -1e-5) { + return false; + } + + area_cda = trangle_area(c, d, a); + area_cdb = area_cda + area_abc - area_abd; + + if (area_cda * area_cdb >= -1e-5) { + return false; + } + T t = area_cda / (area_abd - area_abc); + + T dx = t * (b[0] - a[0]); + T dy = t * (b[1] - a[1]); + temp_pts[0] = a[0] + dx; + temp_pts[1] = a[1] + dy; + + return true; +} + +template +inline bool inrect(T pt_x, T pt_y, T* pts) { + T ab[2] = {pts[2] - pts[0], pts[3] - pts[1]}; + T ad[2] = {pts[6] - pts[0], pts[7] - pts[1]}; + T ap[2] = {pt_x - pts[0], pt_y - pts[1]}; + + T abab = ab[0] * ab[0] + ab[1] * ab[1]; + T abap = ab[0] * ap[0] + ab[1] * ap[1]; + T adad = ad[0] * ad[0] + ad[1] * ad[1]; + T adap = ad[0] * ap[0] + ad[1] * ap[1]; + bool result = (abab - abap >= -1) and (abap >= -1) and (adad - adap >= -1) and + (adap >= -1); + return result; +} + +// calculate the number of intersection points +template +inline int inter_pts(T* pts1, T* pts2, T* int_pts) { + int num_of_inter = 0; + + for (int i = 0; i < 4; i++) { + if (inrect(pts1[2 * i], pts1[2 * i + 1], pts2)) { + int_pts[num_of_inter * 2] = pts1[2 * i]; + int_pts[num_of_inter * 2 + 1] = pts1[2 * i + 1]; + num_of_inter++; + } + if (inrect(pts2[2 * i], pts2[2 * i + 1], pts1)) { + int_pts[num_of_inter * 2] = pts2[2 * i]; + int_pts[num_of_inter * 2 + 1] = pts2[2 * i + 1]; + num_of_inter++; + } + } + + T out_pts[2]; + + for (int i = 0; i < 4; i++) { + for (int j = 0; j < 4; j++) { + bool has_pts = inter2line(pts1, pts2, i, j, out_pts); + if (has_pts) { + int_pts[num_of_inter * 2] = out_pts[0]; + int_pts[num_of_inter * 2 + 1] = out_pts[1]; + num_of_inter++; + } + } + } + + + return num_of_inter; +} + +// convert x,y,w,h,angle to x1,y1,x2,y2,x3,y3,x4,y4 +template +inline void convert_region(T* pts, + const framework::Tensor& _region, + int index) { + auto region = framework::EigenTensor::From(_region); + T angle = region(index, 4); + T a_cos = cos(angle / 180.0 * PI); + T a_sin = -sin(angle / 180.0 * PI); // anti clock-wise + + T ctr_x = region(index, 0); + T ctr_y = region(index, 1); + T h = region(index, 3); + T w = region(index, 2); + + + T pts_x[4] = {-w / 2, -w / 2, w / 2, w / 2}; + T pts_y[4] = {-h / 2, h / 2, h / 2, -h / 2}; + + for (int i = 0; i < 4; i++) { + pts[2 * i] = a_cos * pts_x[i] - a_sin * pts_y[i] + ctr_x; + pts[2 * i + 1] = a_sin * pts_x[i] + a_cos * pts_y[i] + ctr_y; + } +} + + +// Calculate the area of intersection +template +inline float inter(const framework::Tensor& _region1, + const framework::Tensor& _region2, + const int& r, + const int& c) { + T pts1[8]; + T pts2[8]; + T int_pts[16]; + int num_of_inter; + + + convert_region(pts1, _region1, r); + convert_region(pts2, _region2, c); + + num_of_inter = inter_pts(pts1, pts2, int_pts); + + reorder_pts(int_pts, num_of_inter); + + return get_area(int_pts, num_of_inter); +} + +template +inline float devRotateIoU(const framework::Tensor& _region1, + const framework::Tensor& _region2, + const int r, + const int c) { + auto __region1 = framework::EigenTensor::From(_region1); + auto __region2 = framework::EigenTensor::From(_region2); + + if ((fabs(__region1(r, 0) - __region2(c, 0)) < 1e-5) && + (fabs(__region1(r, 1) - __region2(c, 1)) < 1e-5) && + (fabs(__region1(r, 2) - __region2(c, 2)) < 1e-5) && + (fabs(__region1(r, 3) - __region2(c, 3)) < 1e-5) && + (fabs(__region1(r, 4) - __region2(c, 4)) < 1e-5)) { + return 1.0; + } + T area1, area2, area_inter; + area1 = __region1(r, 2) * __region1(r, 3); + area2 = __region2(c, 2) * __region2(c, 3); + area_inter = inter(_region1, _region2, r, c); + auto result = area_inter / (area1 + area2 - area_inter); + + if (result < 0) { + result = 0.0; + } + // may have bugs which cause overlap > 1 + if (result > 1.00000001) { + result = 0.0; + } + return result; +} + + +template +inline void BoxToDelta2(const int box_num, + const framework::Tensor& ex_boxes, + const framework::Tensor& gt_boxes, + const float* weights, + framework::Tensor* box_delta) { + auto ex_boxes_et = framework::EigenTensor::From(ex_boxes); + auto gt_boxes_et = framework::EigenTensor::From(gt_boxes); + auto trg = framework::EigenTensor::From(*box_delta); + T ex_w, ex_h, ex_ctr_x, ex_ctr_y, ex_angle, gt_w, gt_h, gt_ctr_x, gt_ctr_y, + gt_angle; + for (int64_t i = 0; i < box_num; ++i) { + ex_w = ex_boxes_et(i, 2); + ex_h = ex_boxes_et(i, 3); + ex_ctr_x = ex_boxes_et(i, 0); + ex_ctr_y = ex_boxes_et(i, 1); + ex_angle = ex_boxes_et(i, 4); + + gt_w = gt_boxes_et(i, 2); + gt_h = gt_boxes_et(i, 3); + gt_ctr_x = gt_boxes_et(i, 0); + gt_ctr_y = gt_boxes_et(i, 1); + gt_angle = gt_boxes_et(i, 4); + + trg(i, 0) = (gt_ctr_x - ex_ctr_x) / ex_w; + trg(i, 1) = (gt_ctr_y - ex_ctr_y) / ex_h; + trg(i, 2) = std::log(gt_w / ex_w); + trg(i, 3) = std::log(gt_h / ex_h); + trg(i, 4) = gt_angle - ex_angle; + + if (weights) { + trg(i, 0) = trg(i, 0) * weights[0]; + trg(i, 1) = trg(i, 1) * weights[1]; + trg(i, 2) = trg(i, 2) * weights[2]; + trg(i, 3) = trg(i, 3) * weights[3]; + trg(i, 4) = trg(i, 4) * weights[4]; + } + + if (gt_angle <= -30 && ex_angle >= 120) { + trg(i, 4) = trg(i, 4) + 180.0; + } + if (gt_angle >= 120 && ex_angle <= -30) { + trg(i, 4) = trg(i, 4) - 180.0; + } + trg(i, 4) = (PI / 180) * trg(i, 4); + } +} + + +template +void Gather( + const T* in, const int in_stride, const int* index, const int num, T* out) { + const int stride_bytes = in_stride * sizeof(T); + for (int i = 0; i < num; ++i) { + int id = index[i]; + memcpy(out + i * in_stride, in + id * in_stride, stride_bytes); + } +} + +template +void BboxOverlaps2(const framework::Tensor& r_boxes, + const framework::Tensor& c_boxes, + framework::Tensor* overlaps) { + auto overlaps_et = framework::EigenTensor::From(*overlaps); + int r_num = r_boxes.dims()[0]; + int c_num = c_boxes.dims()[0]; + for (int i = 0; i < r_num; ++i) { + for (int j = 0; j < c_num; ++j) { + overlaps_et(i, j) = devRotateIoU(r_boxes, c_boxes, i, j); + } + } +} + + +} // namespace operators +} // namespace paddle diff --git a/PaddleCV/rrpn/models/ext_op/src/blas.h b/PaddleCV/rrpn/models/ext_op/src/blas.h new file mode 100644 index 00000000..5229882c --- /dev/null +++ b/PaddleCV/rrpn/models/ext_op/src/blas.h @@ -0,0 +1,487 @@ +// 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. + +#pragma once + +#include "paddle/fluid/framework/operator.h" +#include "paddle/fluid/framework/tensor.h" + +#ifdef PADDLE_WITH_MKLML +#include "paddle/fluid/platform/dynload/mklml.h" +#endif + +#ifdef PADDLE_WITH_LIBXSMM +#include +#endif + +#ifdef PADDLE_USE_OPENBLAS +#include +#endif + +namespace paddle { +namespace operators { +namespace math { + +/** + * Matrix Descriptor of a memory buffer. + * + * It is used for Blas::MatMul. MatMul operator can be batched. + * if Mat A is [BatchSize, H, W], Mat B is [BatchSize, H, W]. It will be a + * `batch_size` times of GEMM. The batched GEMM could be faster base on the + * implementation of the blas library. The batch size could be zero. If any + * matrix of `matmul` has a batch size, the will be a batched GEMM, too. e.g., + * Mat A is [BatchSize, H1, W2], and Mat B [H2, W2], The result matrix wil be + * [BatchSize, H1, W2] + * + * The boolean flag, `trans`, describe the memory is the transpose of matrix or + * not. If the trans is true, the last two dims of matrix are transposed. The + * memory layout of the matrix is [Width, Height] or [BatchSize, Width, Height]. + * + * The MatDescriptor is not only the dimension or shape of a matrix, it also + * contains the layout, stride of matrix. It is clearer to have a structure than + * reuse `DDim`. + */ +struct MatDescriptor { + int64_t height_; + int64_t width_; + int64_t stride_{0}; + int64_t batch_size_{0}; + bool trans_; +}; + +/** + * Create Matrix Descriptor from a tensor dim, num_flatten_cols, and transpose + * flag + * + * @param tensor_dim: The dimension of the tensor. The rank of this dimension + * must larger than 1. + * + * @param num_flatten_cols: Reshape a tensor to a matrix. The matrix's first + * dimension(column length) will be the product of tensor's first `num_col_dims` + * dimensions. If num_flatten_cols is zero, the first N-2 dimension will be the + * batch_size of descriptor. + * + * @param trans: True if the matrix is transposed. + */ +extern MatDescriptor CreateMatrixDescriptor(const framework::DDim& tensor_dim, + int num_flatten_cols, + bool trans); + +template +class Blas { +public: + explicit Blas(const DeviceContext& context) : context_(context) {} + + template + void GEMM(CBLAS_TRANSPOSE transA, + CBLAS_TRANSPOSE transB, + int M, + int N, + int K, + T alpha, + const T* A, + const T* B, + T beta, + T* C) const; + + template + void GEMM(bool transA, + bool transB, + int M, + int N, + int K, + T alpha, + const T* A, + int lda, + const T* B, + int ldb, + T beta, + T* C, + int ldc) const; + + template + void GEMM(CBLAS_TRANSPOSE transA, + CBLAS_TRANSPOSE transB, + int M, + int N, + int K, + T alpha, + const T* A, + int lda, + const T* B, + int ldb, + T beta, + T* C, + int ldc) const; + +#ifdef PADDLE_WITH_MKLML + template + T* GEMM_ALLOC(const CBLAS_IDENTIFIER id, + const int M, + const int N, + const int K) const; + + template + void GEMM_PACK(const CBLAS_IDENTIFIER id, + const CBLAS_TRANSPOSE trans, + int M, + int N, + int K, + const T alpha, + const T* src, + const int ld, + T* dst) const; + + template + void GEMM_COMPUTE(int transA, + int transB, + int M, + int N, + int K, + const T* A, + const int lda, + const T* B, + const int ldb, + T beta, + T* C, + const int ldc) const; + + template + void GEMM_FREE(T* data) const; + + template + void CSRMM(const char* transa, + const int* m, + const int* n, + const int* k, + const T* alpha, + const char* matdescra, + const T* val, + const int* indx, + const int* pntrb, + const int* pntre, + const T* b, + const int* ldb, + const T* beta, + T* c, + const int* ldc) const; + +#if !defined(PADDLE_WITH_CUDA) + template + void MatMulWithHead(const framework::Tensor& mat_a, + const MatDescriptor& dim_a, + const framework::Tensor& mat_b, + const MatDescriptor& dim_b, + T alpha, + int head_number, + framework::Tensor* mat_out, + T beta, + bool mat_y_split_vertical) const; +#endif +#endif + + template + void MatMul(const int M, + const int N, + const int K, + const T* A, + const T* B, + T* C) const; + + template + void MatMul(const framework::Tensor& mat_a, + bool trans_a, + const framework::Tensor& mat_b, + bool trans_b, + T alpha, + framework::Tensor* mat_out, + T beta) const; + + template + void MatMul(const framework::Tensor& mat_a, + bool trans_a, + const framework::Tensor& mat_b, + bool trans_b, + framework::Tensor* mat_out) const { + MatMul(mat_a, + trans_a, + mat_b, + trans_b, + static_cast(1.0), + mat_out, + static_cast(0.0)); + } + + template + void MatMul(const framework::Tensor& mat_a, + const framework::Tensor& mat_b, + framework::Tensor* mat_out) const { + this->template MatMul(mat_a, false, mat_b, false, mat_out); + } + + template + void AXPY(int n, T alpha, const T* x, T* y) const; + + template + void VADD(int n, const T* x, const T* y, T* z) const; + + template + void VSUB(int n, const T* x, const T* y, T* z) const; + + template + void VMUL(int n, const T* x, const T* y, T* z) const; + + template + void VDIV(int n, const T* x, const T* y, T* z) const; + + template + void VCOPY(int n, const T* x, T* y) const; + + template + void VEXP(int n, const T* x, T* y) const; + + template + void VSQUARE(int n, const T* x, T* y) const; + + template + void VPOW(int n, const T* x, T alpha, T* y) const; + + template + void GEMV(bool trans_a, + int M, + int N, + T alpha, + const T* A, + const T* B, + T beta, + T* C) const; + + template + T DOT(int n, const T* x, const T* y) const; + + template + void SCAL(int n, const T a, T* x) const; + + template + T ASUM(int n, T* x, int inc) const; + + template + void BatchedGEMM(CBLAS_TRANSPOSE transA, + CBLAS_TRANSPOSE transB, + int M, + int N, + int K, + T alpha, + const T* A, + const T* B, + T beta, + T* C, + int batchCount, + int64_t strideA, + int64_t strideB) const; + +#if defined(PADDLE_WITH_MKLML) && !defined(PADDLE_WITH_CUDA) + template + void BatchedGEMMWithHead(CBLAS_TRANSPOSE transA, + CBLAS_TRANSPOSE transB, + int W1, + int H1, + int W2, + int H2, + T alpha, + const T* A, + const T* B, + T beta, + T* C, + int batchCount, + int64_t strideA, + int64_t strideB, + int64_t head_number, + bool split_b_vertical) const; +#endif + + template + void MatMul(const framework::Tensor& mat_a, + const MatDescriptor& dim_a, + const framework::Tensor& mat_b, + const MatDescriptor& dim_b, + T alpha, + framework::Tensor* mat_out, + T beta) const; + + template + void VINV(int n, const T* a, T* y) const; + + template + void VMERF(int n, const T* a, T* y, int64_t mode) const; + +private: + const DeviceContext& context_; +}; + +template +class BlasT : private Blas { +public: + using Blas::Blas; + + template + void GEMM(ARGS... args) const { + Base()->template GEMM(args...); + } + +#ifdef PADDLE_WITH_MKLML + template + T* GEMM_ALLOC(ARGS... args) const { + return Base()->template GEMM_ALLOC(args...); + } + + template + void GEMM_PACK(ARGS... args) const { + Base()->template GEMM_PACK(args...); + } + + template + void GEMM_COMPUTE(ARGS... args) const { + Base()->template GEMM_COMPUTE(args...); + } + + template + void GEMM_FREE(ARGS... args) const { + Base()->template GEMM_FREE(args...); + } + + template + void CSRMM(ARGS... args) const { + Base()->template CSRMM(args...); + } + +#if !defined(PADDLE_WITH_CUDA) + template + void MatMulWithHead(ARGS... args) const { + Base()->template MatMulWithHead(args...); + } +#endif +#endif + + template + void MatMul(ARGS... args) const { + Base()->template MatMul(args...); + } + + template + void AXPY(ARGS... args) const { + Base()->template AXPY(args...); + } + + template + void VADD(ARGS... args) const { + Base()->template VADD(args...); + } + + template + void VSUB(ARGS... args) const { + Base()->template VSUB(args...); + } + + template + void VMUL(ARGS... args) const { + Base()->template VMUL(args...); + } + + template + void VDIV(ARGS... args) const { + Base()->template VDIV(args...); + } + + template + void VCOPY(ARGS... args) const { + Base()->template VCOPY(args...); + } + + template + void VEXP(ARGS... args) const { + Base()->template VEXP(args...); + } + + template + void VSQUARE(ARGS... args) const { + Base()->template VSQUARE(args...); + } + + template + void VPOW(ARGS... args) const { + Base()->template VPOW(args...); + } + + template + void GEMV(ARGS... args) const { + Base()->template GEMV(args...); + } + + template + T DOT(ARGS... args) const { + return Base()->template DOT(args...); + } + + template + void SCAL(ARGS... args) const { + Base()->template SCAL(args...); + } + + template + T ASUM(ARGS... args) const { + return Base()->template ASUM(args...); + } + + template + void BatchedGEMM(ARGS... args) const { + Base()->template BatchedGEMM(args...); + } + + template + void VINV(ARGS... args) const { + Base()->template VINV(args...); + } + + template + void VMERF(ARGS... args) const { + Base()->template VMERF(args...); + } + +private: + const Blas* Base() const { + return static_cast*>(this); + } +}; + +template +inline BlasT GetBlas( + const framework::ExecutionContext& exe_ctx) { + return BlasT( + exe_ctx.template device_context()); +} + +template +inline BlasT GetBlas(const DeviceContext& dev_ctx) { + return BlasT(dev_ctx); +} + +} // namespace math +} // namespace operators +} // namespace paddle + +#include "paddle/fluid/operators/math/blas_impl.h" +#ifdef PADDLE_WITH_CUDA +#include "paddle/fluid/operators/math/blas_impl.cu.h" +#endif diff --git a/PaddleCV/rrpn/models/ext_op/src/concat_and_split.cc b/PaddleCV/rrpn/models/ext_op/src/concat_and_split.cc new file mode 100644 index 00000000..20bf9963 --- /dev/null +++ b/PaddleCV/rrpn/models/ext_op/src/concat_and_split.cc @@ -0,0 +1,76 @@ +/* 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. */ + +#include "concat_and_split.h" +#include + +namespace paddle { +namespace operators { +namespace math { + +/* + * All tensors' dimension should be the same and the values of + * each dimension must be the same, except the axis dimension. + */ +template +class ConcatFunctor { +public: + void operator()(const platform::CPUDeviceContext& context, + const std::vector& input, + int axis, + framework::Tensor* output) { + // TODO(zcd): Add input data validity checking + int num = input.size(); + + int rows = 1; + auto dim_0 = input[0].dims(); + for (int i = 0; i < axis; ++i) { + rows *= dim_0[i]; + } + int out_rows = rows, out_cols = 0; + + std::vector input_cols(input.size()); + for (int i = 0; i < num; ++i) { + int t_cols = input[i].numel() / rows; + out_cols += t_cols; + input_cols[i] = t_cols; + } + auto cpu_place = boost::get(context.GetPlace()); + + // computation + auto output_data = output->data(); + int col_idx = 0; + for (int j = 0; j < num; ++j) { + int col_len = input_cols[j]; + auto input_data = input[j].data(); + for (int k = 0; k < out_rows; ++k) { + memory::Copy(cpu_place, + output_data + k * out_cols + col_idx, + cpu_place, + input_data + k * col_len, + sizeof(T) * col_len); + } + col_idx += col_len; + } + } +}; + +#define DEFINE_FUNCTOR(type) \ + template class ConcatFunctor; + +FOR_ALL_TYPES(DEFINE_FUNCTOR); + +} // namespace math +} // namespace operators +} // namespace paddle diff --git a/PaddleCV/rrpn/models/ext_op/src/concat_and_split.h b/PaddleCV/rrpn/models/ext_op/src/concat_and_split.h new file mode 100644 index 00000000..d5947597 --- /dev/null +++ b/PaddleCV/rrpn/models/ext_op/src/concat_and_split.h @@ -0,0 +1,59 @@ +/* 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. */ + +#pragma once +#include +#include "paddle/fluid/framework/data_type.h" +#include "paddle/fluid/framework/lod_tensor.h" + +namespace paddle { +namespace operators { +namespace math { + +/* + * \brief Concatenate the input tensors along the dimension axis. + * TODO(zcd): maybe it needs to be more detailed. + * Examples: + * Input[0] = [[1,2],[3,4]] + * Input[1] = [[5,6]] + * axis = 0 + * + * Output = [[1,2], + * [3,4], + * [5,6]] + */ +template +class ConcatFunctor { +public: + void operator()(const DeviceContext& context, + const std::vector& input, + int axis, + framework::Tensor* output); +}; + + +} // namespace math +} // namespace operators +} // namespace paddle + +#define FOR_ALL_TYPES(macro) \ + macro(int); \ + macro(float); \ + macro(double); \ + macro(bool); \ + macro(int64_t); \ + macro(int16_t); \ + macro(uint8_t); \ + macro(int8_t); \ + macro(::paddle::platform::float16) diff --git a/PaddleCV/rrpn/models/ext_op/src/gather.cu.h b/PaddleCV/rrpn/models/ext_op/src/gather.cu.h new file mode 100644 index 00000000..9e6b76b3 --- /dev/null +++ b/PaddleCV/rrpn/models/ext_op/src/gather.cu.h @@ -0,0 +1,125 @@ +/* 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. */ + +#pragma once +#include +#include "paddle/fluid/framework/dim.h" +#include "paddle/fluid/framework/operator.h" +#include "paddle/fluid/framework/tensor.h" +#include "paddle/fluid/memory/malloc.h" +#include "paddle/fluid/platform/cuda_primitives.h" +#include "paddle/fluid/platform/place.h" + +namespace paddle { +namespace operators { + +using framework::Tensor; +using platform::DeviceContext; + +#define CUDA_1D_KERNEL_LOOP(i, n) \ + for (int i = blockIdx.x * blockDim.x + threadIdx.x; i < (n); \ + i += blockDim.x * gridDim.x) + +template +__global__ void GatherCUDAKernel(const T* params, + const IndexT* indices, + T* output, + size_t index_size, + size_t slice_size) { + CUDA_1D_KERNEL_LOOP(i, index_size * slice_size) { + int indices_i = i / slice_size; + int slice_i = i - indices_i * slice_size; // offset inside the slice + IndexT gather_i = indices[indices_i]; + IndexT params_i = gather_i * slice_size + slice_i; + *(output + i) = *(params + params_i); + } +} + +template +__global__ void GatherNdCUDAKernel(const T* input, + const int* input_dims, + const IndexT* indices, + T* output, + size_t remain_size, + size_t slice_size, + size_t end_size) { + CUDA_1D_KERNEL_LOOP(i, remain_size * slice_size) { + int indices_i = i / slice_size; + int slice_i = i - indices_i * slice_size; // offset inside the slice + IndexT gather_i = 0; + int64_t temp = slice_size; + for (int64_t j = end_size - 1; j >= 0; --j) { + auto index_value = indices[indices_i * end_size + j]; + assert(index_value >= 0 && index_value < input_dims[j]); + gather_i += (index_value * temp); + temp *= input_dims[j]; + } + IndexT input_i = gather_i + slice_i; + *(output + i) = *(input + input_i); + } +} + +/** + * A thin wrapper on gpu tensor + * Return a new tensor from source tensor, gathered according to index + * input[src]: type-T source Tensor + * input[index]: type-IndexT index Tensor (1-D) + * return: output tensor + */ +template +void GPUGather(const platform::DeviceContext& ctx, + const Tensor& src, + const Tensor& index, + Tensor* output) { + // check index of shape 1-D + if (index.dims().size() == 1) { + PADDLE_ENFORCE_GT(index.dims()[0], + 0, + "The index of gather_op should not be empty when the " + "index's rank is 1."); + } else if (index.dims().size() == 2) { + PADDLE_ENFORCE_EQ(index.dims()[1], + 1, + " If the index's rank of gather_op is 2, the second " + "dimension should be 1."); + } + + int index_size = index.dims()[0]; + + auto src_dims = src.dims(); + framework::DDim output_dims(src_dims); + output_dims[0] = index_size; + + // slice size + int slice_size = 1; + for (int i = 1; i < src_dims.size(); ++i) slice_size *= src_dims[i]; + + const T* p_src = src.data(); + const IndexT* p_index = index.data(); + T* p_output = output->data(); + + int block = 512; + int n = slice_size * index_size; + int grid = (n + block - 1) / block; + + GatherCUDAKernel<<< + grid, + block, + 0, + reinterpret_cast(ctx).stream()>>>( + p_src, p_index, p_output, index_size, slice_size); +} + +} // namespace operators +} // namespace paddle diff --git a/PaddleCV/rrpn/models/ext_op/src/gather.h b/PaddleCV/rrpn/models/ext_op/src/gather.h new file mode 100644 index 00000000..a2ee0742 --- /dev/null +++ b/PaddleCV/rrpn/models/ext_op/src/gather.h @@ -0,0 +1,74 @@ +/* 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. */ + +#pragma once +#include +#include + +#include "paddle/fluid/framework/ddim.h" +#include "paddle/fluid/framework/eigen.h" +#include "paddle/fluid/framework/tensor.h" +#include "paddle/fluid/platform/place.h" + +namespace paddle { +namespace operators { + +using framework::Tensor; + +/** + * A thin wrapper for gathering on cpu tensor + * Return a new tensor from source tensor, gathered according to index + * input[src]: type-T source Tensor + * input[index]: type-IndexT index Tensor (1-D) + * return: output tensor + */ +template +void CPUGather(const platform::DeviceContext& ctx, + const Tensor& src, + const Tensor& index, + Tensor* output) { + PADDLE_ENFORCE_EQ(platform::is_cpu_place(ctx.GetPlace()), true); + // check index of shape 1-D + if (index.dims().size() == 2) { + PADDLE_ENFORCE_EQ(index.dims()[1], + 1, + "index.dims()[1] should be 1 when index.dims().size() == " + "2 in gather_op."); + } else { + PADDLE_ENFORCE_EQ(index.dims().size(), + 1, + "index.dims().size() should be 1 or 2 in gather_op."); + } + int64_t index_size = index.dims()[0]; + + auto src_dims = src.dims(); + + const T* p_src = src.data(); + const IndexT* p_index = index.data(); + T* p_output = output->data(); + + // slice size + int slice_size = 1; + for (int i = 1; i < src_dims.size(); ++i) slice_size *= src_dims[i]; + + const size_t slice_bytes = slice_size * sizeof(T); + + for (int64_t i = 0; i < index_size; ++i) { + IndexT index_ = p_index[i]; + memcpy(p_output + i * slice_size, p_src + index_ * slice_size, slice_bytes); + } +} + +} // namespace operators +} // namespace paddle diff --git a/PaddleCV/rrpn/models/ext_op/src/make.sh b/PaddleCV/rrpn/models/ext_op/src/make.sh new file mode 100644 index 00000000..96810820 --- /dev/null +++ b/PaddleCV/rrpn/models/ext_op/src/make.sh @@ -0,0 +1,73 @@ +include_dir=$( python -c 'import paddle; print(paddle.sysconfig.get_include())' ) +lib_dir=$( python -c 'import paddle; print(paddle.sysconfig.get_lib())' ) + +echo $include_dir +echo $lib_dir + +CUDA=$1 +CUDNN=$2 +NCCL=$3 + +if [ ! -d "$CUDA" ]; then +echo "Usage: sh make.sh \$CUDA_PATH \$CUDNN_PATH \$NCCL_PATH" +exit +fi + +if [ ! -d "$CUDNN" ]; then +echo "Usage: sh make.sh \${CUDA_PATH} \${CUDNN_PATH} \${NCCL_PATH}" +exit +fi + +if [ ! -d "$NCCL" ]; then +echo "Usage: sh make.sh \${CUDA_PATH} \${CUDNN_PATH} \${NCCL_PATH}" +exit +fi + +git clone https://github.com/NVlabs/cub.git + +nvcc rrpn_generate_proposals_op.cu -c -o rrpn_generate_proposals_op.cu.o -ccbin cc -DPADDLE_WITH_MKLDNN -DPADDLE_WITH_CUDA -DEIGEN_USE_GPU -DPADDLE_USE_DSO -Xcompiler -fPIC -std=c++11 -Xcompiler -fPIC -w --expt-relaxed-constexpr -O3 -DNVCC \ + -I ${include_dir} \ + -I ${include_dir}/third_party \ + -I ${CUDA}/include \ + -I ${CUDNN}/include \ + -I ${NCCL}/include \ + -L ${lib_dir} -lpaddle_framework \ + -L ${CUDA}/lib64 -lcudart + + +nvcc rotated_anchor_generator_op.cu -c -o rotated_anchor_generator_op.cu.o -ccbin cc -DPADDLE_WITH_MKLDNN -DPADDLE_WITH_CUDA -DEIGEN_USE_GPU -DPADDLE_USE_DSO -Xcompiler -fPIC -std=c++11 -Xcompiler -fPIC -w --expt-relaxed-constexpr -O3 -DNVCC \ + -I ${include_dir} \ + -I ${include_dir}/third_party \ + -I ${CUDA}/include \ + -I ${CUDNN}/include \ + -I ${NCCL}/include \ + -L ${lib_dir} -lpaddle_framework \ + -L ${CUDA}/lib64 -lcudart + +nvcc rrpn_box_coder_op.cu -c -o rrpn_box_coder_op.cu.o -ccbin cc -DPADDLE_WITH_MKLDNN -DPADDLE_WITH_CUDA -DEIGEN_USE_GPU -DPADDLE_USE_DSO -Xcompiler -fPIC -std=c++11 -Xcompiler -fPIC -w --expt-relaxed-constexpr -O3 -DNVCC \ + -I ${include_dir} \ + -I ${include_dir}/third_party \ + -I ${CUDA}/include \ + -I ${CUDNN}/include \ + -I ${NCCL}/include \ + -L ${lib_dir} -lpaddle_framework \ + -L ${CUDA}/lib64 -lcudart + +nvcc rrpn_rotated_roi_align_op.cu -c -o rrpn_rotated_roi_align_op.cu.o -ccbin cc -DPADDLE_WITH_MKLDNN -DPADDLE_WITH_CUDA -DEIGEN_USE_GPU -DPADDLE_USE_DSO -Xcompiler -fPIC -std=c++11 -Xcompiler -fPIC -w --expt-relaxed-constexpr -O3 -DNVCC \ + -I ${include_dir} \ + -I ${include_dir}/third_party \ + -I ${CUDA}/include \ + -I ${CUDNN}/include \ + -I ${NCCL}/include \ + -L ${lib_dir} -lpaddle_framework \ + -L ${CUDA}/lib64 -lcudart + + +g++ rotated_anchor_generator_op.cc concat_and_split.cc rrpn_generate_proposal_labels_op.cc rrpn_generate_proposals_op.cc rrpn_target_assign_op.cc rrpn_box_coder_op.cc rrpn_rotated_roi_align_op.cc rrpn_rotated_roi_align_op.cu.o rrpn_box_coder_op.cu.o rotated_anchor_generator_op.cu.o rrpn_generate_proposals_op.cu.o -o rrpn_lib.so -shared -fPIC -std=c++11 -O3 -DPADDLE_WITH_MKLDNN -DPADDLE_WITH_CUDA -DEIGEN_USE_GPU -DPADDLE_USE_DSO \ + -I ${include_dir} \ + -I ${include_dir}/third_party \ + -I ${CUDA}/include \ + -I ${CUDNN}/include \ + -I ${NCCL}/include \ + -L ${lib_dir} -lpaddle_framework \ + -L ${CUDA}/lib64 -lcudart diff --git a/PaddleCV/rrpn/models/ext_op/src/math_function.cc b/PaddleCV/rrpn/models/ext_op/src/math_function.cc new file mode 100644 index 00000000..24d5909e --- /dev/null +++ b/PaddleCV/rrpn/models/ext_op/src/math_function.cc @@ -0,0 +1,73 @@ +/* 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. */ + +#include "math_function.h" + +#ifdef PADDLE_WITH_MKLML +#include "paddle/fluid/platform/dynload/mklml.h" +#endif + +#ifdef PADDLE_USE_OPENBLAS +#include +#endif + +#include +#include "math_function_impl.h" +#include "paddle/fluid/framework/data_type.h" +#include "paddle/fluid/platform/float16.h" + +namespace paddle { +namespace operators { +namespace math { + +#define DEFINE_CPU_TRANS(RANK) \ + template struct Transpose; \ + template struct Transpose; \ + template struct Transpose; \ + template struct Transpose; \ + template struct Transpose; \ + template struct Transpose; \ + template struct Transpose; \ + template struct Transpose; \ + template struct Transpose; + +DEFINE_CPU_TRANS(1); +DEFINE_CPU_TRANS(2); +DEFINE_CPU_TRANS(3); +DEFINE_CPU_TRANS(4); +DEFINE_CPU_TRANS(5); +DEFINE_CPU_TRANS(6); + +template +void Transpose::operator()( + const DeviceContext& context, + const framework::Tensor& in, + framework::Tensor* out, + const std::vector& axis) { + Eigen::array permute; + for (int i = 0; i < Rank; i++) { + permute[i] = axis[i]; + } + auto eigen_in = framework::EigenTensor::From(in); + auto eigen_out = framework::EigenTensor::From(*out); + auto* dev = context.eigen_device(); + eigen_out.device(*dev) = eigen_in.shuffle(permute); +} + + +} // namespace math +} // namespace operators +} // namespace paddle diff --git a/PaddleCV/rrpn/models/ext_op/src/math_function.h b/PaddleCV/rrpn/models/ext_op/src/math_function.h new file mode 100644 index 00000000..b8043943 --- /dev/null +++ b/PaddleCV/rrpn/models/ext_op/src/math_function.h @@ -0,0 +1,43 @@ +/* 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. */ + +#pragma once +#include +#include + +#include "paddle/fluid/framework/eigen.h" +#include "paddle/fluid/framework/operator.h" +#include "paddle/fluid/framework/tensor.h" +#include "paddle/fluid/framework/tensor_util.h" +#include "paddle/fluid/platform/device_context.h" +#include "paddle/fluid/platform/enforce.h" + +namespace paddle { +namespace operators { +namespace math { +template +struct Transpose { + void operator()(const DeviceContext& context, + const framework::Tensor& in, + framework::Tensor* out, + const std::vector& axis); +}; + +void set_constant(const platform::DeviceContext& context, + framework::Tensor* tensor, + float value); + +} // namespace math +} // namespace operators +} // namespace paddle diff --git a/PaddleCV/rrpn/models/ext_op/src/rotated_anchor_generator_op.cc b/PaddleCV/rrpn/models/ext_op/src/rotated_anchor_generator_op.cc new file mode 100644 index 00000000..854245aa --- /dev/null +++ b/PaddleCV/rrpn/models/ext_op/src/rotated_anchor_generator_op.cc @@ -0,0 +1,172 @@ +/* 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. */ + +#include "rotated_anchor_generator_op.h" + +namespace paddle { +namespace operators { + +class RotatedAnchorGeneratorOp : public framework::OperatorWithKernel { +public: + using framework::OperatorWithKernel::OperatorWithKernel; + + void InferShape(framework::InferShapeContext* ctx) const override { + PADDLE_ENFORCE( + ctx->HasInput("Input"), + "Input(Input) of RotatedAnchorGeneratorOp should not be null."); + PADDLE_ENFORCE( + ctx->HasOutput("Anchors"), + "Output(Anchors) of RotatedAnchorGeneratorOp should not be null."); + PADDLE_ENFORCE( + ctx->HasOutput("Variances"), + "Output(Variances) of RotatedAnchorGeneratorOp should not be null."); + + auto input_dims = ctx->GetInputDim("Input"); + PADDLE_ENFORCE(input_dims.size() == 4, "The layout of input is NCHW."); + + auto anchor_sizes = ctx->Attrs().Get>("anchor_sizes"); + auto aspect_ratios = ctx->Attrs().Get>("aspect_ratios"); + auto angles = ctx->Attrs().Get>("angles"); + auto stride = ctx->Attrs().Get>("stride"); + auto variances = ctx->Attrs().Get>("variances"); + + size_t num_anchors = + aspect_ratios.size() * anchor_sizes.size() * angles.size(); + + std::vector dim_vec(4); + dim_vec[0] = input_dims[2]; + dim_vec[1] = input_dims[3]; + dim_vec[2] = num_anchors; + dim_vec[3] = 5; + ctx->SetOutputDim("Anchors", framework::make_ddim(dim_vec)); + ctx->SetOutputDim("Variances", framework::make_ddim(dim_vec)); + } + +protected: + framework::OpKernelType GetExpectedKernelType( + const framework::ExecutionContext& ctx) const override { + return framework::OpKernelType( + ctx.Input("Input")->type(), ctx.device_context()); + } +}; + +class RotatedAnchorGeneratorOpMaker : public framework::OpProtoAndCheckerMaker { +public: + void Make() override { + AddInput("Input", + "(Tensor, default Tensor), " + "the input feature is a tensor with a rank of 4. " + "The layout is NCHW."); + AddOutput("Anchors", + "(Tensor, default Tensor), the output is a " + "tensor with a rank of 4. The layout is [H, W, num_anchors, 5]. " + "H is the height of input, W is the width of input, num_anchors " + "is the box count of each position. " + "Each anchor is in (xctr, yctr, w, h, thelta) format"); + AddOutput("Variances", + "(Tensor, default Tensor), the expanded variances for " + "normalizing bbox regression targets. The layout is [H, W, " + "num_anchors, 5]. " + "H is the height of input, W is the width of input, num_anchors " + "is the box count of each position. " + "Each variance is in (xctr, yctr, w, h, thelta) format"); + + AddAttr>( + "anchor_sizes", + "(vector) List of Rotated Region Proposal Network(RRPN) anchor " + "sizes " + " given in absolute pixels e.g. (64, 128, 256, 512)." + " For instance, the anchor size of 64 means the area of this anchor " + "equals to 64**2.") + .AddCustomChecker([](const std::vector& anchor_sizes) { + PADDLE_ENFORCE_GT(anchor_sizes.size(), + 0UL, + "Size of anchor_sizes must be at least 1."); + for (size_t i = 0; i < anchor_sizes.size(); ++i) { + PADDLE_ENFORCE_GT( + anchor_sizes[i], 0.0, "anchor_sizes[%d] must be positive.", i); + } + }); + AddAttr>( + "aspect_ratios", + "(vector) List of Rotated Region Proposal Network(RRPN) anchor " + "aspect " + "ratios, e.g. (0.5, 1, 2)." + "For instacne, the aspect ratio of 0.5 means the height / width of " + "this anchor equals 0.5."); + AddAttr>( + "angles", + "(vector) List of Rotated Region Proposal Network(RRPN) anchor " + "angles, " + "e.g. (-30.0, 0.0, 30.0, 60.0, 90.0, 120.0)." + "For instacne, the aspect ratio of 0.5 means the height / width of " + "this anchor equals 0.5."); + + AddAttr>("variances", + "(vector) List of variances to be used " + "in box regression deltas") + .AddCustomChecker([](const std::vector& variances) { + PADDLE_ENFORCE_EQ( + variances.size(), 5UL, "Must and only provide 5 variance."); + for (size_t i = 0; i < variances.size(); ++i) { + PADDLE_ENFORCE_GT( + variances[i], 0.0, "variance[%d] must be greater than 0.", i); + } + }); + + AddAttr>("stride", + "Anchors stride across width and height, " + "with a default of (16, 16)") + .SetDefault(std::vector(2, 16.0)) + .AddCustomChecker([](const std::vector& stride) { + PADDLE_ENFORCE_EQ( + stride.size(), + 2UL, + "Must and only provide 2 stride for width and height."); + for (size_t i = 0; i < stride.size(); ++i) { + PADDLE_ENFORCE_GT( + stride[i], 0.0, "stride[%d] should be larger than 0.", i); + } + }); + + AddAttr("offset", + "(float) " + "Anchor center offset, with a default of 0.5") + .SetDefault(0.5); + AddComment(R"DOC( +RotatedAnchorGenerator operator +Generates anchors for RRPN. algorithm. +Each position of the input produce N anchors, N = + size(anchor_sizes) * size(aspect_ratios) * size(angles). + +Please get more information from the following papers: +https://arxiv.org/abs/1703.01086. +)DOC"); + } +}; + +} // namespace operators +} // namespace paddle + +namespace ops = paddle::operators; +REGISTER_OPERATOR( + rotated_anchor_generator, + ops::RotatedAnchorGeneratorOp, + ops::RotatedAnchorGeneratorOpMaker, + paddle::framework::EmptyGradOpMaker, + paddle::framework::EmptyGradOpMaker); + +REGISTER_OP_CPU_KERNEL(rotated_anchor_generator, + ops::RotatedAnchorGeneratorOpKernel, + ops::RotatedAnchorGeneratorOpKernel); diff --git a/PaddleCV/rrpn/models/ext_op/src/rotated_anchor_generator_op.cu b/PaddleCV/rrpn/models/ext_op/src/rotated_anchor_generator_op.cu new file mode 100644 index 00000000..9c525010 --- /dev/null +++ b/PaddleCV/rrpn/models/ext_op/src/rotated_anchor_generator_op.cu @@ -0,0 +1,153 @@ +/* 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. */ + +#include "rotated_anchor_generator_op.h" + +namespace paddle { +namespace operators { + +template +__global__ void GenRAnchors(T* out, + const T* aspect_ratios, + const int ar_num, + const T* anchor_sizes, + const int as_num, + const T* angles, + const int aa_num, + const T* stride, + const int sd_num, + const int height, + const int width, + const T offset) { + int num_anchors = as_num * ar_num * aa_num; + int box_num = height * width * num_anchors; + for (int i = blockIdx.x * blockDim.x + threadIdx.x; i < box_num; + i += blockDim.x * gridDim.x) { + int h_idx = i / (num_anchors * width); + int w_idx = (i / num_anchors) % width; + T stride_width = stride[0]; + T stride_height = stride[1]; + T x_ctr = (w_idx * stride_width) + offset * stride_width - 1; + T y_ctr = (h_idx * stride_height) + offset * stride_height - 1; + T area, area_ratios; + T base_w, base_h; + T scale_w, scale_h; + T anchor_width, anchor_height; + int anch_idx = i % num_anchors; + int ar_idx = anch_idx / (as_num * aa_num); + int as_idx = anch_idx / aa_num % as_num; + int aa_idx = anch_idx % aa_num; + T aspect_ratio = aspect_ratios[ar_idx]; + T anchor_size = anchor_sizes[as_idx]; + T angle = angles[aa_idx]; + area = stride_width * stride_height; + area_ratios = area / aspect_ratio; + base_w = round(sqrt(area_ratios)); + base_h = round(base_w * aspect_ratio); + scale_w = anchor_size / stride_width; + scale_h = anchor_size / stride_height; + anchor_width = scale_w * base_w; + anchor_height = scale_h * base_h; + out[i * 5] = x_ctr; + out[i * 5 + 1] = y_ctr; + out[i * 5 + 2] = anchor_width; + out[i * 5 + 3] = anchor_height; + out[i * 5 + 4] = angle; + } +} + +template +__global__ void SetVariance(T* out, + const T* var, + const int vnum, + const int num) { + for (int i = blockIdx.x * blockDim.x + threadIdx.x; i < num; + i += blockDim.x * gridDim.x) { + out[i] = var[i % vnum]; + } +} + +template +class RotatedAnchorGeneratorOpCUDAKernel : public framework::OpKernel { +public: + void Compute(const framework::ExecutionContext& ctx) const override { + auto* input = ctx.Input("Input"); + auto* anchors = ctx.Output("Anchors"); + auto* vars = ctx.Output("Variances"); + + auto anchor_sizes = ctx.Attr>("anchor_sizes"); + auto aspect_ratios = ctx.Attr>("aspect_ratios"); + auto angles = ctx.Attr>("angles"); + auto stride = ctx.Attr>("stride"); + auto variances = ctx.Attr>("variances"); + + T offset = static_cast(ctx.Attr("offset")); + + auto width = input->dims()[3]; + auto height = input->dims()[2]; + + int num_anchors = + aspect_ratios.size() * anchor_sizes.size() * angles.size(); + + int box_num = width * height * num_anchors; + + int block = 512; + int grid = (box_num + block - 1) / block; + + auto stream = + ctx.template device_context().stream(); + + anchors->mutable_data(ctx.GetPlace()); + vars->mutable_data(ctx.GetPlace()); + + framework::Tensor ar; + framework::TensorFromVector(aspect_ratios, ctx.device_context(), &ar); + + framework::Tensor as; + framework::TensorFromVector(anchor_sizes, ctx.device_context(), &as); + + framework::Tensor aa; + framework::TensorFromVector(angles, ctx.device_context(), &aa); + + framework::Tensor sd; + framework::TensorFromVector(stride, ctx.device_context(), &sd); + + GenRAnchors<<>>(anchors->data(), + ar.data(), + aspect_ratios.size(), + as.data(), + anchor_sizes.size(), + aa.data(), + angles.size(), + sd.data(), + stride.size(), + height, + width, + offset); + + framework::Tensor v; + framework::TensorFromVector(variances, ctx.device_context(), &v); + grid = (box_num * 5 + block - 1) / block; + SetVariance<<>>( + vars->data(), v.data(), variances.size(), box_num * 5); + } +}; + +} // namespace operators +} // namespace paddle + +namespace ops = paddle::operators; +REGISTER_OP_CUDA_KERNEL(rotated_anchor_generator, + ops::RotatedAnchorGeneratorOpCUDAKernel, + ops::RotatedAnchorGeneratorOpCUDAKernel); diff --git a/PaddleCV/rrpn/models/ext_op/src/rotated_anchor_generator_op.h b/PaddleCV/rrpn/models/ext_op/src/rotated_anchor_generator_op.h new file mode 100644 index 00000000..81239d1f --- /dev/null +++ b/PaddleCV/rrpn/models/ext_op/src/rotated_anchor_generator_op.h @@ -0,0 +1,111 @@ +/* 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. */ + +#pragma once +#include +#include +#include "paddle/fluid/framework/op_registry.h" +//#include "paddle/fluid/operators/math/math_function.h" +#include "paddle/fluid/platform/transform.h" + +namespace paddle { +namespace operators { + +template +class RotatedAnchorGeneratorOpKernel : public framework::OpKernel { +public: + void Compute(const framework::ExecutionContext& ctx) const override { + auto* input = ctx.Input("Input"); + auto* anchors = ctx.Output("Anchors"); + auto* vars = ctx.Output("Variances"); + + auto anchor_sizes = ctx.Attr>("anchor_sizes"); + auto aspect_ratios = ctx.Attr>("aspect_ratios"); + auto angles = ctx.Attr>("angles"); + auto stride = ctx.Attr>("stride"); + auto variances = ctx.Attr>("variances"); + + T offset = static_cast(ctx.Attr("offset")); + + auto feature_width = input->dims()[3]; + auto feature_height = input->dims()[2]; + + T stride_width, stride_height; + stride_width = stride[0]; + stride_height = stride[1]; + + int num_anchors = + aspect_ratios.size() * anchor_sizes.size() * angles.size(); + + anchors->mutable_data(ctx.GetPlace()); + vars->mutable_data(ctx.GetPlace()); + + auto e_anchors = framework::EigenTensor::From(*anchors); + for (int h_idx = 0; h_idx < feature_height; ++h_idx) { + for (int w_idx = 0; w_idx < feature_width; ++w_idx) { + T x_ctr = (w_idx * stride_width) + offset * stride_width - 1; + T y_ctr = (h_idx * stride_height) + offset * stride_height - 1; + T area, area_ratios; + T base_w, base_h; + T scale_w, scale_h; + T anchor_width, anchor_height; + int idx = 0; + for (size_t r = 0; r < aspect_ratios.size(); ++r) { + auto ar = aspect_ratios[r]; + for (size_t s = 0; s < anchor_sizes.size(); ++s) { + auto anchor_size = anchor_sizes[s]; + area = stride_width * stride_height; + area_ratios = area / ar; + base_w = round(sqrt(area_ratios)); + base_h = round(base_w * ar); + scale_w = anchor_size / stride_width; + scale_h = anchor_size / stride_height; + anchor_width = scale_w * base_w; + anchor_height = scale_h * base_h; + for (size_t a = 0; a < angles.size(); ++a) { + auto angle = angles[a]; + e_anchors(h_idx, w_idx, idx, 0) = x_ctr; + e_anchors(h_idx, w_idx, idx, 1) = y_ctr; + e_anchors(h_idx, w_idx, idx, 2) = anchor_width; + e_anchors(h_idx, w_idx, idx, 3) = anchor_height; + e_anchors(h_idx, w_idx, idx, 4) = angle; + idx++; + } + } + } + } + } + + framework::Tensor var_t; + var_t.mutable_data( + framework::make_ddim({1, static_cast(variances.size())}), + ctx.GetPlace()); + auto var_et = framework::EigenTensor::From(var_t); + for (size_t i = 0; i < variances.size(); ++i) { + var_et(0, i) = variances[i]; + } + + int anchor_num = feature_height * feature_width * num_anchors; + auto var_dim = vars->dims(); + vars->Resize({anchor_num, static_cast(variances.size())}); + + auto e_vars = framework::EigenMatrix::From(*vars); + e_vars = var_et.broadcast(Eigen::DSizes(anchor_num, 1)); + + vars->Resize(var_dim); + } +}; + +} // namespace operators +} // namespace paddle diff --git a/PaddleCV/rrpn/models/ext_op/src/rrpn_box_coder_op.cc b/PaddleCV/rrpn/models/ext_op/src/rrpn_box_coder_op.cc new file mode 100644 index 00000000..63c6c6e9 --- /dev/null +++ b/PaddleCV/rrpn/models/ext_op/src/rrpn_box_coder_op.cc @@ -0,0 +1,128 @@ +/* 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. */ + +//#include "rrpn_box_coder_op.h" +#include +#include +#include "paddle/fluid/framework/op_registry.h" + +namespace paddle { +namespace operators { + +class RRPNBoxCoderOp : public framework::OperatorWithKernel { +public: + using framework::OperatorWithKernel::OperatorWithKernel; + +protected: + void InferShape(framework::InferShapeContext *ctx) const override { + PADDLE_ENFORCE(ctx->HasInput("PriorBox"), + "Input(PriorBox) of BoxCoderOp should not be null."); + PADDLE_ENFORCE(ctx->HasInput("TargetBox"), + "Input(TargetBox) of BoxCoderOp should not be null."); + PADDLE_ENFORCE(ctx->HasOutput("OutputBox"), + "Output(OutputBox) of BoxCoderOp should not be null."); + + auto prior_box_dims = ctx->GetInputDim("PriorBox"); + // auto target_box_dims = ctx->GetInputDim("TargetBox"); + + if (ctx->IsRuntime()) { + PADDLE_ENFORCE_EQ( + prior_box_dims.size(), 2, "The rank of Input PriorBox must be 2"); + PADDLE_ENFORCE_EQ( + prior_box_dims[1], 5, "The shape of PriorBox is [N, 5]"); + if (ctx->HasInput("PriorBoxVar")) { + auto prior_box_var_dims = ctx->GetInputDim("PriorBoxVar"); + PADDLE_ENFORCE(prior_box_var_dims.size() == 2, + "Input(PriorBoxVar) of BoxCoderOp should be 2."); + PADDLE_ENFORCE_EQ( + prior_box_dims, + prior_box_var_dims, + "The dimension of Input(PriorBoxVar) should be equal to" + "the dimension of Input(PriorBox) when the rank is 2."); + } + } + } +}; + +class RRPNBoxCoderOpMaker : public framework::OpProtoAndCheckerMaker { +public: + void Make() override { + AddInput( + "PriorBox", + "(Tensor, default Tensor) " + "Box list PriorBox is a 2-D Tensor with shape [M, 5] holds M boxes, " + "each box is represented as [x, y, w, h, angle], " + "[x, y] is the center coordinate of the anchor box, " + "if the input is image feature map, they are close to the origin " + "of the coordinate system. [w, h] is the width and height " + "of the anchor box, angle is angle of rotation."); + AddInput("PriorBoxVar", + "(Tensor, default Tensor, optional) " + "PriorBoxVar is a 2-D Tensor with shape [M, 5] holds M group " + "of variance. PriorBoxVar will set all elements to 1 by " + "default.") + .AsDispensable(); + AddInput( + "TargetBox", + "(LoDTensor or Tensor) This input can be a 2-D LoDTensor with shape " + "[N, 5], each box is represented as [x, y, w, h, angle]," + "[x, y] is the center coordinate of the box, [w, h] is width and " + "height of the box," + "angle is angle of rotation around the center of box."); + AddAttr>( + "variance", + "(vector, default {})," + "variance of prior box with shape [5]. PriorBoxVar and variance can" + "not be provided at the same time.") + .SetDefault(std::vector{}); + AddOutput("OutputBox", + "(Tensor) " + "2-D Tensor with shape [M, 5] which M represents the number of " + "deocded boxes" + "and 5 represents [x, y, w, h, angle]"); + + AddComment(R"DOC( + +Rotatedi Bounding Box Coder. + +Decode the target bounding box with the priorbox information. + +The Decoding schema described below: + + ox = pw * tx / pxv + cx + + oy = ph * ty / pyv + cy + + ow = exp(tw / pwv) * pw + + oh = exp(th / phv) * ph + + oa = ta / pav * 1.0 / 3.141592653 * 180 + pa + +where `tx`, `ty`, `tw`, `th`, `ta` denote the target box's center coordinates, width +,height and angle respectively. Similarly, `px`, `py`, `pw`, `ph`, `pa` denote the +priorbox's (anchor) center coordinates, width, height and angle. `pxv`, `pyv`, `pwv`, +`phv`, `pav` denote the variance of the priorbox and `ox`, `oy`, `ow`, `oh`, `oa` +denote the encoded/decoded coordinates, width and height. +)DOC"); + } +}; + +} // namespace operators +} // namespace paddle + +namespace ops = paddle::operators; +REGISTER_OPERATOR( + rrpn_box_coder, + ops::RRPNBoxCoderOp, + ops::RRPNBoxCoderOpMaker, + paddle::framework::EmptyGradOpMaker, + paddle::framework::EmptyGradOpMaker); diff --git a/PaddleCV/rrpn/models/ext_op/src/rrpn_box_coder_op.cu b/PaddleCV/rrpn/models/ext_op/src/rrpn_box_coder_op.cu new file mode 100644 index 00000000..9640f0ff --- /dev/null +++ b/PaddleCV/rrpn/models/ext_op/src/rrpn_box_coder_op.cu @@ -0,0 +1,198 @@ +/* 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. */ + +#include +#include +#include +#include +#include "paddle/fluid/memory/memory.h" +//#include "rrpn_box_coder_op.h" +#include "paddle/fluid/framework/op_registry.h" +#include "paddle/fluid/platform/cuda_primitives.h" + +namespace paddle { +namespace operators { + +#define PI 3.141592654 + +template +__global__ void DecodeCenterSizeKernel(const T* prior_box_data, + const T* prior_box_var_data, + const T* target_box_data, + const int row, + const int len, + const T prior_box_var_size, + const float* variance, + const int var_size, + T* output) { + const int idx = threadIdx.x + blockIdx.x * blockDim.x; + int prior_box_offset = 0; + if (idx < row) { + const int row_idx = idx; + prior_box_offset = row_idx * len; + T prior_box_width = prior_box_data[prior_box_offset + 2]; + + T prior_box_height = prior_box_data[prior_box_offset + 3]; + + T prior_box_center_x = prior_box_data[prior_box_offset]; + T prior_box_center_y = prior_box_data[prior_box_offset + 1]; + T prior_box_angle = prior_box_data[prior_box_offset + 4]; + + T target_box_width, target_box_height, target_box_angle; + T target_box_center_x, target_box_center_y; + T box_var_x = T(1), box_var_y = T(1); + T box_var_w = T(1), box_var_h = T(1), box_var_angle = T(1); + if (prior_box_var_data) { + int prior_var_offset = row_idx * len; + box_var_x = prior_box_var_data[prior_var_offset]; + box_var_y = prior_box_var_data[prior_var_offset + 1]; + box_var_w = prior_box_var_data[prior_var_offset + 2]; + box_var_h = prior_box_var_data[prior_var_offset + 3]; + box_var_angle = prior_box_var_data[prior_var_offset + 4]; + } else if (var_size == 5) { + box_var_x = static_cast(variance[0]); + box_var_y = static_cast(variance[1]); + box_var_w = static_cast(variance[2]); + box_var_h = static_cast(variance[3]); + box_var_angle = static_cast(variance[4]); + } + target_box_width = + exp(target_box_data[idx * len + 2] / box_var_w) * prior_box_width / 1.4; + target_box_height = exp(target_box_data[idx * len + 3] / box_var_h) * + prior_box_height / 1.4; + target_box_center_x = + target_box_data[idx * len] / box_var_x * prior_box_width + + prior_box_center_x; + target_box_center_y = + target_box_data[idx * len + 1] / box_var_y * prior_box_height + + prior_box_center_y; + + target_box_angle = + (target_box_data[idx * len + 4] / box_var_angle) * 1.0 / PI * 180 + + prior_box_angle; + + T a_cos = cos(PI / 180 * target_box_angle); + T a_sin = -sin(PI / 180 * target_box_angle); + + T rotation_matrix[3][3]; + + rotation_matrix[0][0] = a_cos; + rotation_matrix[0][1] = a_sin; + rotation_matrix[0][2] = 0; + rotation_matrix[1][0] = -a_sin; + rotation_matrix[1][1] = a_cos; + rotation_matrix[1][2] = 0; + rotation_matrix[2][0] = -target_box_center_x * a_cos + + target_box_center_y * a_sin + target_box_center_x; + rotation_matrix[2][1] = -target_box_center_x * a_sin - + target_box_center_y * a_cos + target_box_center_y; + rotation_matrix[2][2] = 1; + + T pt_x0 = target_box_center_x - target_box_width / 2; + T pt_x1 = target_box_center_x + target_box_width / 2; + T pt_x2 = target_box_center_x + target_box_width / 2; + T pt_x3 = target_box_center_x - target_box_width / 2; + + T pt_y0 = target_box_center_y - target_box_height / 2; + T pt_y1 = target_box_center_y - target_box_height / 2; + T pt_y2 = target_box_center_y + target_box_height / 2; + T pt_y3 = target_box_center_y + target_box_height / 2; + + + output[idx * 8] = pt_x0 * rotation_matrix[0][0] + + pt_y0 * rotation_matrix[1][0] + rotation_matrix[2][0]; + output[idx * 8 + 1] = pt_x0 * rotation_matrix[0][1] + + pt_y0 * rotation_matrix[1][1] + rotation_matrix[2][1]; + output[idx * 8 + 2] = pt_x1 * rotation_matrix[0][0] + + pt_y1 * rotation_matrix[1][0] + rotation_matrix[2][0]; + output[idx * 8 + 3] = pt_x1 * rotation_matrix[0][1] + + pt_y1 * rotation_matrix[1][1] + rotation_matrix[2][1]; + output[idx * 8 + 4] = pt_x2 * rotation_matrix[0][0] + + pt_y2 * rotation_matrix[1][0] + rotation_matrix[2][0]; + output[idx * 8 + 5] = pt_x2 * rotation_matrix[0][1] + + pt_y2 * rotation_matrix[1][1] + rotation_matrix[2][1]; + output[idx * 8 + 6] = pt_x3 * rotation_matrix[0][0] + + pt_y3 * rotation_matrix[1][0] + rotation_matrix[2][0]; + output[idx * 8 + 7] = pt_x3 * rotation_matrix[0][1] + + pt_y3 * rotation_matrix[1][1] + rotation_matrix[2][1]; + } +} + +template +class RRPNBoxCoderCUDAKernel : public framework::OpKernel { +public: + void Compute(const framework::ExecutionContext& context) const override { + PADDLE_ENFORCE(platform::is_gpu_place(context.GetPlace()), + "This kernel only runs on GPU device."); + auto* prior_box = context.Input("PriorBox"); + auto* prior_box_var = context.Input("PriorBoxVar"); + auto* target_box = context.Input("TargetBox"); + auto* output_box = context.Output("OutputBox"); + std::vector variance = context.Attr>("variance"); + const T* prior_box_data = prior_box->data(); + const T* target_box_data = target_box->data(); + const T* prior_box_var_data = nullptr; + auto prior_box_var_size = 0; + if (prior_box_var) { + PADDLE_ENFORCE(variance.empty(), + "Input 'PriorBoxVar' and attribute 'variance' should not" + "be used at the same time."); + prior_box_var_data = prior_box_var->data(); + prior_box_var_size = prior_box_var->dims().size(); + } + if (!(variance.empty())) { + PADDLE_ENFORCE(static_cast(variance.size()) == 5, + "Size of attribute 'variance' should be 4"); + } + + if (target_box->lod().size()) { + PADDLE_ENFORCE_EQ( + target_box->lod().size(), 1, "Only support 1 level of LoD."); + } + const int var_size = static_cast(variance.size()); + auto row = target_box->dims()[0]; + auto len = 5; + int block = 512; + int grid = (row + block - 1) / block; + auto& device_ctx = context.cuda_device_context(); + + int bytes = var_size * sizeof(float); + auto dev_var = memory::Alloc(device_ctx, bytes); + float* dev_var_data = reinterpret_cast(dev_var->ptr()); + auto cplace = platform::CPUPlace(); + const auto gplace = boost::get(context.GetPlace()); + memory::Copy( + gplace, dev_var_data, cplace, &variance[0], bytes, device_ctx.stream()); + + output_box->mutable_data({row, 8}, context.GetPlace()); + T* output = output_box->data(); + + DecodeCenterSizeKernel<<>>( + prior_box_data, + prior_box_var_data, + target_box_data, + row, + len, + prior_box_var_size, + dev_var_data, + var_size, + output); + } +}; + +} // namespace operators +} // namespace paddle + +namespace ops = paddle::operators; +REGISTER_OP_CUDA_KERNEL( + rrpn_box_coder, + ops::RRPNBoxCoderCUDAKernel, + ops::RRPNBoxCoderCUDAKernel); diff --git a/PaddleCV/rrpn/models/ext_op/src/rrpn_generate_proposal_labels_op.cc b/PaddleCV/rrpn/models/ext_op/src/rrpn_generate_proposal_labels_op.cc new file mode 100644 index 00000000..3174df86 --- /dev/null +++ b/PaddleCV/rrpn/models/ext_op/src/rrpn_generate_proposal_labels_op.cc @@ -0,0 +1,638 @@ +/* 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. */ + +#include +#include +#include +#include +#include +#include "bbox_util.h" +#include "concat_and_split.h" +#include "gather.h" +#include "math_function.h" +#include "paddle/fluid/framework/op_registry.h" + +namespace paddle { +namespace operators { + +using Tensor = framework::Tensor; +using LoDTensor = framework::LoDTensor; +const int kBoxDim = 5; + +template +void AppendRois(LoDTensor* out, int64_t offset, Tensor* to_add) { + auto* out_data = out->data(); + auto* to_add_data = to_add->data(); + memcpy(out_data + offset, to_add_data, to_add->numel() * sizeof(T)); +} + + +class RRPNGenerateProposalLabelsOp : public framework::OperatorWithKernel { +public: + using framework::OperatorWithKernel::OperatorWithKernel; + + void InferShape(framework::InferShapeContext* ctx) const override { + PADDLE_ENFORCE(ctx->HasInput("RpnRois"), + "Input(RpnRois) shouldn't be null."); + PADDLE_ENFORCE(ctx->HasInput("GtClasses"), + "Input(GtClasses) shouldn't be null."); + PADDLE_ENFORCE(ctx->HasInput("IsCrowd"), + "Input(IsCrowd) shouldn't be null."); + PADDLE_ENFORCE(ctx->HasInput("GtBoxes"), + "Input(GtBoxes) shouldn't be null."); + PADDLE_ENFORCE(ctx->HasInput("ImInfo"), "Input(ImInfo) shouldn't be null."); + + PADDLE_ENFORCE( + ctx->HasOutput("Rois"), + "Output(Rois) of RRPNGenerateProposalLabelsOp should not be null"); + PADDLE_ENFORCE(ctx->HasOutput("LabelsInt32"), + "Output(LabelsInt32) of RRPNGenerateProposalLabelsOp should " + "not be null"); + PADDLE_ENFORCE(ctx->HasOutput("BboxTargets"), + "Output(BboxTargets) of RRPNGenerateProposalLabelsOp should " + "not be null"); + PADDLE_ENFORCE(ctx->HasOutput("BboxInsideWeights"), + "Output(BboxInsideWeights) of RRPNGenerateProposalLabelsOp " + "should not be null"); + PADDLE_ENFORCE(ctx->HasOutput("BboxOutsideWeights"), + "Output(BboxOutsideWeights) of RRPNGenerateProposalLabelsOp " + "should not be null"); + + auto rpn_rois_dims = ctx->GetInputDim("RpnRois"); + auto gt_boxes_dims = ctx->GetInputDim("GtBoxes"); + auto im_info_dims = ctx->GetInputDim("ImInfo"); + + PADDLE_ENFORCE_EQ( + rpn_rois_dims.size(), 2, "The rank of Input(RpnRois) must be 2."); + PADDLE_ENFORCE_EQ( + gt_boxes_dims.size(), 2, "The rank of Input(GtBoxes) must be 2."); + PADDLE_ENFORCE_EQ( + im_info_dims.size(), 2, "The rank of Input(ImInfo) must be 2."); + + int class_nums = ctx->Attrs().Get("class_nums"); + + ctx->SetOutputDim("Rois", {-1, 5}); + ctx->SetOutputDim("LabelsInt32", {-1, 1}); + ctx->SetOutputDim("BboxTargets", {-1, 5 * class_nums}); + ctx->SetOutputDim("BboxInsideWeights", {-1, 5 * class_nums}); + ctx->SetOutputDim("BboxOutsideWeights", {-1, 5 * class_nums}); + } + +protected: + framework::OpKernelType GetExpectedKernelType( + const framework::ExecutionContext& ctx) const override { + return framework::OpKernelType( + ctx.Input("RpnRois")->type(), + platform::CPUPlace()); + } +}; + +template +void Concat(const platform::CPUDeviceContext& context, + const Tensor& in_tensor_a, + const Tensor& in_tensor_b, + Tensor* out_tensor) { + int axis = 0; + std::vector inputs; + inputs.emplace_back(in_tensor_a); + inputs.emplace_back(in_tensor_b); + math::ConcatFunctor concat_functor; + concat_functor(context, inputs, axis, out_tensor); +} + +template +std::vector> SampleFgBgGt( + const platform::CPUDeviceContext& context, + Tensor* iou, + const Tensor& is_crowd, + const int batch_size_per_im, + const float fg_fraction, + const float fg_thresh, + const float bg_thresh_hi, + const float bg_thresh_lo, + std::minstd_rand engine, + const bool use_random, + const Tensor& rpn_rois) { + std::vector fg_inds; + std::vector bg_inds; + std::vector mapped_gt_inds; + int64_t gt_num = is_crowd.numel(); + const int* crowd_data = is_crowd.data(); + T* proposal_to_gt_overlaps = iou->data(); + int64_t row = iou->dims()[0]; + int64_t col = iou->dims()[1]; + float epsilon = 0.00001; + const T* rpn_rois_dt = rpn_rois.data(); + // Follow the Faster RCNN's implementation + for (int64_t i = 0; i < row; ++i) { + const T* v = proposal_to_gt_overlaps + i * col; + T max_overlap = *std::max_element(v, v + col); + if ((i < gt_num) && (crowd_data[i])) { + max_overlap = -1.0; + } + if (max_overlap >= fg_thresh) { + // fg mapped gt label index + for (int64_t j = 0; j < col; ++j) { + T val = proposal_to_gt_overlaps[i * col + j]; + auto diff = std::abs(max_overlap - val); + if (diff < epsilon) { + fg_inds.emplace_back(i); + mapped_gt_inds.emplace_back(j); + break; + } + } + } else if ((max_overlap >= bg_thresh_lo) && (max_overlap < bg_thresh_hi)) { + bg_inds.emplace_back(i); + } else { + continue; + } + } + + std::vector> res; + // sampling fg + std::uniform_real_distribution uniform(0, 1); + int fg_rois_per_im = std::floor(batch_size_per_im * fg_fraction); + int fg_rois_this_image = fg_inds.size(); + int fg_rois_per_this_image = std::min(fg_rois_per_im, fg_rois_this_image); + if (use_random) { + const int64_t fg_size = static_cast(fg_inds.size()); + if (fg_size > fg_rois_per_this_image) { + for (int64_t i = fg_rois_per_this_image; i < fg_size; ++i) { + int rng_ind = std::floor(uniform(engine) * i); + if (rng_ind < fg_rois_per_this_image) { + std::iter_swap(fg_inds.begin() + rng_ind, fg_inds.begin() + i); + std::iter_swap(mapped_gt_inds.begin() + rng_ind, + mapped_gt_inds.begin() + i); + } + } + } + } + std::vector new_fg_inds(fg_inds.begin(), + fg_inds.begin() + fg_rois_per_this_image); + std::vector new_gt_inds(mapped_gt_inds.begin(), + mapped_gt_inds.begin() + fg_rois_per_this_image); + // sampling bg + int bg_rois_per_image = batch_size_per_im - fg_rois_per_this_image; + int bg_rois_this_image = bg_inds.size(); + int bg_rois_per_this_image = std::min(bg_rois_per_image, bg_rois_this_image); + if (use_random) { + const int64_t bg_size = static_cast(bg_inds.size()); + if (bg_size > bg_rois_per_this_image) { + for (int64_t i = bg_rois_per_this_image; i < bg_size; ++i) { + int rng_ind = std::floor(uniform(engine) * i); + if (rng_ind < fg_rois_per_this_image) + std::iter_swap(bg_inds.begin() + rng_ind, bg_inds.begin() + i); + } + } + } + std::vector new_bg_inds(bg_inds.begin(), + bg_inds.begin() + bg_rois_per_this_image); + res.emplace_back(new_fg_inds); + res.emplace_back(new_bg_inds); + res.emplace_back(new_gt_inds); + + return res; +} + +template +void GatherBoxesLabels(const platform::CPUDeviceContext& context, + const Tensor& boxes, + const Tensor& gt_boxes, + const Tensor& gt_classes, + const std::vector& fg_inds, + const std::vector& bg_inds, + const std::vector& gt_inds, + Tensor* sampled_boxes, + Tensor* sampled_labels, + Tensor* sampled_gts) { + int fg_num = fg_inds.size(); + int bg_num = bg_inds.size(); + Tensor fg_inds_t, bg_inds_t, gt_box_inds_t, gt_label_inds_t; + int* fg_inds_data = fg_inds_t.mutable_data({fg_num}, context.GetPlace()); + int* bg_inds_data = bg_inds_t.mutable_data({bg_num}, context.GetPlace()); + int* gt_box_inds_data = + gt_box_inds_t.mutable_data({fg_num}, context.GetPlace()); + int* gt_label_inds_data = + gt_label_inds_t.mutable_data({fg_num}, context.GetPlace()); + std::copy(fg_inds.begin(), fg_inds.end(), fg_inds_data); + std::copy(bg_inds.begin(), bg_inds.end(), bg_inds_data); + std::copy(gt_inds.begin(), gt_inds.end(), gt_box_inds_data); + std::copy(gt_inds.begin(), gt_inds.end(), gt_label_inds_data); + + Tensor fg_boxes, bg_boxes, fg_labels, bg_labels; + fg_boxes.mutable_data({fg_num, kBoxDim}, context.GetPlace()); + CPUGather(context, boxes, fg_inds_t, &fg_boxes); + bg_boxes.mutable_data({bg_num, kBoxDim}, context.GetPlace()); + CPUGather(context, boxes, bg_inds_t, &bg_boxes); + Concat(context, fg_boxes, bg_boxes, sampled_boxes); + CPUGather(context, gt_boxes, gt_box_inds_t, sampled_gts); + fg_labels.mutable_data({fg_num}, context.GetPlace()); + CPUGather(context, gt_classes, gt_label_inds_t, &fg_labels); + bg_labels.mutable_data({bg_num}, context.GetPlace()); + math::set_constant(context, &bg_labels, 0); + Concat(context, fg_labels, bg_labels, sampled_labels); +} + +template +std::vector SampleRoisForOneImage( + const platform::CPUDeviceContext& context, + const Tensor& rpn_rois_in, + const Tensor& gt_classes, + const Tensor& is_crowd, + const Tensor& gt_boxes, + const Tensor& im_info, + const int batch_size_per_im, + const float fg_fraction, + const float fg_thresh, + const float bg_thresh_hi, + const float bg_thresh_lo, + const std::vector& bbox_reg_weights, + const int class_nums, + std::minstd_rand engine, + bool use_random, + bool is_cls_agnostic) { + // 1.1 map to original image + auto im_scale = im_info.data()[2]; + Tensor rpn_rois_slice; + Tensor rpn_rois; + + rpn_rois.mutable_data(rpn_rois_in.dims(), context.GetPlace()); + const T* rpn_rois_in_dt = rpn_rois_in.data(); + T* rpn_rois_dt = rpn_rois.data(); + for (int i = 0; i < rpn_rois.numel(); ++i) { + rpn_rois_dt[i] = rpn_rois_in_dt[i]; + } + + // 1.2 compute overlaps + int proposals_num = gt_boxes.dims()[0] + rpn_rois.dims()[0]; + Tensor boxes; + boxes.mutable_data({proposals_num, kBoxDim}, context.GetPlace()); + Concat(context, gt_boxes, rpn_rois, &boxes); + Tensor proposal_to_gt_overlaps; + proposal_to_gt_overlaps.mutable_data({proposals_num, gt_boxes.dims()[0]}, + context.GetPlace()); + BboxOverlaps2(boxes, gt_boxes, &proposal_to_gt_overlaps); + std::vector> fg_bg_gt = + SampleFgBgGt(context, + &proposal_to_gt_overlaps, + is_crowd, + batch_size_per_im, + fg_fraction, + fg_thresh, + bg_thresh_hi, + bg_thresh_lo, + engine, + use_random, + boxes); + std::vector fg_inds = fg_bg_gt[0]; + std::vector bg_inds = fg_bg_gt[1]; + std::vector mapped_gt_inds = fg_bg_gt[2]; // mapped_gt_labels + + + Tensor sampled_boxes, sampled_labels, sampled_gts; + int fg_num = fg_inds.size(); + int bg_num = bg_inds.size(); + int boxes_num = fg_num + bg_num; + framework::DDim bbox_dim({boxes_num, kBoxDim}); + + sampled_boxes.mutable_data(bbox_dim, context.GetPlace()); + + sampled_labels.mutable_data({boxes_num}, context.GetPlace()); + + sampled_gts.mutable_data({fg_num, kBoxDim}, context.GetPlace()); + + GatherBoxesLabels(context, + boxes, + gt_boxes, + gt_classes, + fg_inds, + bg_inds, + mapped_gt_inds, + &sampled_boxes, + &sampled_labels, + &sampled_gts); + + // Compute targets + Tensor bbox_targets_single; + bbox_targets_single.mutable_data(bbox_dim, context.GetPlace()); + BoxToDelta2(fg_num, + sampled_boxes, + sampled_gts, + bbox_reg_weights.data(), + &bbox_targets_single); + + // Scale rois + Tensor sampled_rois; + sampled_rois.mutable_data(sampled_boxes.dims(), context.GetPlace()); + auto sampled_rois_et = framework::EigenTensor::From(sampled_rois); + auto sampled_boxes_et = framework::EigenTensor::From(sampled_boxes); + + sampled_rois_et = sampled_boxes_et; + // Expand box targets + Tensor bbox_targets, bbox_inside_weights, bbox_outside_weights; + framework::DDim bbox_expand_dim({boxes_num, kBoxDim * class_nums}); + bbox_targets.mutable_data(bbox_expand_dim, context.GetPlace()); + bbox_inside_weights.mutable_data(bbox_expand_dim, context.GetPlace()); + bbox_outside_weights.mutable_data(bbox_expand_dim, context.GetPlace()); + math::set_constant(context, &bbox_targets, 0.0); + math::set_constant(context, &bbox_inside_weights, 0.0); + math::set_constant(context, &bbox_outside_weights, 0.0); + + auto* bbox_targets_single_data = bbox_targets_single.data(); + auto* sampled_labels_data = sampled_labels.data(); + auto* bbox_targets_data = bbox_targets.data(); + auto* bbox_inside_weights_data = bbox_inside_weights.data(); + auto* bbox_outside_weights_data = bbox_outside_weights.data(); + int width = kBoxDim * class_nums; + + for (int64_t i = 0; i < boxes_num; ++i) { + int label = sampled_labels_data[i]; + + if (label > 0) { + if (is_cls_agnostic) { + label = 1; + } + + int dst_idx = i * width + kBoxDim * label; + int src_idx = kBoxDim * i; + bbox_targets_data[dst_idx] = bbox_targets_single_data[src_idx]; + bbox_targets_data[dst_idx + 1] = bbox_targets_single_data[src_idx + 1]; + bbox_targets_data[dst_idx + 2] = bbox_targets_single_data[src_idx + 2]; + bbox_targets_data[dst_idx + 3] = bbox_targets_single_data[src_idx + 3]; + bbox_targets_data[dst_idx + 4] = bbox_targets_single_data[src_idx + 4]; + + bbox_inside_weights_data[dst_idx] = 1; + bbox_inside_weights_data[dst_idx + 1] = 1; + bbox_inside_weights_data[dst_idx + 2] = 1; + bbox_inside_weights_data[dst_idx + 3] = 1; + bbox_inside_weights_data[dst_idx + 4] = 1; + + + bbox_outside_weights_data[dst_idx] = 1; + bbox_outside_weights_data[dst_idx + 1] = 1; + bbox_outside_weights_data[dst_idx + 2] = 1; + bbox_outside_weights_data[dst_idx + 3] = 1; + bbox_outside_weights_data[dst_idx + 4] = 1; + } + } + + + std::vector res; + res.emplace_back(sampled_rois); + res.emplace_back(sampled_labels); + res.emplace_back(bbox_targets); + res.emplace_back(bbox_inside_weights); + res.emplace_back(bbox_outside_weights); + return res; +} + +template +class RRPNGenerateProposalLabelsKernel : public framework::OpKernel { +public: + void Compute(const framework::ExecutionContext& context) const override { + auto* rpn_rois = context.Input("RpnRois"); + auto* gt_classes = context.Input("GtClasses"); + auto* is_crowd = context.Input("IsCrowd"); + auto* gt_boxes = context.Input("GtBoxes"); + auto* im_info = context.Input("ImInfo"); + + auto* rois = context.Output("Rois"); + auto* labels_int32 = context.Output("LabelsInt32"); + auto* bbox_targets = context.Output("BboxTargets"); + auto* bbox_inside_weights = context.Output("BboxInsideWeights"); + auto* bbox_outside_weights = + context.Output("BboxOutsideWeights"); + + int batch_size_per_im = context.Attr("batch_size_per_im"); + float fg_fraction = context.Attr("fg_fraction"); + float fg_thresh = context.Attr("fg_thresh"); + float bg_thresh_hi = context.Attr("bg_thresh_hi"); + float bg_thresh_lo = context.Attr("bg_thresh_lo"); + std::vector bbox_reg_weights = + context.Attr>("bbox_reg_weights"); + int class_nums = context.Attr("class_nums"); + bool use_random = context.Attr("use_random"); + bool is_cls_agnostic = context.Attr("is_cls_agnostic"); + PADDLE_ENFORCE_EQ( + rpn_rois->lod().size(), + 1UL, + "RRPNGenerateProposalLabelsOp rpn_rois needs 1 level of LoD"); + PADDLE_ENFORCE_EQ( + gt_classes->lod().size(), + 1UL, + "RRPNGenerateProposalLabelsOp gt_classes needs 1 level of LoD"); + PADDLE_ENFORCE_EQ( + is_crowd->lod().size(), + 1UL, + "RRPNGenerateProposalLabelsOp is_crowd needs 1 level of LoD"); + PADDLE_ENFORCE_EQ( + gt_boxes->lod().size(), + 1UL, + "RRPNGenerateProposalLabelsOp gt_boxes needs 1 level of LoD"); + int64_t n = static_cast(rpn_rois->lod().back().size() - 1); + + rois->mutable_data({n * batch_size_per_im, kBoxDim}, context.GetPlace()); + labels_int32->mutable_data({n * batch_size_per_im, 1}, + context.GetPlace()); + bbox_targets->mutable_data({n * batch_size_per_im, kBoxDim * class_nums}, + context.GetPlace()); + bbox_inside_weights->mutable_data( + {n * batch_size_per_im, kBoxDim * class_nums}, context.GetPlace()); + bbox_outside_weights->mutable_data( + {n * batch_size_per_im, kBoxDim * class_nums}, context.GetPlace()); + + std::random_device rnd; + std::minstd_rand engine; + int seed = rnd(); + engine.seed(seed); + + framework::LoD lod; + std::vector lod0(1, 0); + + int64_t num_rois = 0; + auto& dev_ctx = context.device_context(); + + auto rpn_rois_lod = rpn_rois->lod().back(); + auto gt_classes_lod = gt_classes->lod().back(); + auto is_crowd_lod = is_crowd->lod().back(); + auto gt_boxes_lod = gt_boxes->lod().back(); + + for (int i = 0; i < n; ++i) { + if (rpn_rois_lod[i] == rpn_rois_lod[i + 1]) { + lod0.emplace_back(num_rois); + continue; + } + Tensor rpn_rois_slice = + rpn_rois->Slice(rpn_rois_lod[i], rpn_rois_lod[i + 1]); + Tensor gt_classes_slice = + gt_classes->Slice(gt_classes_lod[i], gt_classes_lod[i + 1]); + Tensor is_crowd_slice = + is_crowd->Slice(is_crowd_lod[i], is_crowd_lod[i + 1]); + Tensor gt_boxes_slice = + gt_boxes->Slice(gt_boxes_lod[i], gt_boxes_lod[i + 1]); + Tensor im_info_slice = im_info->Slice(i, i + 1); + + std::vector tensor_output = + SampleRoisForOneImage(dev_ctx, + rpn_rois_slice, + gt_classes_slice, + is_crowd_slice, + gt_boxes_slice, + im_info_slice, + batch_size_per_im, + fg_fraction, + fg_thresh, + bg_thresh_hi, + bg_thresh_lo, + bbox_reg_weights, + class_nums, + engine, + use_random, + is_cls_agnostic); + Tensor sampled_rois = tensor_output[0]; + Tensor sampled_labels_int32 = tensor_output[1]; + Tensor sampled_bbox_targets = tensor_output[2]; + Tensor sampled_bbox_inside_weights = tensor_output[3]; + Tensor sampled_bbox_outside_weights = tensor_output[4]; + + AppendRois(rois, kBoxDim * num_rois, &sampled_rois); + AppendRois(labels_int32, num_rois, &sampled_labels_int32); + AppendRois( + bbox_targets, kBoxDim * num_rois * class_nums, &sampled_bbox_targets); + AppendRois(bbox_inside_weights, + kBoxDim * num_rois * class_nums, + &sampled_bbox_inside_weights); + AppendRois(bbox_outside_weights, + kBoxDim * num_rois * class_nums, + &sampled_bbox_outside_weights); + + num_rois += sampled_rois.dims()[0]; + lod0.emplace_back(num_rois); + } + + lod.emplace_back(lod0); + rois->set_lod(lod); + labels_int32->set_lod(lod); + bbox_targets->set_lod(lod); + bbox_inside_weights->set_lod(lod); + bbox_outside_weights->set_lod(lod); + rois->Resize({num_rois, kBoxDim}); + labels_int32->Resize({num_rois, 1}); + bbox_targets->Resize({num_rois, kBoxDim * class_nums}); + bbox_inside_weights->Resize({num_rois, kBoxDim * class_nums}); + bbox_outside_weights->Resize({num_rois, kBoxDim * class_nums}); + } +}; + +class RRPNGenerateProposalLabelsOpMaker + : public framework::OpProtoAndCheckerMaker { +public: + void Make() override { + AddInput("RpnRois", + "(LoDTensor), This input is a 2D LoDTensor with shape [N, 5]. " + "N is the number of the GenerateProposalOp's output, " + "each element is a bounding box with [x, y, w, h, angle] format."); + AddInput("GtClasses", + "(LoDTensor), This input is a 2D LoDTensor with shape [M, 1]. " + "M is the number of groundtruth, " + "each element is a class label of groundtruth."); + AddInput( + "IsCrowd", + "(LoDTensor), This input is a 2D LoDTensor with shape [M, 1]. " + "M is the number of groundtruth, " + "each element is a flag indicates whether a groundtruth is crowd."); + AddInput("GtBoxes", + "(LoDTensor), This input is a 2D LoDTensor with shape [M, 5. " + "M is the number of groundtruth, " + "each element is a bounding box with [x, y, w, h, angle] format."); + AddInput("ImInfo", + "(Tensor), This input is a 2D Tensor with shape [B, 3]. " + "B is the number of input images, " + "each element consists of im_height, im_width, im_scale."); + + AddOutput( + "Rois", + "(LoDTensor), This output is a 2D LoDTensor with shape [P, 5]. " + "P usuall equal to batch_size_per_im * batch_size, " + "each element is a bounding box with [x, y, w, h ,angle] format."); + AddOutput("LabelsInt32", + "(LoDTensor), This output is a 2D LoDTensor with shape [P, 1], " + "each element repersents a class label of a roi"); + AddOutput("BboxTargets", + "(LoDTensor), This output is a 2D LoDTensor with shape [P, 5 * " + "class_nums], " + "each element repersents a box label of a roi"); + AddOutput( + "BboxInsideWeights", + "(LoDTensor), This output is a 2D LoDTensor with shape [P, 5 * " + "class_nums], " + "each element indicates whether a box should contribute to loss."); + AddOutput( + "BboxOutsideWeights", + "(LoDTensor), This output is a 2D LoDTensor with shape [P, 5 * " + "class_nums], " + "each element indicates whether a box should contribute to loss."); + + AddAttr("batch_size_per_im", "Batch size of rois per images."); + AddAttr("fg_fraction", + "Foreground fraction in total batch_size_per_im."); + AddAttr( + "fg_thresh", + "Overlap threshold which is used to chose foreground sample."); + AddAttr("bg_thresh_hi", + "Overlap threshold upper bound which is used to chose " + "background sample."); + AddAttr("bg_thresh_lo", + "Overlap threshold lower bound which is used to chose " + "background sample."); + AddAttr>("bbox_reg_weights", "Box regression weights."); + AddAttr("class_nums", "Class number."); + AddAttr( + "use_random", + "Use random sampling to choose foreground and background boxes.") + .SetDefault(true); + AddAttr( + "is_cls_agnostic", + "the box regress will only include fg and bg locations if set true ") + .SetDefault(false); + + AddComment(R"DOC( +This operator can be, for given the RotatedGenerateProposalOp output rotated bounding boxes and groundtruth, +to sample foreground boxes and background boxes, and compute loss target. + +RpnRois is the output boxes of RPN and was processed by rotated_generate_proposal_op, these boxes +were combined with groundtruth boxes and sampled according to batch_size_per_im and fg_fraction, +If an instance with a groundtruth overlap greater than fg_thresh, then it was considered as a foreground sample. +If an instance with a groundtruth overlap greater than bg_thresh_lo and lower than bg_thresh_hi, +then it was considered as a background sample. +After all foreground and background boxes are chosen (so called Rois), +then we apply random sampling to make sure +the number of foreground boxes is no more than batch_size_per_im * fg_fraction. + +For each box in Rois, we assign the classification (class label) and regression targets (box label) to it. +Finally BboxInsideWeights and BboxOutsideWeights are used to specify whether it would contribute to training loss. + )DOC"); + } +}; + +} // namespace operators +} // namespace paddle + +namespace ops = paddle::operators; +REGISTER_OPERATOR( + rrpn_generate_proposal_labels, + ops::RRPNGenerateProposalLabelsOp, + ops::RRPNGenerateProposalLabelsOpMaker, + paddle::framework::EmptyGradOpMaker, + paddle::framework::EmptyGradOpMaker); +REGISTER_OP_CPU_KERNEL(rrpn_generate_proposal_labels, + ops::RRPNGenerateProposalLabelsKernel, + ops::RRPNGenerateProposalLabelsKernel); diff --git a/PaddleCV/rrpn/models/ext_op/src/rrpn_generate_proposals_op.cc b/PaddleCV/rrpn/models/ext_op/src/rrpn_generate_proposals_op.cc new file mode 100644 index 00000000..5e344f0d --- /dev/null +++ b/PaddleCV/rrpn/models/ext_op/src/rrpn_generate_proposals_op.cc @@ -0,0 +1,694 @@ +/*opyright (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. */ + +#include +#include +#include +#include +#include +#include +#include "gather.h" +#include "math_function.h" +#include "paddle/fluid/framework/op_registry.h" +#include "safe_ref.h" + +namespace paddle { +namespace operators { + +using Tensor = framework::Tensor; +using LoDTensor = framework::LoDTensor; + +static const double kBBoxClipDefault = std::log(1000.0 / 16.0); +#define PI 3.141592654 + +static void RRPNAppendProposals(Tensor *dst, + int64_t offset, + const Tensor &src) { + auto *out_data = dst->data(); + auto *to_add_data = src.data(); + size_t size_of_t = framework::SizeOfType(src.type()); + offset *= size_of_t; + std::memcpy( + reinterpret_cast(reinterpret_cast(out_data) + offset), + to_add_data, + src.numel() * size_of_t); +} + +template +inline T axr(T x, T r) { + return 0.5 * PI * r * r - x * sqrt(r * r - x * x) - r * r * std::asin(x / r); +} + +class RRPNGenerateProposalsOp : public framework::OperatorWithKernel { +public: + using framework::OperatorWithKernel::OperatorWithKernel; + + void InferShape(framework::InferShapeContext *ctx) const override { + PADDLE_ENFORCE(ctx->HasInput("Scores"), "Input(Scores) shouldn't be null."); + PADDLE_ENFORCE(ctx->HasInput("BboxDeltas"), + "Input(BboxDeltas) shouldn't be null."); + PADDLE_ENFORCE(ctx->HasInput("ImInfo"), "Input(ImInfo) shouldn't be null."); + PADDLE_ENFORCE(ctx->HasInput("Anchors"), + "Input(Anchors) shouldn't be null."); + PADDLE_ENFORCE(ctx->HasInput("Variances"), + "Input(Variances) shouldn't be null."); + + ctx->SetOutputDim("RpnRois", {-1, 5}); + ctx->SetOutputDim("RpnRoiProbs", {-1, 1}); + } + +protected: + framework::OpKernelType GetExpectedKernelType( + const framework::ExecutionContext &ctx) const override { + return framework::OpKernelType(ctx.Input("Anchors")->type(), + ctx.device_context()); + } +}; + +template +static inline void RBoxCoder(const platform::DeviceContext &ctx, + Tensor *all_anchors, + Tensor *bbox_deltas, + Tensor *variances, + Tensor *proposals) { + T *proposals_data = proposals->mutable_data(ctx.GetPlace()); + + int64_t row = all_anchors->dims()[0]; + int64_t len = all_anchors->dims()[1]; + + auto *bbox_deltas_data = bbox_deltas->data(); + auto *anchor_data = all_anchors->data(); + const T *variances_data = nullptr; + if (variances) { + variances_data = variances->data(); + } + + for (int64_t i = 0; i < row; ++i) { + T anchor_width = anchor_data[i * len + 2]; + T anchor_height = anchor_data[i * len + 3]; + T anchor_angle = anchor_data[i * len + 4]; + + T anchor_center_x = anchor_data[i * len]; + T anchor_center_y = anchor_data[i * len + 1]; + + T bbox_center_x = 0, bbox_center_y = 0; + T bbox_width = 0, bbox_height = 0, bbox_angle = 0; + + if (variances) { + bbox_center_x = + bbox_deltas_data[i * len] / variances_data[i * len] * anchor_width + + anchor_center_x; + bbox_center_y = bbox_deltas_data[i * len + 1] / + variances_data[i * len + 1] * anchor_height + + anchor_center_y; + bbox_width = std::exp(std::min(bbox_deltas_data[i * len + 2] / + variances_data[i * len + 2], + kBBoxClipDefault)) * + anchor_width; + bbox_height = std::exp(std::min(bbox_deltas_data[i * len + 3] / + variances_data[i * len + 3], + kBBoxClipDefault)) * + anchor_height; + bbox_angle = + (bbox_deltas_data[i * len + 4] / variances_data[i * len + 4]) * 1.0 / + PI * 180 + + anchor_angle; + + } else { + bbox_center_x = + bbox_deltas_data[i * len] * anchor_width + anchor_center_x; + bbox_center_y = + bbox_deltas_data[i * len + 1] * anchor_height + anchor_center_y; + bbox_width = std::exp(std::min(bbox_deltas_data[i * len + 2], + kBBoxClipDefault)) * + anchor_width; + bbox_height = std::exp(std::min(bbox_deltas_data[i * len + 3], + kBBoxClipDefault)) * + anchor_height; + bbox_angle = + bbox_deltas_data[i * len + 4] * 1.0 / PI * 180 + anchor_angle; + } + + proposals_data[i * len] = bbox_center_x; + proposals_data[i * len + 1] = bbox_center_y; + proposals_data[i * len + 2] = bbox_width; + proposals_data[i * len + 3] = bbox_height; + proposals_data[i * len + 4] = bbox_angle; + } + // return proposals; +} + + +template +static inline void RFilterBoxes(const platform::DeviceContext &ctx, + Tensor *boxes, + float min_size, + const Tensor &im_info, + Tensor *keep) { + T *boxes_data = boxes->mutable_data(ctx.GetPlace()); + keep->Resize({boxes->dims()[0]}); + min_size = std::max(min_size, 0.0f); + int *keep_data = keep->mutable_data(ctx.GetPlace()); + + int keep_len = 0; + for (int i = 0; i < boxes->dims()[0]; ++i) { + T ws = boxes_data[5 * i + 2]; + T hs = boxes_data[5 * i + 3]; + if (ws >= min_size && hs >= min_size) { + keep_data[keep_len++] = i; + } + } + keep->Resize({keep_len}); +} + +template +static inline std::vector> GetSortedScoreIndex( + const std::vector &scores) { + std::vector> sorted_indices; + sorted_indices.reserve(scores.size()); + for (size_t i = 0; i < scores.size(); ++i) { + sorted_indices.emplace_back(scores[i], i); + } + // Sort the score pair according to the scores in descending order + std::stable_sort(sorted_indices.begin(), + sorted_indices.end(), + [](const std::pair &a, const std::pair &b) { + return a.first < b.first; + }); + return sorted_indices; +} + + +template +static inline Tensor VectorToTensor(const std::vector &selected_indices, + int selected_num) { + Tensor keep_nms; + keep_nms.Resize({selected_num}); + auto *keep_data = keep_nms.mutable_data(platform::CPUPlace()); + for (int i = 0; i < selected_num; ++i) { + keep_data[i] = selected_indices[i]; + } + return keep_nms; +} + +template +inline T trangle_area(T *a, T *b, T *c) { + return ((a[0] - c[0]) * (b[1] - c[1]) - (a[1] - c[1]) * (b[0] - c[0])) / 2.0; +} + +template +inline T area(T *int_pts, int num_of_inter) { + float area = 0.0; + for (int i = 0; i < num_of_inter - 2; i++) { + area += + fabs(trangle_area(int_pts, int_pts + 2 * i + 2, int_pts + 2 * i + 4)); + } + return area; +} + +template +inline void reorder_pts(T *int_pts, int num_of_inter) { + if (num_of_inter > 0) { + float center[2]; + + center[0] = 0.0; + center[1] = 0.0; + + for (int i = 0; i < num_of_inter; i++) { + center[0] += int_pts[2 * i]; + center[1] += int_pts[2 * i + 1]; + } + center[0] /= num_of_inter; + center[1] /= num_of_inter; + + float vs[16]; + float v[2]; + float d; + for (int i = 0; i < num_of_inter; i++) { + v[0] = int_pts[2 * i] - center[0]; + v[1] = int_pts[2 * i + 1] - center[1]; + d = sqrt(v[0] * v[0] + v[1] * v[1]); + v[0] = v[0] / d; + v[1] = v[1] / d; + if (v[1] < 0) { + v[0] = -2 - v[0]; + } + vs[i] = v[0]; + } + + float temp, tx, ty; + int j; + for (int i = 1; i < num_of_inter; ++i) { + if (vs[i - 1] > vs[i]) { + temp = vs[i]; + tx = int_pts[2 * i]; + ty = int_pts[2 * i + 1]; + j = i; + while (j > 0 && vs[j - 1] > temp) { + vs[j] = vs[j - 1]; + int_pts[j * 2] = int_pts[j * 2 - 2]; + int_pts[j * 2 + 1] = int_pts[j * 2 - 1]; + j--; + } + vs[j] = temp; + int_pts[j * 2] = tx; + int_pts[j * 2 + 1] = ty; + } + } + } +} + +template +inline bool inter2line(T *pts1, T *pts2, int i, int j, T *temp_pts) { + T a[2]; + T b[2]; + T c[2]; + T d[2]; + + T area_abc, area_abd, area_cda, area_cdb; + + a[0] = pts1[2 * i]; + a[1] = pts1[2 * i + 1]; + + b[0] = pts1[2 * ((i + 1) % 4)]; + b[1] = pts1[2 * ((i + 1) % 4) + 1]; + + c[0] = pts2[2 * j]; + c[1] = pts2[2 * j + 1]; + + d[0] = pts2[2 * ((j + 1) % 4)]; + d[1] = pts2[2 * ((j + 1) % 4) + 1]; + + area_abc = trangle_area(a, b, c); + area_abd = trangle_area(a, b, d); + + if (area_abc * area_abd >= 0) { + return false; + } + + area_cda = trangle_area(c, d, a); + area_cdb = area_cda + area_abc - area_abd; + + if (area_cda * area_cdb >= 0) { + return false; + } + float t = area_cda / (area_abd - area_abc); + + float dx = t * (b[0] - a[0]); + float dy = t * (b[1] - a[1]); + temp_pts[0] = a[0] + dx; + temp_pts[1] = a[1] + dy; + + return true; +} + +template +inline bool in_rect(T pt_x, T pt_y, T *pts) { + float ab[2]; + float ad[2]; + float ap[2]; + + float abab; + float abap; + float adad; + float adap; + + ab[0] = pts[2] - pts[0]; + ab[1] = pts[3] - pts[1]; + + ad[0] = pts[6] - pts[0]; + ad[1] = pts[7] - pts[1]; + + ap[0] = pt_x - pts[0]; + ap[1] = pt_y - pts[1]; + + abab = ab[0] * ab[0] + ab[1] * ab[1]; + abap = ab[0] * ap[0] + ab[1] * ap[1]; + adad = ad[0] * ad[0] + ad[1] * ad[1]; + adap = ad[0] * ap[0] + ad[1] * ap[1]; + + return abab >= abap and abap >= 0 and adad >= adap and adap >= 0; +} + +template +inline int inter_pts(T *pts1, T *pts2, T *int_pts) { + int num_of_inter = 0; + + for (int i = 0; i < 4; i++) { + if (in_rect(pts1[2 * i], pts1[2 * i + 1], pts2)) { + int_pts[num_of_inter * 2] = pts1[2 * i]; + int_pts[num_of_inter * 2 + 1] = pts1[2 * i + 1]; + num_of_inter++; + } + if (in_rect(pts2[2 * i], pts2[2 * i + 1], pts1)) { + int_pts[num_of_inter * 2] = pts2[2 * i]; + int_pts[num_of_inter * 2 + 1] = pts2[2 * i + 1]; + num_of_inter++; + } + } + + T temp_pts[2]; + + for (int i = 0; i < 4; i++) { + for (int j = 0; j < 4; j++) { + bool has_pts = inter2line(pts1, pts2, i, j, temp_pts); + if (has_pts) { + int_pts[num_of_inter * 2] = temp_pts[0]; + int_pts[num_of_inter * 2 + 1] = temp_pts[1]; + num_of_inter++; + } + } + } + + return num_of_inter; +} + +template +inline void convert_region(T *pts, const T *region) { + float angle = region[4]; + float a_cos = cos(angle / 180.0 * PI); + float a_sin = -sin(angle / 180.0 * PI); // anti clock-wise + + float ctr_x = region[0]; + float ctr_y = region[1]; + float h = region[3]; + float w = region[2]; + + float pts_x[4]; + float pts_y[4]; + + pts_x[0] = -w / 2; + pts_x[1] = -w / 2; + pts_x[2] = w / 2; + pts_x[3] = w / 2; + + pts_y[0] = -h / 2; + pts_y[1] = h / 2; + pts_y[2] = h / 2; + pts_y[3] = -h / 2; + + for (int i = 0; i < 4; i++) { + pts[2 * i] = a_cos * pts_x[i] - a_sin * pts_y[i] + ctr_x; + pts[2 * i + 1] = a_sin * pts_x[i] + a_cos * pts_y[i] + ctr_y; + } +} + +template +inline float inter(const T *region1, const T *region2) { + T pts1[8]; + T pts2[8]; + T int_pts[16]; + int num_of_inter; + + convert_region(pts1, region1); + convert_region(pts2, region2); + + num_of_inter = inter_pts(pts1, pts2, int_pts); + + reorder_pts(int_pts, num_of_inter); + + return area(int_pts, num_of_inter); +} + +template +inline float DevRotateIoU(const T *region1, const T *region2) { + T area1 = region1[2] * region1[3]; + T area2 = region2[2] * region2[3]; + T area_inter = inter(region1, region2); + + return area_inter / (area1 + area2 - area_inter); +} + +template +static inline Tensor RNMS(const platform::DeviceContext &ctx, + Tensor *bbox, + Tensor *scores, + T nms_threshold) { + PADDLE_ENFORCE_NOT_NULL(bbox); + int64_t num_boxes = bbox->dims()[0]; + // 4: [xmin ymin xmax ymax] + int64_t box_size = bbox->dims()[1]; + + std::vector scores_data(num_boxes); + std::copy_n(scores->data(), num_boxes, scores_data.begin()); + std::vector> sorted_indices = + GetSortedScoreIndex(scores_data); + + std::vector selected_indices; + int selected_num = 0; + T adaptive_threshold = nms_threshold; + const T *bbox_data = bbox->data(); + while (sorted_indices.size() != 0) { + int idx = sorted_indices.back().second; + bool flag = true; + for (int kept_idx : selected_indices) { + if (flag) { + T overlap = DevRotateIoU(bbox_data + idx * box_size, + bbox_data + kept_idx * box_size); + flag = (overlap <= adaptive_threshold); + } else { + break; + } + } + if (flag) { + selected_indices.push_back(idx); + ++selected_num; + } + sorted_indices.erase(sorted_indices.end() - 1); + } + return VectorToTensor(selected_indices, selected_num); +} + +template +class RRPNGenerateProposalsKernel : public framework::OpKernel { +public: + void Compute(const framework::ExecutionContext &context) const override { + auto *scores = context.Input("Scores"); + auto *bbox_deltas = context.Input("BboxDeltas"); + auto *im_info = context.Input("ImInfo"); + auto anchors = detail::Ref(context.Input("Anchors"), + "Cannot find input Anchors(%s) in scope", + context.InputNames("Anchors")[0]); + auto variances = detail::Ref(context.Input("Variances"), + "Cannot find input Variances(%s) in scope", + context.InputNames("Variances")[0]); + + auto *rpn_rois = context.Output("RpnRois"); + auto *rpn_roi_probs = context.Output("RpnRoiProbs"); + + int pre_nms_top_n = context.Attr("pre_nms_topN"); + int post_nms_top_n = context.Attr("post_nms_topN"); + float nms_thresh = context.Attr("nms_thresh"); + float min_size = context.Attr("min_size"); + + auto &dev_ctx = + context.template device_context(); + + auto &scores_dim = scores->dims(); + int64_t num = scores_dim[0]; + int64_t c_score = scores_dim[1]; + int64_t h_score = scores_dim[2]; + int64_t w_score = scores_dim[3]; + + auto &bbox_dim = bbox_deltas->dims(); + int64_t c_bbox = bbox_dim[1]; + int64_t h_bbox = bbox_dim[2]; + int64_t w_bbox = bbox_dim[3]; + + rpn_rois->mutable_data({bbox_deltas->numel() / 5, 5}, + context.GetPlace()); + rpn_roi_probs->mutable_data({scores->numel(), 1}, context.GetPlace()); + + Tensor bbox_deltas_swap, scores_swap; + bbox_deltas_swap.mutable_data({num, h_bbox, w_bbox, c_bbox}, + dev_ctx.GetPlace()); + scores_swap.mutable_data({num, h_score, w_score, c_score}, + dev_ctx.GetPlace()); + + math::Transpose trans; + std::vector axis = {0, 2, 3, 1}; + trans(dev_ctx, *bbox_deltas, &bbox_deltas_swap, axis); + trans(dev_ctx, *scores, &scores_swap, axis); + + framework::LoD lod; + lod.resize(1); + auto &lod0 = lod[0]; + lod0.push_back(0); + anchors.Resize({anchors.numel() / 5, 5}); + variances.Resize({variances.numel() / 5, 5}); + + int64_t num_proposals = 0; + for (int64_t i = 0; i < num; ++i) { + Tensor im_info_slice = im_info->Slice(i, i + 1); + Tensor bbox_deltas_slice = bbox_deltas_swap.Slice(i, i + 1); + Tensor scores_slice = scores_swap.Slice(i, i + 1); + + bbox_deltas_slice.Resize({h_bbox * w_bbox * c_bbox / 5, 5}); + scores_slice.Resize({h_score * w_score * c_score, 1}); + + std::pair tensor_pair = + ProposalForOneImage(dev_ctx, + im_info_slice, + anchors, + variances, + bbox_deltas_slice, + scores_slice, + pre_nms_top_n, + post_nms_top_n, + nms_thresh, + min_size); + Tensor &proposals = tensor_pair.first; + Tensor &scores = tensor_pair.second; + + RRPNAppendProposals(rpn_rois, 5 * num_proposals, proposals); + RRPNAppendProposals(rpn_roi_probs, num_proposals, scores); + num_proposals += proposals.dims()[0]; + lod0.push_back(num_proposals); + } + rpn_rois->set_lod(lod); + rpn_roi_probs->set_lod(lod); + rpn_rois->Resize({num_proposals, 5}); + rpn_roi_probs->Resize({num_proposals, 1}); + } + + std::pair ProposalForOneImage( + const platform::CPUDeviceContext &ctx, + const Tensor &im_info_slice, + const Tensor &anchors, + const Tensor &variances, + const Tensor &bbox_deltas_slice, // [M, 5] + const Tensor &scores_slice, // [N, 1] + int pre_nms_top_n, + int post_nms_top_n, + float nms_thresh, + float min_size) const { + auto *scores_data = scores_slice.data(); + // Sort index + Tensor index_t; + index_t.Resize({scores_slice.numel()}); + int *index = index_t.mutable_data(ctx.GetPlace()); + for (int i = 0; i < scores_slice.numel(); ++i) { + index[i] = i; + } + auto compare = [scores_data](const int64_t &i, const int64_t &j) { + return scores_data[i] > scores_data[j]; + }; + + if (pre_nms_top_n <= 0 || pre_nms_top_n >= scores_slice.numel()) { + std::sort(index, index + scores_slice.numel(), compare); + } else { + std::nth_element( + index, index + pre_nms_top_n, index + scores_slice.numel(), compare); + index_t.Resize({pre_nms_top_n}); + } + + Tensor scores_sel, bbox_sel, anchor_sel, var_sel; + scores_sel.mutable_data({index_t.numel(), 1}, ctx.GetPlace()); + bbox_sel.mutable_data({index_t.numel(), 5}, ctx.GetPlace()); + anchor_sel.mutable_data({index_t.numel(), 5}, ctx.GetPlace()); + var_sel.mutable_data({index_t.numel(), 5}, ctx.GetPlace()); + + CPUGather(ctx, scores_slice, index_t, &scores_sel); + CPUGather(ctx, bbox_deltas_slice, index_t, &bbox_sel); + CPUGather(ctx, anchors, index_t, &anchor_sel); + CPUGather(ctx, variances, index_t, &var_sel); + + auto *scores_ = scores_sel.data(); + + Tensor proposals; + proposals.mutable_data({index_t.numel(), 5}, ctx.GetPlace()); + RBoxCoder(ctx, &anchor_sel, &bbox_sel, &var_sel, &proposals); + + Tensor keep; + RFilterBoxes(ctx, &proposals, min_size, im_info_slice, &keep); + Tensor scores_filter; + bbox_sel.mutable_data({keep.numel(), 5}, ctx.GetPlace()); + scores_filter.mutable_data({keep.numel(), 1}, ctx.GetPlace()); + CPUGather(ctx, proposals, keep, &bbox_sel); + CPUGather(ctx, scores_sel, keep, &scores_filter); + if (nms_thresh <= 0) { + return std::make_pair(bbox_sel, scores_filter); + } + Tensor keep_nms = RNMS(ctx, &bbox_sel, &scores_filter, nms_thresh); + + if (post_nms_top_n > 0 && post_nms_top_n < keep_nms.numel()) { + keep_nms.Resize({post_nms_top_n}); + } + proposals.mutable_data({keep_nms.numel(), 5}, ctx.GetPlace()); + scores_sel.mutable_data({keep_nms.numel(), 1}, ctx.GetPlace()); + CPUGather(ctx, bbox_sel, keep_nms, &proposals); + CPUGather(ctx, scores_filter, keep_nms, &scores_sel); + + return std::make_pair(proposals, scores_sel); + } +}; + +class RRPNGenerateProposalsOpMaker : public framework::OpProtoAndCheckerMaker { +public: + void Make() override { + AddInput("Scores", + "(Tensor) The scores from conv is in shape (N, A, H, W), " + "N is batch size, A is number of anchors, " + "H and W are height and width of the feature map"); + AddInput("BboxDeltas", + "(Tensor) Bounding box deltas from conv is in " + "shape (N, 5*A, H, W)."); + AddInput("ImInfo", + "(Tensor) Information for image reshape is in shape (N, 3), " + "in format (height, width, scale)"); + AddInput("Anchors", + "(Tensor) Bounding box anchors from anchor_generator_op " + "is in shape (A, H, W, 5)."); + AddInput("Variances", + "(Tensor) Bounding box variances with same shape as `Anchors`."); + + AddOutput("RpnRois", + "(LoDTensor), Output proposals with shape (rois_num, 5)."); + AddOutput("RpnRoiProbs", + "(LoDTensor) Scores of proposals with shape (rois_num, 1)."); + AddAttr("pre_nms_topN", + "Number of top scoring RPN proposals to keep before " + "applying NMS."); + AddAttr("post_nms_topN", + "Number of top scoring RPN proposals to keep after " + "applying NMS"); + AddAttr("nms_thresh", "NMS threshold used on RPN proposals."); + AddAttr("min_size", + "Proposal height and width both need to be greater " + "than this min_size."); + AddComment(R"DOC( +This operator Generate bounding box proposals for Faster RCNN. +The propoasls are generated for a list of images based on image +score 'Scores', bounding box regression result 'BboxDeltas' as +well as predefined bounding box shapes 'anchors'. Greedy +non-maximum suppression is applied to generate the final bounding +boxes. + +)DOC"); + } +}; + +} // namespace operators +} // namespace paddle + +namespace ops = paddle::operators; +REGISTER_OPERATOR( + rrpn_generate_proposals, + ops::RRPNGenerateProposalsOp, + ops::RRPNGenerateProposalsOpMaker, + paddle::framework::EmptyGradOpMaker, + paddle::framework::EmptyGradOpMaker); +REGISTER_OP_CPU_KERNEL(rrpn_generate_proposals, + ops::RRPNGenerateProposalsKernel, + ops::RRPNGenerateProposalsKernel); diff --git a/PaddleCV/rrpn/models/ext_op/src/rrpn_generate_proposals_op.cu b/PaddleCV/rrpn/models/ext_op/src/rrpn_generate_proposals_op.cu new file mode 100644 index 00000000..a074e79e --- /dev/null +++ b/PaddleCV/rrpn/models/ext_op/src/rrpn_generate_proposals_op.cu @@ -0,0 +1,747 @@ +/* Copyright (c) 2018 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 +-------------------------------------------------------- +@misc{ma2019rrpn, + author = {Jianqi Ma}, + title = {{RRPN in pytorch}}, + year = {2019}, + howpublished = {\url{https://github.com/mjq11302010044/RRPN_pytorch}}, +} +@article{Jianqi17RRPN, + Author = {Jianqi Ma and Weiyuan Shao and Hao Ye and Li Wang and Hong Wang +and Yingbin Zheng and Xiangyang Xue}, + Title = {Arbitrary-Oriented Scene Text Detection via Rotation Proposals}, + journal = {IEEE Transactions on Multimedia}, + volume={20}, + number={11}, + pages={3111-3122}, + year={2018} +} +-------------------------------------------------------- +*/ + +#include +#include +#include +#include +#include "cub/cub/cub.cuh" +#include "gather.cu.h" +#include "math_function.h" +#include "paddle/fluid/framework/mixed_vector.h" +#include "paddle/fluid/framework/op_registry.h" +#include "paddle/fluid/memory/memory.h" +#include "paddle/fluid/platform/for_range.h" +#include "safe_ref.h" + +namespace paddle { +namespace operators { + +using Tensor = framework::Tensor; +using LoDTensor = framework::LoDTensor; +#define PI 3.141592654 + +namespace { + +#define DIVUP(m, n) ((m) / (n) + ((m) % (n) > 0)) +#define CUDA_1D_KERNEL_LOOP(i, n) \ + for (int i = blockIdx.x * blockDim.x + threadIdx.x; i < (n); \ + i += blockDim.x * gridDim.x) + +int const kThreadsPerBlock = sizeof(uint64_t) * 8; + +static const double kBBoxClipDefault = std::log(1000.0 / 16.0); + +struct RangeInitFunctor { + int start_; + int delta_; + int *out_; + __device__ void operator()(size_t i) { out_[i] = start_ + i * delta_; } +}; + +template +static void RSortDescending(const platform::CUDADeviceContext &ctx, + const Tensor &value, + Tensor *value_out, + Tensor *index_out) { + int num = static_cast(value.numel()); + Tensor index_in_t; + int *idx_in = index_in_t.mutable_data({num}, ctx.GetPlace()); + platform::ForRange for_range(ctx, num); + for_range(RangeInitFunctor{0, 1, idx_in}); + + int *idx_out = index_out->mutable_data({num}, ctx.GetPlace()); + + const T *keys_in = value.data(); + T *keys_out = value_out->mutable_data({num}, ctx.GetPlace()); + + // Determine temporary device storage requirements + size_t temp_storage_bytes = 0; + cub::DeviceRadixSort::SortPairsDescending( + nullptr, temp_storage_bytes, keys_in, keys_out, idx_in, idx_out, num); + // Allocate temporary storage + auto place = boost::get(ctx.GetPlace()); + auto d_temp_storage = memory::Alloc(place, temp_storage_bytes); + + // Run sorting operation + cub::DeviceRadixSort::SortPairsDescending(d_temp_storage->ptr(), + temp_storage_bytes, + keys_in, + keys_out, + idx_in, + idx_out, + num); +} + +template +struct RBoxDecodeAndClipFunctor { + const T *anchor; + const T *deltas; + const T *var; + const int *index; + const T *im_info; + + T *proposals; + + RBoxDecodeAndClipFunctor(const T *anchor, + const T *deltas, + const T *var, + const int *index, + const T *im_info, + T *proposals) + : anchor(anchor), + deltas(deltas), + var(var), + index(index), + im_info(im_info), + proposals(proposals) {} + + T bbox_clip_default{static_cast(kBBoxClipDefault)}; + + __device__ void operator()(size_t i) { + int k = index[i] * 5; + + T w = anchor[k + 2]; + T h = anchor[k + 3]; + T cx = anchor[k]; + T cy = anchor[k + 1]; + T angle = anchor[k + 4]; + + T de_cx = deltas[k]; + T de_cy = deltas[k + 1]; + T de_w = deltas[k + 2]; + T de_h = deltas[k + 3]; + T de_g = deltas[k + 4]; + + T d_cx, d_cy, d_w, d_h, d_g; + if (var) { + d_cx = cx + de_cx * w / var[k]; + d_cy = cy + de_cy * h / var[k + 1]; + d_w = exp(Min(de_w / var[k + 2], bbox_clip_default)) * w; + d_h = exp(Min(de_h / var[k + 3], bbox_clip_default)) * h; + d_g = de_g / var[k + 4] * 1.0 / PI * 180 + angle; + } else { + d_cx = cx + de_cx * w; + d_cy = cy + de_cy * h; + d_w = exp(Min(de_w, bbox_clip_default)) * w; + d_h = exp(Min(de_h, bbox_clip_default)) * h; + d_g = de_g * 1.0 / PI * 180 + angle; + } + + proposals[i * 5] = d_cx; + proposals[i * 5 + 1] = d_cy; + proposals[i * 5 + 2] = d_w; + proposals[i * 5 + 3] = d_h; + proposals[i * 5 + 4] = d_g; + } + + __device__ __forceinline__ T Min(T a, T b) const { return a > b ? b : a; } + + __device__ __forceinline__ T Max(T a, T b) const { return a > b ? a : b; } +}; + +template +static __global__ void RFilterBBoxes(const T *bboxes, + const T *im_info, + const T min_size, + const int num, + int *keep_num, + int *keep) { + T im_h = im_info[0]; + T im_w = im_info[1]; + T im_scale = im_info[2]; + + int cnt = 0; + __shared__ int keep_index[BlockSize]; + + CUDA_1D_KERNEL_LOOP(i, num) { + keep_index[threadIdx.x] = -1; + __syncthreads(); + + int k = i * 5; + + T cx = bboxes[k]; + T cy = bboxes[k + 1]; + T w_s = bboxes[k + 2]; + T h_s = bboxes[k + 3]; + + if (w_s >= min_size && h_s >= min_size) { + keep_index[threadIdx.x] = i; + } + __syncthreads(); + if (threadIdx.x == 0) { + int size = (num - i) < BlockSize ? num - i : BlockSize; + for (int j = 0; j < size; ++j) { + if (keep_index[j] > -1) { + keep[cnt++] = keep_index[j]; + } + } + } + __syncthreads(); + } + if (threadIdx.x == 0) { + keep_num[0] = cnt; + } +} + + +__device__ inline float trangle_area(float *a, float *b, float *c) { + return ((a[0] - c[0]) * (b[1] - c[1]) - (a[1] - c[1]) * (b[0] - c[0])) / 2.0; +} + + +__device__ inline float area(float *int_pts, int num_of_inter) { + float area = 0.0; + for (int i = 0; i < num_of_inter - 2; i++) { + area += + fabs(trangle_area(int_pts, int_pts + 2 * i + 2, int_pts + 2 * i + 4)); + } + return area; +} + + +__device__ inline void reorder_pts(float *int_pts, int num_of_inter) { + if (num_of_inter > 0) { + float center[2] = {0.0, 0.0}; + + // center[0] = 0.0; + // center[1] = 0.0; + + for (int i = 0; i < num_of_inter; i++) { + center[0] += int_pts[2 * i]; + center[1] += int_pts[2 * i + 1]; + } + center[0] /= num_of_inter; + center[1] /= num_of_inter; + + float vs[16]; + float v[2]; + float d; + for (int i = 0; i < num_of_inter; i++) { + v[0] = int_pts[2 * i] - center[0]; + v[1] = int_pts[2 * i + 1] - center[1]; + d = sqrt(v[0] * v[0] + v[1] * v[1]); + v[0] = v[0] / d; + v[1] = v[1] / d; + if (v[1] < 0) { + v[0] = -2 - v[0]; + } + vs[i] = v[0]; + } + + float temp, tx, ty; + int j; + for (int i = 1; i < num_of_inter; ++i) { + if (vs[i - 1] > vs[i]) { + temp = vs[i]; + tx = int_pts[2 * i]; + ty = int_pts[2 * i + 1]; + j = i; + while (j > 0 && vs[j - 1] > temp) { + vs[j] = vs[j - 1]; + int_pts[j * 2] = int_pts[j * 2 - 2]; + int_pts[j * 2 + 1] = int_pts[j * 2 - 1]; + j--; + } + vs[j] = temp; + int_pts[j * 2] = tx; + int_pts[j * 2 + 1] = ty; + } + } + } +} + + +__device__ inline bool inter2line( + float *pts1, float *pts2, int i, int j, float *temp_pts) { + float a[2] = {pts1[2 * i], pts1[2 * i + 1]}; + float b[2] = {pts1[2 * ((i + 1) % 4)], pts1[2 * ((i + 1) % 4) + 1]}; + float c[2] = {pts2[2 * j], pts2[2 * j + 1]}; + float d[2] = {pts2[2 * ((j + 1) % 4)], pts2[2 * ((j + 1) % 4) + 1]}; + + // T area_abc, area_abd, area_cda, area_cdb; + + // a[0] = pts1[2 * i]; + // a[1] = pts1[2 * i + 1]; + + // b[0] = pts1[2 * ((i + 1) % 4)]; + // b[1] = pts1[2 * ((i + 1) % 4) + 1]; + + // c[0] = pts2[2 * j]; + // c[1] = pts2[2 * j + 1]; + + // d[0] = pts2[2 * ((j + 1) % 4)]; + // d[1] = pts2[2 * ((j + 1) % 4) + 1]; + + float area_abc = trangle_area(a, b, c); + float area_abd = trangle_area(a, b, d); + + if (area_abc * area_abd >= 0) { + return false; + } + + float area_cda = trangle_area(c, d, a); + float area_cdb = area_cda + area_abc - area_abd; + + if (area_cda * area_cdb >= 0) { + return false; + } + float t = area_cda / (area_abd - area_abc); + + float dx = t * (b[0] - a[0]); + float dy = t * (b[1] - a[1]); + temp_pts[0] = a[0] + dx; + temp_pts[1] = a[1] + dy; + + return true; +} + + +__device__ inline bool in_rect(float pt_x, float pt_y, float *pts) { + float ab[2] = {pts[2] - pts[0], pts[3] - pts[1]}; + float ad[2] = {pts[6] - pts[0], pts[7] - pts[1]}; + float ap[2] = {pt_x - pts[0], pt_y - pts[1]}; + + // float abab; + // float abap; + // float adad; + // float adap; + + // ab[0] = pts[2] - pts[0]; + // ab[1] = pts[3] - pts[1]; + // + // ad[0] = pts[6] - pts[0]; + // ad[1] = pts[7] - pts[1]; + // + // ap[0] = pt_x - pts[0]; + // ap[1] = pt_y - pts[1]; + + float abab = ab[0] * ab[0] + ab[1] * ab[1]; + float abap = ab[0] * ap[0] + ab[1] * ap[1]; + float adad = ad[0] * ad[0] + ad[1] * ad[1]; + float adap = ad[0] * ap[0] + ad[1] * ap[1]; + + return abab >= abap and abap >= 0 and adad >= adap and adap >= 0; +} + + +__device__ inline int inter_pts(float *pts1, float *pts2, float *int_pts) { + int num_of_inter = 0; + + for (int i = 0; i < 4; i++) { + if (in_rect(pts1[2 * i], pts1[2 * i + 1], pts2)) { + int_pts[num_of_inter * 2] = pts1[2 * i]; + int_pts[num_of_inter * 2 + 1] = pts1[2 * i + 1]; + num_of_inter++; + } + if (in_rect(pts2[2 * i], pts2[2 * i + 1], pts1)) { + int_pts[num_of_inter * 2] = pts2[2 * i]; + int_pts[num_of_inter * 2 + 1] = pts2[2 * i + 1]; + num_of_inter++; + } + } + + float temp_pts[2]; + + for (int i = 0; i < 4; i++) { + for (int j = 0; j < 4; j++) { + bool has_pts = inter2line(pts1, pts2, i, j, temp_pts); + if (has_pts) { + int_pts[num_of_inter * 2] = temp_pts[0]; + int_pts[num_of_inter * 2 + 1] = temp_pts[1]; + num_of_inter++; + } + } + } + + return num_of_inter; +} + + +__device__ inline void convert_region(float *pts, const float *region) { + float angle = region[4]; + float a_cos = cos(angle / 180.0 * PI); + float a_sin = -sin(angle / 180.0 * PI); // anti clock-wise + + float ctr_x = region[0]; + float ctr_y = region[1]; + float h = region[3]; + float w = region[2]; + + float pts_x[4] = {-w / 2, -w / 2, w / 2, w / 2}; + float pts_y[4] = {-h / 2, h / 2, h / 2, -h / 2}; + + // pts_x[0] = -w / 2; + // pts_x[1] = -w / 2; + // pts_x[2] = w / 2; + // pts_x[3] = w / 2; + // + // pts_y[0] = -h / 2; + // pts_y[1] = h / 2; + // pts_y[2] = h / 2; + // pts_y[3] = -h / 2; + + for (int i = 0; i < 4; i++) { + pts[2 * i] = a_cos * pts_x[i] - a_sin * pts_y[i] + ctr_x; + pts[2 * i + 1] = a_sin * pts_x[i] + a_cos * pts_y[i] + ctr_y; + } +} + +__device__ inline float inter(const float *region1, const float *region2) { + float pts1[8]; + float pts2[8]; + float int_pts[16]; + int num_of_inter; + + convert_region(pts1, region1); + convert_region(pts2, region2); + + num_of_inter = inter_pts(pts1, pts2, int_pts); + + reorder_pts(int_pts, num_of_inter); + + return area(int_pts, num_of_inter); +} + + +__device__ inline float IoU(const float *region1, const float *region2) { + float area1 = region1[2] * region1[3]; + float area2 = region2[2] * region2[3]; + float area_inter = inter(region1, region2); + + return area_inter / (area1 + area2 - area_inter); +} + +static __global__ void RNMSKernel(const int n_boxes, + const float nms_overlap_thresh, + const float *dev_boxes, + uint64_t *dev_mask) { + const int row_start = blockIdx.y; + const int col_start = blockIdx.x; + + const int row_size = + min(n_boxes - row_start * kThreadsPerBlock, kThreadsPerBlock); + const int col_size = + min(n_boxes - col_start * kThreadsPerBlock, kThreadsPerBlock); + + __shared__ float block_boxes[kThreadsPerBlock * 5]; + if (threadIdx.x < col_size) { + block_boxes[threadIdx.x * 5 + 0] = + dev_boxes[(kThreadsPerBlock * col_start + threadIdx.x) * 5 + 0]; + block_boxes[threadIdx.x * 5 + 1] = + dev_boxes[(kThreadsPerBlock * col_start + threadIdx.x) * 5 + 1]; + block_boxes[threadIdx.x * 5 + 2] = + dev_boxes[(kThreadsPerBlock * col_start + threadIdx.x) * 5 + 2]; + block_boxes[threadIdx.x * 5 + 3] = + dev_boxes[(kThreadsPerBlock * col_start + threadIdx.x) * 5 + 3]; + block_boxes[threadIdx.x * 5 + 4] = + dev_boxes[(kThreadsPerBlock * col_start + threadIdx.x) * 5 + 4]; + } + __syncthreads(); + + if (threadIdx.x < row_size) { + const int cur_box_idx = kThreadsPerBlock * row_start + threadIdx.x; + const float *cur_box = dev_boxes + cur_box_idx * 5; + int i = 0; + uint64_t t = 0; + int start = 0; + if (row_start == col_start) { + start = threadIdx.x + 1; + } + for (i = start; i < col_size; i++) { + if (IoU(cur_box, block_boxes + i * 5) > nms_overlap_thresh) { + t |= 1ULL << i; + } + } + const int col_blocks = DIVUP(n_boxes, kThreadsPerBlock); + dev_mask[cur_box_idx * col_blocks + col_start] = t; + } +} + +template +static void RNMS(const platform::CUDADeviceContext &ctx, + const Tensor &proposals, + const Tensor &sorted_indices, + const T nms_threshold, + Tensor *keep_out) { + int boxes_num = proposals.dims()[0]; + PADDLE_ENFORCE_EQ(boxes_num, sorted_indices.dims()[0]); + + const int col_blocks = DIVUP(boxes_num, kThreadsPerBlock); + dim3 blocks(DIVUP(boxes_num, kThreadsPerBlock), + DIVUP(boxes_num, kThreadsPerBlock)); + dim3 threads(kThreadsPerBlock); + + const T *boxes = proposals.data(); + auto place = boost::get(ctx.GetPlace()); + framework::Vector mask(boxes_num * col_blocks); + RNMSKernel<<>>( + boxes_num, + nms_threshold, + boxes, + mask.CUDAMutableData(boost::get(ctx.GetPlace()))); + + std::vector remv(col_blocks); + memset(&remv[0], 0, sizeof(uint64_t) * col_blocks); + + std::vector keep_vec; + int num_to_keep = 0; + for (int i = 0; i < boxes_num; i++) { + int nblock = i / kThreadsPerBlock; + int inblock = i % kThreadsPerBlock; + + if (!(remv[nblock] & (1ULL << inblock))) { + ++num_to_keep; + keep_vec.push_back(i); + uint64_t *p = &mask[0] + i * col_blocks; + for (int j = nblock; j < col_blocks; j++) { + remv[j] |= p[j]; + } + } + } + int *keep = keep_out->mutable_data({num_to_keep}, ctx.GetPlace()); + memory::Copy(place, + keep, + platform::CPUPlace(), + keep_vec.data(), + sizeof(int) * num_to_keep, + ctx.stream()); + ctx.Wait(); +} + +template +static std::pair RRPNProposalForOneImage( + const platform::CUDADeviceContext &ctx, + const Tensor &im_info, + const Tensor &anchors, + const Tensor &variances, + const Tensor &bbox_deltas, // [M, 5] + const Tensor &scores, // [N, 1] + int pre_nms_top_n, + int post_nms_top_n, + float nms_thresh, + float min_size) { + // 1. pre nms + Tensor scores_sort, index_sort; + RSortDescending(ctx, scores, &scores_sort, &index_sort); + int num = scores.numel(); + int pre_nms_num = (pre_nms_top_n <= 0 || pre_nms_top_n > num) ? scores.numel() + : pre_nms_top_n; + scores_sort.Resize({pre_nms_num, 1}); + index_sort.Resize({pre_nms_num, 1}); + + // 2. box decode and clipping + Tensor proposals; + proposals.mutable_data({pre_nms_num, 5}, ctx.GetPlace()); + + { + platform::ForRange for_range(ctx, pre_nms_num); + for_range(RBoxDecodeAndClipFunctor{anchors.data(), + bbox_deltas.data(), + variances.data(), + index_sort.data(), + im_info.data(), + proposals.data()}); + } + + // 3. filter + Tensor keep_index, keep_num_t; + keep_index.mutable_data({pre_nms_num}, ctx.GetPlace()); + keep_num_t.mutable_data({1}, ctx.GetPlace()); + min_size = std::max(min_size, 0.0f); + auto stream = ctx.stream(); + RFilterBBoxes<<<1, 256, 0, stream>>>(proposals.data(), + im_info.data(), + min_size, + pre_nms_num, + keep_num_t.data(), + keep_index.data()); + int keep_num; + const auto gpu_place = boost::get(ctx.GetPlace()); + memory::Copy(platform::CPUPlace(), + &keep_num, + gpu_place, + keep_num_t.data(), + sizeof(int), + ctx.stream()); + ctx.Wait(); + keep_index.Resize({keep_num}); + + Tensor scores_filter, proposals_filter; + proposals_filter.mutable_data({keep_num, 5}, ctx.GetPlace()); + scores_filter.mutable_data({keep_num, 1}, ctx.GetPlace()); + GPUGather(ctx, proposals, keep_index, &proposals_filter); + GPUGather(ctx, scores_sort, keep_index, &scores_filter); + + if (nms_thresh <= 0) { + return std::make_pair(proposals_filter, scores_filter); + } + + // 4. nms + Tensor keep_nms; + RNMS(ctx, proposals_filter, keep_index, nms_thresh, &keep_nms); + if (post_nms_top_n > 0 && post_nms_top_n < keep_nms.numel()) { + keep_nms.Resize({post_nms_top_n}); + } + + Tensor scores_nms, proposals_nms; + proposals_nms.mutable_data({keep_nms.numel(), 5}, ctx.GetPlace()); + scores_nms.mutable_data({keep_nms.numel(), 1}, ctx.GetPlace()); + GPUGather(ctx, proposals_filter, keep_nms, &proposals_nms); + GPUGather(ctx, scores_filter, keep_nms, &scores_nms); + + return std::make_pair(proposals_nms, scores_nms); +} +} // namespace + +template +class CUDARRPNGenerateProposalsKernel : public framework::OpKernel { +public: + void Compute(const framework::ExecutionContext &context) const override { + auto *scores = context.Input("Scores"); + auto *bbox_deltas = context.Input("BboxDeltas"); + auto *im_info = context.Input("ImInfo"); + auto anchors = detail::Ref(context.Input("Anchors"), + "Cannot find input Anchors(%s) in scope", + context.InputNames("Anchors")[0]); + auto variances = detail::Ref(context.Input("Variances"), + "Cannot find input Variances(%s) in scope", + context.InputNames("Variances")[0]); + + auto *rpn_rois = context.Output("RpnRois"); + auto *rpn_roi_probs = context.Output("RpnRoiProbs"); + + int pre_nms_top_n = context.Attr("pre_nms_topN"); + int post_nms_top_n = context.Attr("post_nms_topN"); + float nms_thresh = context.Attr("nms_thresh"); + float min_size = context.Attr("min_size"); + + auto &dev_ctx = context.template device_context(); + + auto scores_dim = scores->dims(); + int64_t num = scores_dim[0]; + int64_t c_score = scores_dim[1]; + int64_t h_score = scores_dim[2]; + int64_t w_score = scores_dim[3]; + + auto bbox_dim = bbox_deltas->dims(); + int64_t c_bbox = bbox_dim[1]; + int64_t h_bbox = bbox_dim[2]; + int64_t w_bbox = bbox_dim[3]; + + Tensor bbox_deltas_swap, scores_swap; + bbox_deltas_swap.mutable_data({num, h_bbox, w_bbox, c_bbox}, + dev_ctx.GetPlace()); + scores_swap.mutable_data({num, h_score, w_score, c_score}, + dev_ctx.GetPlace()); + + math::Transpose trans; + std::vector axis = {0, 2, 3, 1}; + trans(dev_ctx, *bbox_deltas, &bbox_deltas_swap, axis); + trans(dev_ctx, *scores, &scores_swap, axis); + + anchors.Resize({anchors.numel() / 5, 5}); + variances.Resize({variances.numel() / 5, 5}); + + rpn_rois->mutable_data({bbox_deltas->numel() / 5, 5}, + context.GetPlace()); + rpn_roi_probs->mutable_data({scores->numel(), 1}, context.GetPlace()); + + T *rpn_rois_data = rpn_rois->data(); + T *rpn_roi_probs_data = rpn_roi_probs->data(); + + auto place = boost::get(dev_ctx.GetPlace()); + + int64_t num_proposals = 0; + std::vector offset(1, 0); + for (int64_t i = 0; i < num; ++i) { + Tensor im_info_slice = im_info->Slice(i, i + 1); + Tensor bbox_deltas_slice = bbox_deltas_swap.Slice(i, i + 1); + Tensor scores_slice = scores_swap.Slice(i, i + 1); + + bbox_deltas_slice.Resize({h_bbox * w_bbox * c_bbox / 5, 5}); + scores_slice.Resize({h_score * w_score * c_score, 1}); + // auto* scores_data = scores_slice.data(); + // for(int k=0; k < 256; k++) { + // std::cout << scores_data[k] << std::endl; + // } + std::pair box_score_pair = + RRPNProposalForOneImage(dev_ctx, + im_info_slice, + anchors, + variances, + bbox_deltas_slice, + scores_slice, + pre_nms_top_n, + post_nms_top_n, + nms_thresh, + min_size); + + Tensor &proposals = box_score_pair.first; + Tensor &scores = box_score_pair.second; + + memory::Copy(place, + rpn_rois_data + num_proposals * 5, + place, + proposals.data(), + sizeof(T) * proposals.numel(), + dev_ctx.stream()); + memory::Copy(place, + rpn_roi_probs_data + num_proposals, + place, + scores.data(), + sizeof(T) * scores.numel(), + dev_ctx.stream()); + dev_ctx.Wait(); + num_proposals += proposals.dims()[0]; + offset.emplace_back(num_proposals); + } + framework::LoD lod; + lod.emplace_back(offset); + rpn_rois->set_lod(lod); + rpn_roi_probs->set_lod(lod); + rpn_rois->Resize({num_proposals, 5}); + rpn_roi_probs->Resize({num_proposals, 1}); + } +}; + +} // namespace operators +} // namespace paddle + +namespace ops = paddle::operators; +REGISTER_OP_CUDA_KERNEL( + rrpn_generate_proposals, + ops::CUDARRPNGenerateProposalsKernel); diff --git a/PaddleCV/rrpn/models/ext_op/src/rrpn_rotated_roi_align_op.cc b/PaddleCV/rrpn/models/ext_op/src/rrpn_rotated_roi_align_op.cc new file mode 100644 index 00000000..2d3ff455 --- /dev/null +++ b/PaddleCV/rrpn/models/ext_op/src/rrpn_rotated_roi_align_op.cc @@ -0,0 +1,197 @@ +/* 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. */ + +#include +#include +#include +#include "math_function.h" +#include "paddle/fluid/framework/op_registry.h" + +namespace paddle { +namespace operators { + +using Tensor = framework::Tensor; +using LoDTensor = framework::LoDTensor; + +class RRPNRotatedROIAlignOp : public framework::OperatorWithKernel { +public: + using framework::OperatorWithKernel::OperatorWithKernel; + + void InferShape(framework::InferShapeContext* ctx) const override { + PADDLE_ENFORCE(ctx->HasInput("X"), + "Input(X) of Rotated ROIAlignOp should not be null."); + PADDLE_ENFORCE(ctx->HasInput("ROIs"), + "Input(ROIs) of Rotated ROIAlignOp should not be null."); + PADDLE_ENFORCE(ctx->HasOutput("Out"), + "Output(Out) of Rotated ROIAlignOp should not be null."); + auto input_dims = ctx->GetInputDim("X"); + auto rois_dims = ctx->GetInputDim("ROIs"); + + PADDLE_ENFORCE(input_dims.size() == 4, + "The format of input tensor is NCHW."); + PADDLE_ENFORCE(rois_dims.size() == 2, + "ROIs should be a 2-D LoDTensor of shape (num_rois, 5)" + "given as [[x1, y1, x2, y2, theta], ...]."); + if (ctx->IsRuntime()) { + PADDLE_ENFORCE(rois_dims[1] == 5, + "ROIs should be a 2-D LoDTensor of shape (num_rois, 5)" + "given as [[x1, y1, x2, y2, theta], ...]."); + } + int pooled_height = ctx->Attrs().Get("pooled_height"); + int pooled_width = ctx->Attrs().Get("pooled_width"); + float spatial_scale = ctx->Attrs().Get("spatial_scale"); + + PADDLE_ENFORCE_GT( + pooled_height, 0, "The pooled output height must greater than 0"); + PADDLE_ENFORCE_GT( + pooled_width, 0, "The pooled output width must greater than 0"); + PADDLE_ENFORCE_GT( + spatial_scale, 0.0f, "The spatial scale must greater than 0"); + + auto out_dims = input_dims; + out_dims[0] = rois_dims[0]; + out_dims[1] = input_dims[1]; + out_dims[2] = pooled_height; + out_dims[3] = pooled_width; + + ctx->SetOutputDim("Out", out_dims); + ctx->SetOutputDim("ConIdX", out_dims); + ctx->SetOutputDim("ConIdY", out_dims); + } + +protected: + framework::OpKernelType GetExpectedKernelType( + const framework::ExecutionContext& ctx) const override { + return framework::OpKernelType(ctx.Input("X")->type(), + ctx.device_context()); + } +}; + +class RRPNRotatedROIAlignGradOp : public framework::OperatorWithKernel { +public: + using framework::OperatorWithKernel::OperatorWithKernel; + + void InferShape(framework::InferShapeContext* ctx) const override { + PADDLE_ENFORCE(ctx->HasInput(framework::GradVarName("Out")), + "The GRAD@Out of RotatedROIAlignGradOp should not be null."); + PADDLE_ENFORCE(ctx->HasOutputs(framework::GradVarName("X")), + "The GRAD@X of RotatedROIAlignGradOp should not be null."); + ctx->SetOutputsDim(framework::GradVarName("X"), ctx->GetInputsDim("X")); + } + +protected: + framework::OpKernelType GetExpectedKernelType( + const framework::ExecutionContext& ctx) const override { + return framework::OpKernelType(ctx.Input("ROIs")->type(), + ctx.device_context()); + } +}; + +class RRPNRotatedROIAlignOpMaker : public framework::OpProtoAndCheckerMaker { +public: + void Make() override { + AddInput("X", + "(Tensor), " + "The input of RRPNRotatedROIAlignOp. The data type is float32 or " + "float64." + "The format of input tensor is NCHW. Where N is batch size, " + "C is the number of input channels, " + "H is the height of the feature, and " + "W is the width of the feature."); + AddInput("ROIs", + "(LoDTensor), " + "ROIs (Regions of Interest) to pool over. " + "should be a 2-D LoDTensor of shape (num_rois, 5)" + "given as [[x, y, w, h, theta], ...]. " + "(x, y) is the center coordinates, and " + "(w, h) is the bottom right coordinates, theta is rotation angle" + "of ROI."); + AddOutput("Out", + "(Tensor), " + "The output of ROIAlignOp is a 4-D tensor with shape " + "(num_rois, channels, pooled_h, pooled_w). The data type is " + "float32 or float64."); + AddOutput("ConIdX", + "(Tensor), " + "index x of affine transform"); + AddOutput("ConIdY", + "(Tensor), " + "index y of affine transform"); + + AddAttr("spatial_scale", + "(float, default 1.0), " + "Multiplicative spatial scale factor " + "to translate ROI coords from their input scale " + "to the scale used when pooling.") + .SetDefault(1.0); + AddAttr("pooled_height", + "(int, default 1), " + "The pooled output height.") + .SetDefault(1); + AddAttr("pooled_width", + "(int, default 1), " + "The pooled output width.") + .SetDefault(1); + AddComment(R"DOC( +**RotatedRoIAlign Operator** + +Rotated Region of interest align (also known as Rotated RoI align) is to perform +bilinear interpolation on inputs of nonuniform sizes to obtain +fixed-size feature maps (e.g. 7*7) + +Dividing each region proposal into equal-sized sections with +the pooled_width and pooled_height. Location remains the origin +result. + +In each ROI bin, the value of the four regularly sampled locations +are computed directly through bilinear interpolation. The output is +the mean of four locations. +Thus avoid the misaligned problem. + )DOC"); + } +}; + +template +class RRPNRotatedROIAlignGradMaker : public framework::SingleGradOpMaker { +public: + using framework::SingleGradOpMaker::SingleGradOpMaker; + +protected: + std::unique_ptr Apply() const override { + std::unique_ptr op(new T); + op->SetType("rrpn_rotated_roi_align_grad"); + op->SetInput("X", this->Input("X")); + op->SetInput("ROIs", this->Input("ROIs")); + op->SetInput("ConIdX", this->Output("ConIdX")); + op->SetInput("ConIdY", this->Output("ConIdY")); + op->SetInput(framework::GradVarName("Out"), this->OutputGrad("Out")); + op->SetOutput(framework::GradVarName("X"), this->InputGrad("X")); + op->SetAttrMap(this->Attrs()); + return op; + } +}; + +DECLARE_NO_NEED_BUFFER_VARS_INFERENCE( + RRPNRotatedRoiAlignGradNoNeedBufVarsInferer, "X"); + +} // namespace operators +} // namespace paddle + +namespace ops = paddle::operators; +REGISTER_OPERATOR( + rrpn_rotated_roi_align, + ops::RRPNRotatedROIAlignOp, + ops::RRPNRotatedROIAlignOpMaker, + ops::RRPNRotatedROIAlignGradMaker, + ops::RRPNRotatedROIAlignGradMaker); +REGISTER_OPERATOR(rrpn_rotated_roi_align_grad, + ops::RRPNRotatedROIAlignGradOp, + ops::RRPNRotatedRoiAlignGradNoNeedBufVarsInferer); diff --git a/PaddleCV/rrpn/models/ext_op/src/rrpn_rotated_roi_align_op.cu b/PaddleCV/rrpn/models/ext_op/src/rrpn_rotated_roi_align_op.cu new file mode 100644 index 00000000..c68209e2 --- /dev/null +++ b/PaddleCV/rrpn/models/ext_op/src/rrpn_rotated_roi_align_op.cu @@ -0,0 +1,442 @@ +/* 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 +@misc{ma2019rrpn, + author = {Jianqi Ma}, + title = {{RRPN in pytorch}}, + year = {2019}, + howpublished = {\url{https://github.com/mjq11302010044/RRPN_pytorch}}, +} +@article{Jianqi17RRPN, + Author = {Jianqi Ma and Weiyuan Shao and Hao Ye and Li Wang and Hong Wang +and Yingbin Zheng and Xiangyang Xue}, + Title = {Arbitrary-Oriented Scene Text Detection via Rotation Proposals}, + journal = {IEEE Transactions on Multimedia}, + volume={20}, + number={11}, + pages={3111-3122}, + year={2018} +}*/ + +#include +#include +#include "paddle/fluid/framework/op_registry.h" +#include "paddle/fluid/memory/memory.h" +#include "paddle/fluid/platform/cuda_primitives.h" + +#define CUDA_1D_KERNEL_LOOP(i, n) \ + for (int i = blockIdx.x * blockDim.x + threadIdx.x; i < n; \ + i += blockDim.x * gridDim.x) + +namespace paddle { +namespace operators { + +using Tensor = framework::Tensor; +using LoDTensor = framework::LoDTensor; + +static constexpr int kNumCUDAThreads = 512; +static constexpr int kNumMaxinumNumBlocks = 4096; +#define PI 3.141592654 + +static inline int NumBlocks(const int N) { + return std::min((N + kNumCUDAThreads - 1) / kNumCUDAThreads, + kNumMaxinumNumBlocks); +} + + +template +__global__ void Zero(T* x, int num) { + for (int i = blockIdx.x * blockDim.x + threadIdx.x; i < num; + i += blockDim.x * gridDim.x) { + x[i] = static_cast(0); + } +} + +template +__global__ void RROIAlignForward(const int nthreads, + const T* bottom_data, + const T spatial_scale, + int height, + int width, + int channels, + const int pooled_height, + const int pooled_width, + const T* bottom_rois, + int* roi_batch_id_data, + T* top_data, + T* con_idx_x, + T* con_idx_y) { + CUDA_1D_KERNEL_LOOP(index, nthreads) { + int imageWidth = width; + int imageHeight = height; + + // (n, c, ph, pw) is an element in the pooled output + int n = index; + int pw = n % pooled_width; + n /= pooled_width; + int ph = n % pooled_height; + n /= pooled_height; + int c = n % channels; + n /= channels; + + const T* offset_bottom_rois = bottom_rois + n * 5; + + int roi_batch_ind = roi_batch_id_data[n]; + T cx = offset_bottom_rois[0]; + T cy = offset_bottom_rois[1]; + T h = offset_bottom_rois[3]; + T w = offset_bottom_rois[2]; + T angle = offset_bottom_rois[4] / 180.0 * PI; + + // TransformPrepare + T dx = -pooled_width / 2.0; + T dy = -pooled_height / 2.0; + T Sx = w * spatial_scale / pooled_width; + T Sy = h * spatial_scale / pooled_height; + T Alpha = cos(angle); + T Beta = sin(angle); + T Dx = cx * spatial_scale; + T Dy = cy * spatial_scale; + + T M[2][3]; + M[0][0] = Alpha * Sx; + M[0][1] = Beta * Sy; + M[0][2] = Alpha * Sx * dx + Beta * Sy * dy + Dx; + M[1][0] = -Beta * Sx; + M[1][1] = Alpha * Sy; + M[1][2] = -Beta * Sx * dx + Alpha * Sy * dy + Dy; + + T P[8]; + P[0] = M[0][0] * pw + M[0][1] * ph + M[0][2]; + P[1] = M[1][0] * pw + M[1][1] * ph + M[1][2]; + P[2] = M[0][0] * pw + M[0][1] * (ph + 1) + M[0][2]; + P[3] = M[1][0] * pw + M[1][1] * (ph + 1) + M[1][2]; + P[4] = M[0][0] * (pw + 1) + M[0][1] * ph + M[0][2]; + P[5] = M[1][0] * (pw + 1) + M[1][1] * ph + M[1][2]; + P[6] = M[0][0] * (pw + 1) + M[0][1] * (ph + 1) + M[0][2]; + P[7] = M[1][0] * (pw + 1) + M[1][1] * (ph + 1) + M[1][2]; + + T leftMost = (max(round(min(min(P[0], P[2]), min(P[4], P[6]))), 0.0)); + T rightMost = + (min(round(max(max(P[0], P[2]), max(P[4], P[6]))), imageWidth - 1.0)); + T topMost = (max(round(min(min(P[1], P[3]), min(P[5], P[7]))), 0.0)); + T bottomMost = + (min(round(max(max(P[1], P[3]), max(P[5], P[7]))), imageHeight - 1.0)); + + const T* offset_bottom_data = + bottom_data + (roi_batch_ind * channels + c) * height * width; + + + float bin_cx = (leftMost + rightMost) / 2.0; // shift + float bin_cy = (topMost + bottomMost) / 2.0; + + int bin_l = (int)floor(bin_cx); + int bin_r = (int)ceil(bin_cx); + int bin_t = (int)floor(bin_cy); + int bin_b = (int)ceil(bin_cy); + + T lt_value = 0.0; + if (bin_t > 0 && bin_l > 0 && bin_t < height && bin_l < width) + lt_value = offset_bottom_data[bin_t * width + bin_l]; + T rt_value = 0.0; + if (bin_t > 0 && bin_r > 0 && bin_t < height && bin_r < width) + rt_value = offset_bottom_data[bin_t * width + bin_r]; + T lb_value = 0.0; + if (bin_b > 0 && bin_l > 0 && bin_b < height && bin_l < width) + lb_value = offset_bottom_data[bin_b * width + bin_l]; + T rb_value = 0.0; + if (bin_b > 0 && bin_r > 0 && bin_b < height && bin_r < width) + rb_value = offset_bottom_data[bin_b * width + bin_r]; + + T rx = bin_cx - floor(bin_cx); + T ry = bin_cy - floor(bin_cy); + + T wlt = (1.0 - rx) * (1.0 - ry); + T wrt = rx * (1.0 - ry); + T wrb = rx * ry; + T wlb = (1.0 - rx) * ry; + + T inter_val = 0.0; + + inter_val += lt_value * wlt; + inter_val += rt_value * wrt; + inter_val += rb_value * wrb; + inter_val += lb_value * wlb; + + platform::CudaAtomicAdd(top_data + index, static_cast(inter_val)); + platform::CudaAtomicAdd(con_idx_x + index, static_cast(bin_cx)); + platform::CudaAtomicAdd(con_idx_y + index, static_cast(bin_cy)); + } +} + +template +__global__ void RROIAlignBackward(const int nthreads, + const T* top_diff, + const float* con_idx_x, + const float* con_idx_y, + const int num_rois, + const float spatial_scale, + const int height, + const int width, + const int channels, + const int pooled_height, + const int pooled_width, + T* bottom_diff, + const T* bottom_rois, + int* roi_batch_id_data) { + CUDA_1D_KERNEL_LOOP(index, nthreads) { + // (n, c, ph, pw) is an element in the pooled output + int n = index; + n /= pooled_width; + n /= pooled_height; + int c = n % channels; + n /= channels; + + const T* offset_bottom_rois = bottom_rois + n * 5; + int roi_batch_ind = roi_batch_id_data[n]; + T* offset_bottom_diff = + bottom_diff + (roi_batch_ind * channels + c) * height * width; + + + float bw = con_idx_x[index]; + float bh = con_idx_y[index]; + + int bin_xs = int(floor(bw)); + int bin_ys = int(floor(bh)); + + float rx = bw - float(bin_xs); + float ry = bh - float(bin_ys); + + T wlt = (1.0 - rx) * (1.0 - ry); + T wrt = rx * (1.0 - ry); + T wrb = rx * ry; + T wlb = (1.0 - rx) * ry; + + + int min_x = (int)floor(bw); + int max_x = (int)ceil(bw); + int min_y = (int)floor(bh); + int max_y = (int)ceil(bh); + + T top_diff_of_bin = top_diff[index]; + + T v1 = wlt * top_diff_of_bin; + T v2 = wrt * top_diff_of_bin; + T v3 = wrb * top_diff_of_bin; + T v4 = wlb * top_diff_of_bin; + + + if (min_y > 0 && min_x > 0 && min_y < height - 1 && min_x < width - 1) + platform::CudaAtomicAdd(offset_bottom_diff + min_y * width + min_x, + static_cast(v1)); + if (min_y > 0 && max_x < width - 1 && min_y < height - 1 && max_x > 0) + platform::CudaAtomicAdd(offset_bottom_diff + min_y * width + max_x, + static_cast(v2)); + if (max_y < height - 1 && max_x < width - 1 && max_y > 0 && max_x > 0) + platform::CudaAtomicAdd(offset_bottom_diff + max_y * width + max_x, + static_cast(v3)); + if (max_y < height - 1 && min_x > 0 && max_y > 0 && min_x < width - 1) + platform::CudaAtomicAdd(offset_bottom_diff + max_y * width + min_x, + static_cast(v4)); + } +} + +template +class RRPNROIAlignRotatedCUDAKernel : public framework::OpKernel { +public: + void Compute(const framework::ExecutionContext& ctx) const override { + auto* input = ctx.Input("X"); + auto* rois = ctx.Input("ROIs"); + auto* out = ctx.Output("Out"); + auto* con_idx_x = ctx.Output("ConIdX"); + auto* con_idx_y = ctx.Output("ConIdY"); + + auto pooled_height = ctx.Attr("pooled_height"); + auto pooled_width = ctx.Attr("pooled_width"); + auto spatial_scale = ctx.Attr("spatial_scale"); + + auto in_dims = input->dims(); + int batch_size = in_dims[0]; + int channels = in_dims[1]; + int height = in_dims[2]; + int width = in_dims[3]; + + int rois_num = rois->dims()[0]; + + if (rois_num == 0) return; + + int output_size = out->numel(); + int blocks = NumBlocks(output_size); + int threads = kNumCUDAThreads; + + Tensor roi_batch_id_list; + roi_batch_id_list.Resize({rois_num}); + auto cplace = platform::CPUPlace(); + int* roi_batch_id_data = roi_batch_id_list.mutable_data(cplace); + auto lod = rois->lod(); + PADDLE_ENFORCE_EQ( + lod.empty(), + false, + "Input(ROIs) Tensor of ROIAlignOp does not contain LoD information."); + auto rois_lod = lod.back(); + int rois_batch_size = rois_lod.size() - 1; + PADDLE_ENFORCE_EQ( + rois_batch_size, + batch_size, + "The rois_batch_size and imgs batch_size must be the same."); + int rois_num_with_lod = rois_lod[rois_batch_size]; + PADDLE_ENFORCE_EQ(rois_num, + rois_num_with_lod, + "The rois_num from input and lod must be the same."); + for (int n = 0; n < rois_batch_size; ++n) { + for (size_t i = rois_lod[n]; i < rois_lod[n + 1]; ++i) { + roi_batch_id_data[i] = n; + } + } + auto& dev_ctx = ctx.cuda_device_context(); + int bytes = roi_batch_id_list.numel() * sizeof(int); + auto roi_ptr = memory::Alloc(dev_ctx, bytes); + int* roi_id_data = reinterpret_cast(roi_ptr->ptr()); + const auto gplace = boost::get(ctx.GetPlace()); + memory::Copy(gplace, + roi_id_data, + cplace, + roi_batch_id_data, + bytes, + dev_ctx.stream()); + + T* out_ = out->mutable_data(ctx.GetPlace()); + T* con_idx_x_ = con_idx_x->mutable_data(ctx.GetPlace()); + T* con_idx_y_ = con_idx_y->mutable_data(ctx.GetPlace()); + + int idx_x_num = con_idx_x->numel(); + int idx_y_num = con_idx_y->numel(); + int out_num = out->numel(); + Zero<<<(idx_x_num + 512 - 1) / 512, 512, 0, dev_ctx.stream()>>>(con_idx_x_, + idx_x_num); + Zero<<<(idx_y_num + 512 - 1) / 512, 512, 0, dev_ctx.stream()>>>(con_idx_y_, + idx_y_num); + Zero<<<(out_num + 512 - 1) / 512, 512, 0, dev_ctx.stream()>>>(out_, + out_num); + + RROIAlignForward<<>>( + output_size, + input->data(), + spatial_scale, + height, + width, + channels, + pooled_height, + pooled_width, + rois->data(), + roi_id_data, + out_, + con_idx_x_, + con_idx_y_); + } +}; + +template +class RRPNROIAlignRotatedGradCUDAKernel : public framework::OpKernel { +public: + void Compute(const framework::ExecutionContext& ctx) const override { + auto* input = ctx.Input("X"); + auto* rois = ctx.Input("ROIs"); + + auto* out_grad = ctx.Input(framework::GradVarName("Out")); + auto* in_grad = ctx.Output(framework::GradVarName("X")); + auto* con_idx_x = ctx.Input("ConIdX"); + auto* con_idx_y = ctx.Input("ConIdY"); + auto pooled_height = ctx.Attr("pooled_height"); + auto pooled_width = ctx.Attr("pooled_width"); + auto spatial_scale = ctx.Attr("spatial_scale"); + + int rois_num = rois->dims()[0]; + int channels = input->dims()[1]; + int height = input->dims()[2]; + int width = input->dims()[3]; + + if (!in_grad) { + return; + } + Tensor roi_batch_id_list; + roi_batch_id_list.Resize({rois_num}); + auto cplace = platform::CPUPlace(); + int* roi_batch_id_data = roi_batch_id_list.mutable_data(cplace); + auto rois_lod = rois->lod().back(); + int rois_batch_size = rois_lod.size() - 1; + for (int n = 0; n < rois_batch_size; ++n) { + for (size_t i = rois_lod[n]; i < rois_lod[n + 1]; ++i) { + roi_batch_id_data[i] = n; + } + } + auto& dev_ctx = ctx.cuda_device_context(); + auto roi_ptr = + memory::Alloc(dev_ctx, roi_batch_id_list.numel() * sizeof(int)); + int* roi_id_data = reinterpret_cast(roi_ptr->ptr()); + int bytes = roi_batch_id_list.numel() * sizeof(int); + const auto gplace = boost::get(ctx.GetPlace()); + memory::Copy(gplace, + roi_id_data, + cplace, + roi_batch_id_data, + bytes, + dev_ctx.stream()); + T* in_grad_ = in_grad->mutable_data(ctx.GetPlace()); + int in_grad_num = in_grad->numel(); + Zero<<<(in_grad_num + 512 - 1) / 512, 512, 0, dev_ctx.stream()>>>( + in_grad_, in_grad_num); + int output_grad_size = out_grad->numel(); + int blocks = NumBlocks(output_grad_size); + int threads = kNumCUDAThreads; + con_idx_x->data(); + con_idx_y->data(); + out_grad->data(); + rois->data(); + if (output_grad_size > 0) { + RROIAlignBackward<<>>( + output_grad_size, + out_grad->data(), + con_idx_x->data(), + con_idx_y->data(), + rois_num, + spatial_scale, + height, + width, + channels, + pooled_height, + pooled_width, + in_grad_, + // in_grad->mutable_data(ctx.GetPlace()), + rois->data(), + roi_id_data); + } + } +}; + + +} // namespace operators +} // namespace paddle + +namespace ops = paddle::operators; +REGISTER_OP_CUDA_KERNEL( + rrpn_rotated_roi_align, + ops::RRPNROIAlignRotatedCUDAKernel, + ops::RRPNROIAlignRotatedCUDAKernel); +REGISTER_OP_CUDA_KERNEL( + rrpn_rotated_roi_align_grad, + ops::RRPNROIAlignRotatedGradCUDAKernel, + ops::RRPNROIAlignRotatedGradCUDAKernel); diff --git a/PaddleCV/rrpn/models/ext_op/src/rrpn_target_assign_op.cc b/PaddleCV/rrpn/models/ext_op/src/rrpn_target_assign_op.cc new file mode 100644 index 00000000..74e2fe0b --- /dev/null +++ b/PaddleCV/rrpn/models/ext_op/src/rrpn_target_assign_op.cc @@ -0,0 +1,544 @@ +/* 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. */ + +#include +#include +#include +#include "bbox_util.h" +#include "paddle/fluid/framework/op_registry.h" + +namespace paddle { +namespace operators { + +using Tensor = framework::Tensor; +using LoDTensor = framework::LoDTensor; +template +using EigenMatrix = framework::EigenMatrix; + +class RRpnTargetAssignOp : public framework::OperatorWithKernel { +public: + using framework::OperatorWithKernel::OperatorWithKernel; + + void InferShape(framework::InferShapeContext* ctx) const override { + PADDLE_ENFORCE(ctx->HasInput("Anchor"), + "Input(Anchor) of RRpnTargetAssignOp should not be null"); + PADDLE_ENFORCE(ctx->HasInput("GtBoxes"), + "Input(GtBoxes) of RRpnTargetAssignOp should not be null"); + PADDLE_ENFORCE(ctx->HasInput("ImInfo"), + "Input(ImInfo) of RRpnTargetAssignOp should not be null"); + PADDLE_ENFORCE( + ctx->HasOutput("LocationIndex"), + "Output(LocationIndex) of RRpnTargetAssignOp should not be null"); + PADDLE_ENFORCE( + ctx->HasOutput("ScoreIndex"), + "Output(ScoreIndex) of RRpnTargetAssignOp should not be null"); + PADDLE_ENFORCE( + ctx->HasOutput("TargetLabel"), + "Output(TargetLabel) of RRpnTargetAssignOp should not be null"); + PADDLE_ENFORCE( + ctx->HasOutput("TargetBBox"), + "Output(TargetBBox) of RRpnTargetAssignOp should not be null"); + + auto anchor_dims = ctx->GetInputDim("Anchor"); + auto gt_boxes_dims = ctx->GetInputDim("GtBoxes"); + auto im_info_dims = ctx->GetInputDim("ImInfo"); + PADDLE_ENFORCE_EQ( + anchor_dims.size(), 2, "The rank of Input(Anchor) must be 2."); + PADDLE_ENFORCE_EQ( + gt_boxes_dims.size(), 2, "The rank of Input(GtBoxes) must be 2."); + PADDLE_ENFORCE_EQ( + im_info_dims.size(), 2, "The rank of Input(ImInfo) must be 2."); + + ctx->SetOutputDim("LocationIndex", {-1}); + ctx->SetOutputDim("ScoreIndex", {-1}); + ctx->SetOutputDim("TargetLabel", {-1, 1}); + ctx->SetOutputDim("TargetBBox", {-1, 5}); + } + +protected: + framework::OpKernelType GetExpectedKernelType( + const framework::ExecutionContext& ctx) const override { + return framework::OpKernelType( + ctx.Input("Anchor")->type(), + platform::CPUPlace()); + } +}; + + +template +void AppendRpns(LoDTensor* out, int64_t offset, Tensor* to_add) { + auto* out_data = out->data(); + auto* to_add_data = to_add->data(); + memcpy(out_data + offset, to_add_data, to_add->numel() * sizeof(T)); +} + + +template +std::vector FilterStraddleAnchor( + const platform::CPUDeviceContext& context, + const Tensor* anchor, + const float rpn_straddle_thresh, + T im_height, + T im_width, + int64_t offset) { + std::vector inds_inside; + int anchor_num = anchor->dims()[0]; + auto* anchor_data = anchor->data(); + if (rpn_straddle_thresh >= 0) { + int index; + for (int i = 0; i < anchor_num; ++i) { + index = i * offset; + if ((anchor_data[index + 0] >= -rpn_straddle_thresh) && + (anchor_data[index + 1] >= -rpn_straddle_thresh) && + (anchor_data[index + 2] < im_width + rpn_straddle_thresh) && + (anchor_data[index + 3] < im_height + rpn_straddle_thresh)) { + inds_inside.emplace_back(i); + } + } + } else { + for (int i = 0; i < anchor_num; ++i) { + inds_inside.emplace_back(i); + } + } + int inside_num = inds_inside.size(); + Tensor inds_inside_t; + int* inds_inside_data = + inds_inside_t.mutable_data({inside_num}, context.GetPlace()); + std::copy(inds_inside.begin(), inds_inside.end(), inds_inside_data); + Tensor inside_anchor_t; + T* inside_anchor_data = + inside_anchor_t.mutable_data({inside_num, offset}, context.GetPlace()); + Gather(anchor->data(), + offset, + inds_inside_data, + inside_num, + inside_anchor_data); + std::vector res; + res.emplace_back(inds_inside_t); + res.emplace_back(inside_anchor_t); + return res; +} + + +void ReservoirSampling(const int num, + std::vector* inds, + std::minstd_rand engine, + bool use_random) { + std::uniform_real_distribution uniform(0, 1); + size_t len = inds->size(); + if (len > static_cast(num)) { + if (use_random) { + for (size_t i = num; i < len; ++i) { + int rng_ind = std::floor(uniform(engine) * i); + if (rng_ind < num) + std::iter_swap(inds->begin() + rng_ind, inds->begin() + i); + } + } + inds->resize(num); + } +} + + +template +void RRpnScoreAssign(const T* anchor_by_gt_overlap_data, + const Tensor& anchor_to_gt_max, + const Tensor& gt_to_anchor_max, + const int rpn_batch_size_per_im, + const float rpn_fg_fraction, + const float rpn_positive_overlap, + const float rpn_negative_overlap, + std::vector* fg_inds, + std::vector* bg_inds, + std::vector* tgt_lbl, + std::minstd_rand engine, + bool use_random) { + float epsilon = 0.00000001; + int anchor_num = anchor_to_gt_max.dims()[0]; + int gt_num = gt_to_anchor_max.dims()[0]; + std::vector target_label(anchor_num, -1); + const T* anchor_to_gt_max_data = anchor_to_gt_max.data(); + const T* gt_to_anchor_max_data = gt_to_anchor_max.data(); + for (int64_t i = 0; i < anchor_num; ++i) { + bool is_anchors_with_max_overlap = false; + int64_t j = 0; + for (; j < gt_num; ++j) { + T value = anchor_by_gt_overlap_data[i * gt_num + j]; + T diff = std::abs(value - gt_to_anchor_max_data[j]); + if (diff < epsilon) { + is_anchors_with_max_overlap = true; + break; + } + } + bool is_anchor_great_than_thresh = + (anchor_to_gt_max_data[i] >= rpn_positive_overlap); + if (is_anchors_with_max_overlap || is_anchor_great_than_thresh) { + fg_inds->emplace_back(i); + target_label[i] = 1; + } + } + + // Reservoir Sampling + int fg_num = 0; + if (rpn_fg_fraction > 0 && rpn_batch_size_per_im > 0) { + fg_num = static_cast(rpn_fg_fraction * rpn_batch_size_per_im); + ReservoirSampling(fg_num, fg_inds, engine, use_random); + } + fg_num = static_cast(fg_inds->size()); + + for (int64_t i = 0; i < anchor_num; ++i) { + if (anchor_to_gt_max_data[i] < rpn_negative_overlap && + target_label[i] != 1) { + bg_inds->emplace_back(i); + target_label[i] = 0; + } + } + + + int bg_num = 0; + if (rpn_fg_fraction > 0 && rpn_batch_size_per_im > 0) { + bg_num = rpn_batch_size_per_im - fg_num; + ReservoirSampling(bg_num, bg_inds, engine, use_random); + } + bg_num = static_cast(bg_inds->size()); + tgt_lbl->resize(fg_num + bg_num, 0); + std::vector fg_lbl(fg_num, 1); + std::vector bg_lbl(bg_num, 0); + std::copy(fg_lbl.begin(), fg_lbl.end(), tgt_lbl->data()); + std::copy(bg_lbl.begin(), bg_lbl.end(), tgt_lbl->data() + fg_num); +} + +template +std::vector SampleRRpnFgBgGt(const platform::CPUDeviceContext& ctx, + const Tensor& anchor_by_gt_overlap, + const int rpn_batch_size_per_im, + const float rpn_positive_overlap, + const float rpn_negative_overlap, + const float rpn_fg_fraction, + std::minstd_rand engine, + bool use_random) { + auto* anchor_by_gt_overlap_data = anchor_by_gt_overlap.data(); + int anchor_num = anchor_by_gt_overlap.dims()[0]; + int gt_num = anchor_by_gt_overlap.dims()[1]; + + std::vector fg_inds; + std::vector bg_inds; + std::vector gt_inds; + std::vector tgt_lbl; + // Calculate the max IoU between anchors and gt boxes + // Map from anchor to gt box that has highest overlap + auto place = ctx.GetPlace(); + Tensor anchor_to_gt_max, anchor_to_gt_argmax, gt_to_anchor_max; + anchor_to_gt_max.mutable_data({anchor_num}, place); + int* argmax = anchor_to_gt_argmax.mutable_data({anchor_num}, place); + gt_to_anchor_max.mutable_data({gt_num}, place); + + auto anchor_by_gt_overlap_et = + framework::EigenMatrix::From(anchor_by_gt_overlap); + auto anchor_to_gt_max_et = + framework::EigenVector::Flatten(anchor_to_gt_max); + auto gt_to_anchor_max_et = + framework::EigenVector::Flatten(gt_to_anchor_max); + auto anchor_to_gt_argmax_et = + framework::EigenVector::Flatten(anchor_to_gt_argmax); + anchor_to_gt_max_et = + anchor_by_gt_overlap_et.maximum(Eigen::DSizes(1)); + anchor_to_gt_argmax_et = + anchor_by_gt_overlap_et.argmax(1).template cast(); + gt_to_anchor_max_et = + anchor_by_gt_overlap_et.maximum(Eigen::DSizes(0)); + + // Follow the Faster RCNN's implementation + RRpnScoreAssign(anchor_by_gt_overlap_data, + anchor_to_gt_max, + gt_to_anchor_max, + rpn_batch_size_per_im, + rpn_fg_fraction, + rpn_positive_overlap, + rpn_negative_overlap, + &fg_inds, + &bg_inds, + &tgt_lbl, + engine, + use_random); + + int fg_num = fg_inds.size(); + int bg_num = bg_inds.size(); + gt_inds.reserve(fg_num); + for (int i = 0; i < fg_num; ++i) { + gt_inds.emplace_back(argmax[fg_inds[i]]); + } + Tensor loc_index_t, score_index_t, tgt_lbl_t, gt_inds_t; + int* loc_index_data = loc_index_t.mutable_data({fg_num}, place); + int* score_index_data = + score_index_t.mutable_data({fg_num + bg_num}, place); + int* tgt_lbl_data = tgt_lbl_t.mutable_data({fg_num + bg_num}, place); + int* gt_inds_data = gt_inds_t.mutable_data({fg_num}, place); + std::copy(fg_inds.begin(), fg_inds.end(), loc_index_data); + std::copy(fg_inds.begin(), fg_inds.end(), score_index_data); + std::copy(bg_inds.begin(), bg_inds.end(), score_index_data + fg_num); + std::copy(tgt_lbl.begin(), tgt_lbl.end(), tgt_lbl_data); + std::copy(gt_inds.begin(), gt_inds.end(), gt_inds_data); + std::vector loc_score_tgtlbl_gt; + loc_score_tgtlbl_gt.emplace_back(loc_index_t); + loc_score_tgtlbl_gt.emplace_back(score_index_t); + loc_score_tgtlbl_gt.emplace_back(tgt_lbl_t); + loc_score_tgtlbl_gt.emplace_back(gt_inds_t); + + return loc_score_tgtlbl_gt; +} + +template +class RRpnTargetAssignKernel : public framework::OpKernel { +public: + void Compute(const framework::ExecutionContext& context) const override { + auto* anchor = context.Input("Anchor"); // (H*W*A) * 5 + auto* gt_boxes = context.Input("GtBoxes"); + auto* im_info = context.Input("ImInfo"); + + auto* loc_index = context.Output("LocationIndex"); + auto* score_index = context.Output("ScoreIndex"); + auto* tgt_bbox = context.Output("TargetBBox"); + auto* tgt_lbl = context.Output("TargetLabel"); + + PADDLE_ENFORCE_EQ(gt_boxes->lod().size(), + 1UL, + "RRpnTargetAssignOp gt_boxes needs 1 level of LoD"); + int64_t anchor_num = static_cast(anchor->dims()[0]); + int64_t batch_num = static_cast(gt_boxes->lod().back().size() - 1); + + int rpn_batch_size_per_im = context.Attr("rpn_batch_size_per_im"); + float rpn_straddle_thresh = context.Attr("rpn_straddle_thresh"); + float rpn_positive_overlap = context.Attr("rpn_positive_overlap"); + float rpn_negative_overlap = context.Attr("rpn_negative_overlap"); + float rpn_fg_fraction = context.Attr("rpn_fg_fraction"); + bool use_random = context.Attr("use_random"); + int64_t max_num = batch_num * rpn_batch_size_per_im; + auto place = context.GetPlace(); + + loc_index->mutable_data({max_num}, place); + score_index->mutable_data({max_num}, place); + tgt_bbox->mutable_data({max_num, 5}, place); + tgt_lbl->mutable_data({max_num, 1}, place); + auto& dev_ctx = context.device_context(); + + std::random_device rnd; + std::minstd_rand engine; + int seed = rnd(); + engine.seed(seed); + + framework::LoD lod_loc, loc_score; + std::vector lod0_loc(1, 0); + std::vector lod0_score(1, 0); + + int total_loc_num = 0; + int total_score_num = 0; + auto gt_boxes_lod = gt_boxes->lod().back(); + for (int i = 0; i < batch_num; ++i) { + Tensor gt_boxes_slice = + gt_boxes->Slice(gt_boxes_lod[i], gt_boxes_lod[i + 1]); + Tensor im_info_slice = im_info->Slice(i, i + 1); + auto* im_info_data = im_info_slice.data(); + auto im_height = im_info_data[0]; + auto im_width = im_info_data[1]; + // auto im_scale = im_info_data[2]; + // Filter straddle anchor + std::vector filter_output = FilterStraddleAnchor( + dev_ctx, anchor, rpn_straddle_thresh, im_height, im_width, 5); + Tensor inds_inside = filter_output[0]; + Tensor inside_anchor = filter_output[1]; + + Tensor anchor_by_gt_overlap; + anchor_by_gt_overlap.mutable_data( + {inside_anchor.dims()[0], gt_boxes_slice.dims()[0]}, place); + BboxOverlaps2(inside_anchor, gt_boxes_slice, &anchor_by_gt_overlap); + auto loc_score_tgtlbl_gt = SampleRRpnFgBgGt(dev_ctx, + anchor_by_gt_overlap, + rpn_batch_size_per_im, + rpn_positive_overlap, + rpn_negative_overlap, + rpn_fg_fraction, + engine, + use_random); + + Tensor sampled_loc_index = loc_score_tgtlbl_gt[0]; + Tensor sampled_score_index = loc_score_tgtlbl_gt[1]; + Tensor sampled_tgtlbl = loc_score_tgtlbl_gt[2]; + Tensor sampled_gt_index = loc_score_tgtlbl_gt[3]; + + int loc_num = sampled_loc_index.dims()[0]; + int score_num = sampled_score_index.dims()[0]; + // unmap to all anchor + Tensor sampled_loc_index_unmap, sampled_score_index_unmap; + sampled_loc_index_unmap.mutable_data({loc_num}, place); + sampled_score_index_unmap.mutable_data({score_num}, place); + Gather(inds_inside.data(), + 1, + sampled_loc_index.data(), + loc_num, + sampled_loc_index_unmap.data()); + Gather(inds_inside.data(), + 1, + sampled_score_index.data(), + score_num, + sampled_score_index_unmap.data()); + + // get target bbox deltas + Tensor sampled_anchor, sampled_gt, sampled_tgt_bbox; + auto* sampled_anchor_data = + sampled_anchor.mutable_data({loc_num, 5}, place); + auto* sampled_gt_data = sampled_gt.mutable_data({loc_num, 5}, place); + Gather(anchor->data(), + 5, + sampled_loc_index_unmap.data(), + loc_num, + sampled_anchor_data); + Gather(gt_boxes_slice.data(), + 5, + sampled_gt_index.data(), + loc_num, + sampled_gt_data); + sampled_tgt_bbox.mutable_data({loc_num, 5}, place); + BoxToDelta2( + loc_num, sampled_anchor, sampled_gt, nullptr, &sampled_tgt_bbox); + std::ofstream file_anchor; + // Add anchor offset + int anchor_offset = i * anchor_num; + auto sampled_loc_index_unmap_et = + framework::EigenTensor::From(sampled_loc_index_unmap); + sampled_loc_index_unmap_et = sampled_loc_index_unmap_et + anchor_offset; + auto sampled_score_index_unmap_et = + framework::EigenTensor::From(sampled_score_index_unmap); + sampled_score_index_unmap_et = + sampled_score_index_unmap_et + anchor_offset; + AppendRpns(loc_index, total_loc_num, &sampled_loc_index_unmap); + AppendRpns(score_index, total_score_num, &sampled_score_index_unmap); + AppendRpns(tgt_bbox, total_loc_num * 5, &sampled_tgt_bbox); + AppendRpns(tgt_lbl, total_score_num, &sampled_tgtlbl); + total_loc_num += loc_num; + total_score_num += score_num; + lod0_loc.emplace_back(total_loc_num); + lod0_score.emplace_back(total_score_num); + } + + PADDLE_ENFORCE_LE(total_loc_num, max_num); + PADDLE_ENFORCE_LE(total_score_num, max_num); + + lod_loc.emplace_back(lod0_loc); + loc_score.emplace_back(lod0_score); + loc_index->set_lod(lod_loc); + score_index->set_lod(loc_score); + tgt_bbox->set_lod(lod_loc); + tgt_lbl->set_lod(loc_score); + loc_index->Resize({total_loc_num}); + score_index->Resize({total_score_num}); + tgt_bbox->Resize({total_loc_num, 5}); + tgt_lbl->Resize({total_score_num, 1}); + } +}; + +class RRpnTargetAssignOpMaker : public framework::OpProtoAndCheckerMaker { +public: + void Make() override { + AddInput("Anchor", + "(Tensor) input anchor is a 2-D Tensor with shape [H*W*A, 5]."); + AddInput("GtBoxes", + "(LoDTensor) input ground-truth bbox with shape [K, 5]."); + AddInput("ImInfo", + "(LoDTensor) input image information with shape [N, 3]. " + "N is the batch size, each image information includes height, " + "width and scale."); + AddAttr("rpn_batch_size_per_im", + "Total number of RPN examples per image.") + .SetDefault(256); + AddAttr( + "rpn_straddle_thresh", + "Remove RPN anchors that go outside the image by straddle_thresh " + "pixels, " + "Set to -1 or a large value, e.g. 100000, to disable pruning anchors."); + AddAttr( + "rpn_positive_overlap", + "Minimum overlap required between an anchor and ground-truth " + "box for the (anchor, gt box) pair to be a positive example.") + .SetDefault(0.7); + AddAttr( + "rpn_negative_overlap", + "Maximum overlap allowed between an anchor and ground-truth " + "box for the (anchor, gt box) pair to be a negative examples.") + .SetDefault(0.3); + AddAttr( + "rpn_fg_fraction", + "Target fraction of RoI minibatch that " + "is labeled foreground (i.e. class > 0), 0-th class is background.") + .SetDefault(0.25); + AddAttr("use_random", + "A flag indicating whether to use a ReservoirSampling. " + "NOTE: DO NOT set this flag to false in training. " + "Setting this flag to false is only useful in unittest.") + .SetDefault(true); + AddOutput( + "LocationIndex", + "(Tensor), The indexes of foreground anchors in all RPN anchors, the " + "shape of the LocationIndex is [F], F depends on the value of input " + "tensor and attributes."); + AddOutput( + "ScoreIndex", + "(Tensor), The indexes of foreground and background anchors in all " + "RPN anchors(The rest anchors are ignored). The shape of the " + "ScoreIndex is [F + B], F and B are sampled foreground and background " + " number."); + AddOutput("TargetBBox", + "(Tensor), The target bbox deltas with shape " + "[F, 5], F is the sampled foreground number."); + AddOutput( + "TargetLabel", + "(Tensor), The target labels of each anchor with shape " + "[F + B, 1], F and B are sampled foreground and background number."); + AddComment(R"DOC( +This operator can be, for a given set of ground truth bboxes and the +anchors, to assign classification and regression targets to each prediction. +The ScoreIndex and LocationIndex will be generated according to the anchor-groundtruth IOU. +The rest anchors would not contibute to the RPN training loss + +ScoreIndex is composed of foreground anchor indexes(positive labels) and +background anchor indexes(negative labels). LocationIndex is exactly same +as the foreground anchor indexes since we can not assign regression target to +the background anchors. + +The classification targets(TargetLabel) is a binary class label (of being +an object or not). Following the paper of Faster-RCNN, the positive labels +are two kinds of anchors: (i) the anchor/anchors with the highest IoU +overlap with a ground-truth box, or (ii) an anchor that has an IoU overlap +higher than rpn_positive_overlap(0.7) with any ground-truth box. Note that +a single ground-truth box may assign positive labels to multiple anchors. +A non-positive anchor is when its IoU ratio is lower than rpn_negative_overlap +(0.3) for all ground-truth boxes. Anchors that are neither positive nor +negative do not contribute to the training objective. + +)DOC"); + } +}; + +} // namespace operators +} // namespace paddle + +namespace ops = paddle::operators; + +REGISTER_OPERATOR( + rrpn_target_assign, + ops::RRpnTargetAssignOp, + ops::RRpnTargetAssignOpMaker, + paddle::framework::EmptyGradOpMaker, + paddle::framework::EmptyGradOpMaker); +REGISTER_OP_CPU_KERNEL(rrpn_target_assign, + ops::RRpnTargetAssignKernel, + ops::RRpnTargetAssignKernel); diff --git a/PaddleCV/rrpn/models/ext_op/src/safe_ref.h b/PaddleCV/rrpn/models/ext_op/src/safe_ref.h new file mode 100755 index 00000000..6a67b1a7 --- /dev/null +++ b/PaddleCV/rrpn/models/ext_op/src/safe_ref.h @@ -0,0 +1,35 @@ +/* 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. */ + +#pragma once +#include +#include "paddle/fluid/platform/enforce.h" + +namespace paddle { +namespace operators { +namespace detail { +/** + * Get Reference From Pointer with check. The error message is printf format, + * and passed by `args` + */ +template +inline T& Ref(T* ptr, ARGS&&... args) { + PADDLE_ENFORCE_NOT_NULL(ptr, ::paddle::string::Sprintf(args...)); + return *ptr; +} + + +} // namespace detail +} // namespace operators +} // namespace paddle diff --git a/PaddleCV/rrpn/models/model_builder.py b/PaddleCV/rrpn/models/model_builder.py new file mode 100755 index 00000000..1f976fac --- /dev/null +++ b/PaddleCV/rrpn/models/model_builder.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 paddle.fluid as fluid +from paddle.fluid.param_attr import ParamAttr +from paddle.fluid.initializer import Constant +from paddle.fluid.initializer import Normal +from paddle.fluid.initializer import MSRA +from paddle.fluid.regularizer import L2Decay +from config import cfg +from models.ext_op.rrpn_lib import * + + +class RRPN(object): + def __init__(self, + add_conv_body_func=None, + add_roi_box_head_func=None, + mode='train', + use_pyreader=True, + use_random=True): + self.add_conv_body_func = add_conv_body_func + self.add_roi_box_head_func = add_roi_box_head_func + self.mode = mode + self.use_pyreader = use_pyreader + self.use_random = use_random + + def build_model(self, image_shape): + self.build_input(image_shape) + body_conv = self.add_conv_body_func(self.image) + # RPN + self.rpn_heads(body_conv) + # Fast RCNN + self.fast_rcnn_heads(body_conv) + if self.mode != 'train': + self.eval_bbox() + + def loss(self): + losses = [] + # Fast RCNN loss + loss_cls, loss_bbox = self.fast_rcnn_loss() + # RPN loss + rpn_cls_loss, rpn_reg_loss = self.rpn_loss() + losses = [loss_cls, loss_bbox, rpn_cls_loss, rpn_reg_loss] + rkeys = ['loss', 'loss_cls', 'loss_bbox', \ + 'loss_rpn_cls', 'loss_rpn_bbox',] + loss = fluid.layers.sum(losses) + rloss = [loss] + losses + return rloss, rkeys, self.rpn_rois + + def eval_bbox_out(self): + return self.pred_result + + def build_input(self, image_shape): + if self.use_pyreader: + in_shapes = [[-1] + image_shape, [-1, 5], [-1, 1], [-1, 1], + [-1, 3], [-1, 1]] + lod_levels = [0, 1, 1, 1, 0, 0] + dtypes = [ + 'float32', 'float32', 'int32', 'int32', 'float32', 'int64' + ] + self.py_reader = fluid.layers.py_reader( + capacity=64, + shapes=in_shapes, + lod_levels=lod_levels, + dtypes=dtypes, + use_double_buffer=True) + ins = fluid.layers.read_file(self.py_reader) + self.image = ins[0] + self.gt_box = ins[1] + self.gt_label = ins[2] + self.is_crowd = ins[3] + self.im_info = ins[4] + self.im_id = ins[5] + else: + self.image = fluid.layers.data( + name='image', shape=image_shape, dtype='float32') + self.gt_box = fluid.layers.data( + name='gt_box', shape=[4], dtype='float32', lod_level=1) + self.gt_label = fluid.layers.data( + name='gt_label', shape=[1], dtype='int32', lod_level=1) + self.is_crowd = fluid.layers.data( + name='is_crowd', shape=[1], dtype='int32', lod_level=1) + self.im_info = fluid.layers.data( + name='im_info', shape=[3], dtype='float32') + self.im_id = fluid.layers.data( + name='im_id', shape=[1], dtype='int64') + + self.difficult = fluid.layers.data( + name='difficult', shape=[1], dtype='float32', lod_level=1) + + def feeds(self): + if self.mode == 'infer': + return [self.image, self.im_info] + if self.mode == 'val': + return [ + self.image, self.gt_box, self.gt_label, self.is_crowd, + self.im_info, self.im_id, self.difficult + ] + return [ + self.image, self.gt_box, self.gt_label, self.is_crowd, self.im_info, + self.im_id + ] + + def eval_bbox(self): + self.im_scale = fluid.layers.slice( + self.im_info, [1], starts=[2], ends=[3]) + im_scale_lod = fluid.layers.sequence_expand(self.im_scale, + self.rpn_rois) + results = [] + boxes = self.rpn_rois + cls_prob = fluid.layers.softmax(self.cls_score, use_cudnn=False) + bbox_pred = fluid.layers.reshape(self.bbox_pred, (-1, cfg.class_num, 5)) + for i in range(cfg.class_num - 1): + bbox_pred_slice = fluid.layers.slice( + bbox_pred, axes=[1], starts=[i + 1], ends=[i + 2]) + bbox_pred_reshape = fluid.layers.reshape(bbox_pred_slice, (-1, 5)) + decoded_box = rrpn_box_coder(prior_box=boxes, \ + target_box=bbox_pred_reshape, \ + prior_box_var=cfg.bbox_reg_weights) + score_slice = fluid.layers.slice( + cls_prob, axes=[1], starts=[i + 1], ends=[i + 2]) + score_slice = fluid.layers.reshape(score_slice, shape=[-1, 1]) + box_positive = fluid.layers.reshape(decoded_box, shape=[-1, 8]) + box_reshape = fluid.layers.reshape(x=box_positive, shape=[1, -1, 8]) + score_reshape = fluid.layers.reshape( + x=score_slice, shape=[1, 1, -1]) + pred_result = fluid.layers.multiclass_nms( + bboxes=box_reshape, + scores=score_reshape, + score_threshold=cfg.TEST.score_thresh, + nms_top_k=-1, + nms_threshold=cfg.TEST.nms_thresh, + keep_top_k=cfg.TEST.detections_per_im, + normalized=False, + background_label=-1) + result_shape = fluid.layers.shape(pred_result) + res_dimension = fluid.layers.slice( + result_shape, axes=[0], starts=[1], ends=[2]) + res_dimension = fluid.layers.reshape(res_dimension, shape=[1, 1]) + dimension = fluid.layers.fill_constant( + shape=[1, 1], value=2, dtype='int32') + cond = fluid.layers.less_than(dimension, res_dimension) + res = fluid.layers.create_global_var( + shape=[1, 10], value=0.0, dtype='float32', persistable=False) + with fluid.layers.control_flow.Switch() as switch: + with switch.case(cond): + coordinate = fluid.layers.fill_constant( + shape=[9], value=0.0, dtype='float32') + pred_class = fluid.layers.fill_constant( + shape=[1], value=i + 1, dtype='float32') + add_class = fluid.layers.concat( + [pred_class, coordinate], axis=0) + normal_result = fluid.layers.elementwise_add(pred_result, + add_class) + fluid.layers.assign(normal_result, res) + with switch.default(): + normal_result = fluid.layers.fill_constant( + shape=[1, 10], value=-1.0, dtype='float32') + fluid.layers.assign(normal_result, res) + results.append(res) + if len(results) == 1: + self.pred_result = results[0] + return + outs = [] + out = fluid.layers.concat(results) + zero = fluid.layers.fill_constant( + shape=[1, 1], value=0.0, dtype='float32') + out_split, _ = fluid.layers.split(out, dim=1, num_or_sections=[1, 9]) + out_bool = fluid.layers.greater_than(out_split, zero) + idx = fluid.layers.where(out_bool) + idx_split, _ = fluid.layers.split(idx, dim=1, num_or_sections=[1, 1]) + idx = fluid.layers.reshape(idx_split, [-1, 1]) + self.pred_result = fluid.layers.gather(input=out, index=idx) + + def rpn_heads(self, rpn_input): + # RPN hidden representation + dim_out = rpn_input.shape[1] + rpn_conv = fluid.layers.conv2d( + input=rpn_input, + num_filters=dim_out, + filter_size=3, + stride=1, + padding=1, + act='relu', + name='conv_rpn', + param_attr=ParamAttr( + name="conv_rpn_w", initializer=Normal( + loc=0., scale=0.01)), + bias_attr=ParamAttr( + name="conv_rpn_b", learning_rate=2., regularizer=L2Decay(0.))) + self.anchor, self.var = rotated_anchor_generator( + input=rpn_conv, + anchor_sizes=cfg.anchor_sizes, + aspect_ratios=cfg.aspect_ratios, + angles=cfg.anchor_angle, + variance=cfg.variance, + stride=cfg.rpn_stride, + offset=0.5) + num_anchor = self.anchor.shape[2] + # Proposal classification scores + self.rpn_cls_score = fluid.layers.conv2d( + rpn_conv, + num_filters=num_anchor, + filter_size=1, + stride=1, + padding=0, + act=None, + name='rpn_cls_score', + param_attr=ParamAttr( + name="rpn_cls_logits_w", initializer=Normal( + loc=0., scale=0.01)), + bias_attr=ParamAttr( + name="rpn_cls_logits_b", + learning_rate=2., + regularizer=L2Decay(0.))) + # Proposal bbox regression deltas + self.rpn_bbox_pred = fluid.layers.conv2d( + rpn_conv, + num_filters=5 * num_anchor, + filter_size=1, + stride=1, + padding=0, + act=None, + name='rpn_bbox_pred', + param_attr=ParamAttr( + name="rpn_bbox_pred_w", initializer=Normal( + loc=0., scale=0.01)), + bias_attr=ParamAttr( + name="rpn_bbox_pred_b", + learning_rate=2., + regularizer=L2Decay(0.))) + rpn_cls_score_prob = fluid.layers.sigmoid( + self.rpn_cls_score, name='rpn_cls_score_prob') + + param_obj = cfg.TRAIN if self.mode == 'train' else cfg.TEST + pre_nms_top_n = param_obj.rpn_pre_nms_top_n + post_nms_top_n = param_obj.rpn_post_nms_top_n + nms_thresh = param_obj.rpn_nms_thresh + min_size = param_obj.rpn_min_size + self.rpn_rois, self.rpn_roi_probs = rotated_generate_proposals( + scores=rpn_cls_score_prob, + bbox_deltas=self.rpn_bbox_pred, + im_info=self.im_info, + anchors=self.anchor, + variances=self.var, + pre_nms_top_n=pre_nms_top_n, + post_nms_top_n=post_nms_top_n, + nms_thresh=param_obj.rpn_nms_thresh, + min_size=param_obj.rpn_min_size) + if self.mode == 'train': + outs = rotated_generate_proposal_labels( + rpn_rois=self.rpn_rois, + gt_classes=self.gt_label, + is_crowd=self.is_crowd, + gt_boxes=self.gt_box, + im_info=self.im_info, + batch_size_per_im=cfg.TRAIN.batch_size_per_im, + fg_fraction=cfg.TRAIN.fg_fractrion, + fg_thresh=cfg.TRAIN.fg_thresh, + bg_thresh_hi=cfg.TRAIN.bg_thresh_hi, + bg_thresh_lo=cfg.TRAIN.bg_thresh_lo, + bbox_reg_weights=cfg.bbox_reg_weights, + class_nums=cfg.class_num, + use_random=self.use_random) + + self.rois = outs[0] + self.labels_int32 = outs[1] + self.bbox_targets = outs[2] + self.bbox_inside_weights = outs[3] + self.bbox_outside_weights = outs[4] + + def fast_rcnn_heads(self, roi_input): + if self.mode == 'train': + pool_rois = self.rois + else: + pool_rois = self.rpn_rois + pool = rotated_roi_align( + input=roi_input, + rois=pool_rois, + pooled_height=cfg.roi_resolution, + pooled_width=cfg.roi_resolution, + spatial_scale=cfg.spatial_scale) + self.res5_2_sum = self.add_roi_box_head_func(pool) + rcnn_out = fluid.layers.pool2d( + self.res5_2_sum, pool_type='avg', pool_size=7, name='res5_pool') + self.cls_score = fluid.layers.fc(input=rcnn_out, + size=cfg.class_num, + act=None, + name='cls_score', + param_attr=ParamAttr( + name='cls_score_w', + initializer=Normal( + loc=0.0, scale=0.001)), + bias_attr=ParamAttr( + name='cls_score_b', + learning_rate=2., + regularizer=L2Decay(0.))) + self.bbox_pred = fluid.layers.fc(input=rcnn_out, + size=5 * cfg.class_num, + act=None, + name='bbox_pred', + param_attr=ParamAttr( + name='bbox_pred_w', + initializer=Normal( + loc=0.0, scale=0.01)), + bias_attr=ParamAttr( + name='bbox_pred_b', + learning_rate=2., + regularizer=L2Decay(0.))) + + def fast_rcnn_loss(self): + labels_int64 = fluid.layers.cast(x=self.labels_int32, dtype='int64') + labels_int64.stop_gradient = True + loss_cls = fluid.layers.softmax_with_cross_entropy( + logits=self.cls_score, + label=labels_int64, + numeric_stable_mode=True, ) + loss_cls = fluid.layers.reduce_mean(loss_cls) + loss_bbox = fluid.layers.smooth_l1( + x=self.bbox_pred, + y=self.bbox_targets, + inside_weight=self.bbox_inside_weights, + outside_weight=self.bbox_outside_weights, + sigma=1.0) + loss_bbox = fluid.layers.reduce_mean(loss_bbox) + return loss_cls, loss_bbox + + def rpn_loss(self): + rpn_cls_score_reshape = fluid.layers.transpose( + self.rpn_cls_score, perm=[0, 2, 3, 1]) + rpn_bbox_pred_reshape = fluid.layers.transpose( + self.rpn_bbox_pred, perm=[0, 2, 3, 1]) + + anchor_reshape = fluid.layers.reshape(self.anchor, shape=(-1, 5)) + var_reshape = fluid.layers.reshape(self.var, shape=(-1, 5)) + + rpn_cls_score_reshape = fluid.layers.reshape( + x=rpn_cls_score_reshape, shape=(0, -1, 1)) + rpn_bbox_pred_reshape = fluid.layers.reshape( + x=rpn_bbox_pred_reshape, shape=(0, -1, 5)) + score_pred, loc_pred, score_tgt, loc_tgt = \ + rrpn_target_assign( + bbox_pred=rpn_bbox_pred_reshape, + cls_logits=rpn_cls_score_reshape, + anchor_box=anchor_reshape, + gt_boxes=self.gt_box, + im_info=self.im_info, + rpn_batch_size_per_im=cfg.TRAIN.rpn_batch_size_per_im, + rpn_straddle_thresh=-1, + rpn_fg_fraction=cfg.TRAIN.rpn_fg_fraction, + rpn_positive_overlap=cfg.TRAIN.rpn_positive_overlap, + rpn_negative_overlap=cfg.TRAIN.rpn_negative_overlap, + use_random=self.use_random) + score_tgt = fluid.layers.cast(x=score_tgt, dtype='float32') + rpn_cls_loss = fluid.layers.sigmoid_cross_entropy_with_logits( + x=score_pred, label=score_tgt) + rpn_cls_loss = fluid.layers.reduce_mean( + rpn_cls_loss, name='loss_rpn_cls') + + rpn_reg_loss = fluid.layers.smooth_l1(x=loc_pred, y=loc_tgt, sigma=3.0) + rpn_reg_loss = fluid.layers.reduce_sum( + rpn_reg_loss, name='loss_rpn_bbox') + score_shape = fluid.layers.shape(score_tgt) + score_shape = fluid.layers.cast(x=score_shape, dtype='float32') + norm = fluid.layers.reduce_prod(score_shape) + norm.stop_gradient = True + rpn_reg_loss = rpn_reg_loss / norm + return rpn_cls_loss, rpn_reg_loss diff --git a/PaddleCV/rrpn/models/name_adapter.py b/PaddleCV/rrpn/models/name_adapter.py new file mode 100644 index 00000000..aab88b55 --- /dev/null +++ b/PaddleCV/rrpn/models/name_adapter.py @@ -0,0 +1,71 @@ +# 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. + + +class NameAdapter(object): + """Fix the backbones variable names for pretrained weight""" + + def __init__(self, model): + super(NameAdapter, self).__init__() + self.model = model + + @property + def model_type(self): + return getattr(self.model, '_model_type', '') + + @property + def variant(self): + return getattr(self.model, 'variant', '') + + def fix_conv_norm_name(self, name): + if name == "conv1": + bn_name = "bn_" + name + else: + bn_name = "bn" + name[3:] + # the naming rule is same as pretrained weight + if self.model_type == 'SEResNeXt': + bn_name = name + "_bn" + return bn_name + + def fix_shortcut_name(self, name): + if self.model_type == 'SEResNeXt': + name = 'conv' + name + '_prj' + return name + + def fix_bottleneck_name(self, name): + if self.model_type == 'SEResNeXt': + conv_name1 = 'conv' + name + '_x1' + conv_name2 = 'conv' + name + '_x2' + conv_name3 = 'conv' + name + '_x3' + shortcut_name = name + else: + conv_name1 = name + "_branch2a" + conv_name2 = name + "_branch2b" + conv_name3 = name + "_branch2c" + shortcut_name = name + "_branch1" + return conv_name1, conv_name2, conv_name3, shortcut_name + + def fix_layer_warp_name(self, stage_num, count, i): + name = 'res' + str(stage_num) + if count > 10 and stage_num == 4: + if i == 0: + conv_name = name + "a" + else: + conv_name = name + "b" + str(i) + else: + conv_name = name + chr(ord("a") + i) + return conv_name + + def fix_c1_stage_name(self): + return "conv1" diff --git a/PaddleCV/rrpn/models/resnet.py b/PaddleCV/rrpn/models/resnet.py new file mode 100644 index 00000000..d1505fbe --- /dev/null +++ b/PaddleCV/rrpn/models/resnet.py @@ -0,0 +1,358 @@ +# 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 collections import OrderedDict + +from paddle import fluid +from paddle.fluid.param_attr import ParamAttr +from paddle.fluid.framework import Variable +from paddle.fluid.regularizer import L2Decay +from paddle.fluid.initializer import Constant + +from numbers import Integral +from .name_adapter import NameAdapter + + +class ResNet(object): + """ + Residual Network, see https://arxiv.org/abs/1512.03385 + Args: + depth (int): ResNet depth, should be 18, 34, 50, 101, 152. + freeze_at (int): freeze the backbone at which stage + norm_type (str): normalization type, 'bn'/'sync_bn'/'affine_channel' + freeze_norm (bool): freeze normalization layers + norm_decay (float): weight decay for normalization layer weights + variant (str): ResNet variant, supports 'a', 'b', 'c', 'd' currently + feature_maps (list): index of stages whose feature maps are returned + """ + __shared__ = ['norm_type', 'freeze_norm', 'weight_prefix_name'] + + def __init__(self, + depth=50, + freeze_at=2, + norm_type='affine_channel', + freeze_norm=True, + norm_decay=0., + variant='b', + feature_maps=4, + weight_prefix_name=''): + super(ResNet, self).__init__() + + if isinstance(feature_maps, Integral): + feature_maps = [feature_maps] + + assert depth in [18, 34, 50, 101, 152], \ + "depth {} not in [18, 34, 50, 101, 152]" + assert variant in ['a', 'b', 'c', 'd'], "invalid ResNet variant" + assert 0 <= freeze_at <= 4, "freeze_at should be 0, 1, 2, 3 or 4" + assert len(feature_maps) > 0, "need one or more feature maps" + assert norm_type in ['bn', 'sync_bn', 'affine_channel'] + + self.depth = depth + self.freeze_at = freeze_at + self.norm_type = norm_type + self.norm_decay = norm_decay + self.freeze_norm = freeze_norm + self.variant = variant + self._model_type = 'ResNet' + self.feature_maps = feature_maps + self.depth_cfg = { + 18: ([2, 2, 2, 2], self.basicblock), + 34: ([3, 4, 6, 3], self.basicblock), + 50: ([3, 4, 6, 3], self.bottleneck), + 101: ([3, 4, 23, 3], self.bottleneck), + 152: ([3, 8, 36, 3], self.bottleneck) + } + self.stage_filters = [64, 128, 256, 512] + self._c1_out_chan_num = 64 + self.na = NameAdapter(self) + self.prefix_name = weight_prefix_name + + def _conv_offset(self, + input, + filter_size, + stride, + padding, + act=None, + name=None): + out_channel = filter_size * filter_size * 3 + out = fluid.layers.conv2d( + input, + num_filters=out_channel, + filter_size=filter_size, + stride=stride, + padding=padding, + param_attr=ParamAttr( + initializer=Constant(0.0), name=name + ".w_0"), + bias_attr=ParamAttr( + initializer=Constant(0.0), name=name + ".b_0"), + act=act, + name=name) + return out + + def _conv_norm(self, + input, + num_filters, + filter_size, + stride=1, + groups=1, + act=None, + name=None): + _name = self.prefix_name + name if self.prefix_name != '' else name + conv = fluid.layers.conv2d( + input=input, + num_filters=num_filters, + filter_size=filter_size, + stride=stride, + padding=(filter_size - 1) // 2, + groups=groups, + act=None, + param_attr=ParamAttr(name=_name + "_weights"), + bias_attr=False, + name=_name + '.conv2d.output.1') + + bn_name = self.na.fix_conv_norm_name(name) + bn_name = self.prefix_name + bn_name if self.prefix_name != '' else bn_name + + norm_lr = 0. if self.freeze_norm else 1. + norm_decay = self.norm_decay + pattr = ParamAttr( + name=bn_name + '_scale', + learning_rate=norm_lr, + regularizer=L2Decay(norm_decay)) + battr = ParamAttr( + name=bn_name + '_offset', + learning_rate=norm_lr, + regularizer=L2Decay(norm_decay)) + + if self.norm_type in ['bn', 'sync_bn']: + global_stats = True if self.freeze_norm else False + out = fluid.layers.batch_norm( + input=conv, + act=act, + name=bn_name + '.output.1', + param_attr=pattr, + bias_attr=battr, + moving_mean_name=bn_name + '_mean', + moving_variance_name=bn_name + '_variance', + use_global_stats=global_stats) + scale = fluid.framework._get_var(pattr.name) + bias = fluid.framework._get_var(battr.name) + elif self.norm_type == 'affine_channel': + scale = fluid.layers.create_parameter( + shape=[conv.shape[1]], + dtype=conv.dtype, + attr=pattr, + default_initializer=fluid.initializer.Constant(1.)) + bias = fluid.layers.create_parameter( + shape=[conv.shape[1]], + dtype=conv.dtype, + attr=battr, + default_initializer=fluid.initializer.Constant(0.)) + out = fluid.layers.affine_channel( + x=conv, scale=scale, bias=bias, act=act) + if self.freeze_norm: + scale.stop_gradient = True + bias.stop_gradient = True + return out + + def _shortcut(self, input, ch_out, stride, is_first, name): + max_pooling_in_short_cut = self.variant == 'd' + ch_in = input.shape[1] + # the naming rule is same as pretrained weight + name = self.na.fix_shortcut_name(name) + std_senet = getattr(self, 'std_senet', False) + if ch_in != ch_out or stride != 1 or (self.depth < 50 and is_first): + if std_senet: + if is_first: + return self._conv_norm(input, ch_out, 1, stride, name=name) + else: + return self._conv_norm(input, ch_out, 3, stride, name=name) + if max_pooling_in_short_cut and not is_first: + input = fluid.layers.pool2d( + input=input, + pool_size=2, + pool_stride=2, + pool_padding=0, + ceil_mode=True, + pool_type='avg') + return self._conv_norm(input, ch_out, 1, 1, name=name) + return self._conv_norm(input, ch_out, 1, stride, name=name) + else: + return input + + def bottleneck(self, input, num_filters, stride, is_first, name): + if self.variant == 'a': + stride1, stride2 = stride, 1 + else: + stride1, stride2 = 1, stride + + # ResNeXt + groups = getattr(self, 'groups', 1) + group_width = getattr(self, 'group_width', -1) + if groups == 1: + expand = 4 + elif (groups * group_width) == 256: + expand = 1 + else: # FIXME hard code for now, handles 32x4d, 64x4d and 32x8d + num_filters = num_filters // 2 + expand = 2 + + conv_name1, conv_name2, conv_name3, \ + shortcut_name = self.na.fix_bottleneck_name(name) + std_senet = getattr(self, 'std_senet', False) + conv_def = [[num_filters, 1, stride1, 'relu', 1, conv_name1], + [num_filters, 3, stride2, 'relu', groups, conv_name2], + [num_filters * expand, 1, 1, None, 1, conv_name3]] + + residual = input + for i, (c, k, s, act, g, _name) in enumerate(conv_def): + residual = self._conv_norm( + input=residual, + num_filters=c, + filter_size=k, + stride=s, + act=act, + groups=g, + name=_name) + short = self._shortcut( + input, + num_filters * expand, + stride, + is_first=is_first, + name=shortcut_name) + return fluid.layers.elementwise_add( + x=short, y=residual, act='relu', name=name + ".add.output.5") + + def basicblock(self, input, num_filters, stride, is_first, name): #, + conv0 = self._conv_norm( + input=input, + num_filters=num_filters, + filter_size=3, + act='relu', + stride=stride, + name=name + "_branch2a") + conv1 = self._conv_norm( + input=conv0, + num_filters=num_filters, + filter_size=3, + act=None, + name=name + "_branch2b") + short = self._shortcut( + input, num_filters, stride, is_first, name=name + "_branch1") + return fluid.layers.elementwise_add(x=short, y=conv1, act='relu') + + def layer_warp(self, input, stage_num): + """ + Args: + input (Variable): input variable. + stage_num (int): the stage number, should be 2, 3, 4, 5 + Returns: + The last variable in endpoint-th stage. + """ + assert stage_num in [2, 3, 4, 5] + + stages, block_func = self.depth_cfg[self.depth] + count = stages[stage_num - 2] + + ch_out = self.stage_filters[stage_num - 2] + is_first = False if stage_num != 2 else True + # Make the layer name and parameter name consistent + # with ImageNet pre-trained model + conv = input + for i in range(count): + conv_name = self.na.fix_layer_warp_name(stage_num, count, i) + if self.depth < 50: + is_first = True if i == 0 and stage_num == 2 else False + conv = block_func( + input=conv, + num_filters=ch_out, + stride=2 if i == 0 and stage_num != 2 else 1, + is_first=is_first, + name=conv_name) + return conv + + def c1_stage(self, input): + out_chan = self._c1_out_chan_num + + conv1_name = self.na.fix_c1_stage_name() + + if self.variant in ['c', 'd']: + conv_def = [ + [out_chan // 2, 3, 2, "conv1_1"], + [out_chan // 2, 3, 1, "conv1_2"], + [out_chan, 3, 1, "conv1_3"], + ] + else: + conv_def = [[out_chan, 7, 2, conv1_name]] + + for (c, k, s, _name) in conv_def: + input = self._conv_norm( + input=input, + num_filters=c, + filter_size=k, + stride=s, + act='relu', + name=_name) + + output = fluid.layers.pool2d( + input=input, + pool_size=3, + pool_stride=2, + pool_padding=1, + pool_type='max') + return output + + def __call__(self, input): + assert isinstance(input, Variable) + assert not (set(self.feature_maps) - set([2, 3, 4, 5])), \ + "feature maps {} not in [2, 3, 4, 5]".format(self.feature_maps) + + res_endpoints = [] + + res = input + feature_maps = self.feature_maps + severed_head = getattr(self, 'severed_head', False) + if not severed_head: + res = self.c1_stage(res) + feature_maps = range(2, max(self.feature_maps) + 1) + + for i in feature_maps: + res = self.layer_warp(res, i) + if i in self.feature_maps: + res_endpoints.append(res) + if self.freeze_at >= i: + res.stop_gradient = True + return res + + +class ResNetC5(ResNet): + __doc__ = ResNet.__doc__ + + def __init__(self, + depth=50, + freeze_at=2, + norm_type='affine_channel', + freeze_norm=True, + norm_decay=0., + variant='b', + feature_maps=[5], + weight_prefix_name=''): + super(ResNetC5, self).__init__(depth, freeze_at, norm_type, freeze_norm, + norm_decay, variant, feature_maps) + self.severed_head = True diff --git a/PaddleCV/rrpn/pretrained/download.sh b/PaddleCV/rrpn/pretrained/download.sh new file mode 100755 index 00000000..7999b199 --- /dev/null +++ b/PaddleCV/rrpn/pretrained/download.sh @@ -0,0 +1,5 @@ +# Download the data. +echo "Downloading..." +wget https://paddle-imagenet-models-name.bj.bcebos.com/ResNet50_cos_pretrained.tar --no-check-certificate +echo "Extracting..." +tar -xf ResNet50_cos_pretrained.tar diff --git a/PaddleCV/rrpn/reader.py b/PaddleCV/rrpn/reader.py new file mode 100755 index 00000000..b54561b8 --- /dev/null +++ b/PaddleCV/rrpn/reader.py @@ -0,0 +1,180 @@ +# 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 random +import numpy as np +import xml.etree.ElementTree +import os +import time +import copy +import six +import cv2 +import math +import paddle +from collections import deque + +import data_utils +from roidbs import ICDAR2015Dataset, ICDAR2017Dataset +from config import cfg +from PIL import Image +from data_utils import _resize +num_trainers = int(os.environ.get('PADDLE_TRAINERS_NUM', 1)) +np.random.seed(10) + + +def roidb_reader(roidb, mode): + im, im_scales, gt_boxes, gt_classes = data_utils.get_image_blob(roidb, mode) + im_id = roidb['im_id'] + is_crowd = roidb['is_crowd'] + im_height = np.round(roidb['height'] * im_scales) + im_width = np.round(roidb['width'] * im_scales) + is_difficult = roidb['is_difficult'] + im_info = np.array([im_height, im_width, im_scales], dtype=np.float32) + if mode == 'val': + return im, gt_boxes, gt_classes, is_crowd, im_info, im_id, is_difficult + + outs = (im, gt_boxes, gt_classes, is_crowd, im_info, im_id) + + return outs + + +def RRPNData(mode, + batch_size=None, + total_batch_size=None, + padding_total=False, + shuffle=False, + shuffle_seed=None): #, + #roidbs=None): + total_batch_size = total_batch_size if total_batch_size else batch_size + assert total_batch_size % batch_size == 0 + if cfg.dataset == "icdar2015": + icdar2015_dataset = ICDAR2015Dataset(mode) + roidbs = icdar2015_dataset.get_roidb() + else: + icdar2017_dataset = ICDAR2017Dataset(mode) + roidbs = icdar2017_dataset.get_roidb() + + print("{} on {} with {} roidbs".format(mode, cfg.dataset, len(roidbs))) + + def reader(): + if mode == "train": + if shuffle: + if shuffle_seed is not None: + np.random.seed(shuffle_seed) + roidb_perm = deque(np.random.permutation(roidbs)) + else: + roidb_perm = deque(roidbs) + roidb_cur = 0 + count = 0 + batch_out = [] + device_num = total_batch_size / batch_size + while True: + start = time.time() + roidb = roidb_perm[0] + roidb_cur += 1 + roidb_perm.rotate(-1) + if roidb_cur >= len(roidbs): + if shuffle: + roidb_perm = deque(np.random.permutation(roidbs)) + else: + roidb_perm = deque(roidbs) + roidb_cur = 0 + # im, gt_boxes, gt_classes, is_crowd, im_info, im_id, gt_masks + datas = roidb_reader(roidb, mode) + if datas[1].shape[0] == 0: + continue + batch_out.append(datas) + end = time.time() + #print('reader time:', end - start) + if len(batch_out) == batch_size: + yield batch_out + count += 1 + batch_out = [] + iter_id = count // device_num + if iter_id >= cfg.max_iter * num_trainers: + return + elif mode == "val": + batch_out = [] + for roidb in roidbs: + im, gt_boxes, gt_classes, is_crowd, im_info, im_id, is_difficult = roidb_reader( + roidb, mode) + batch_out.append((im, gt_boxes, gt_classes, is_crowd, im_info, + im_id, is_difficult)) + if len(batch_out) == batch_size: + yield batch_out + batch_out = [] + if len(batch_out) != 0: + yield batch_out + + return reader + + +def train(batch_size, + total_batch_size=None, + padding_total=False, + num_workers=20, + shuffle=True, + shuffle_seed=None): + return RRPNData( + 'train', + batch_size, + total_batch_size, + padding_total, + shuffle=shuffle, + shuffle_seed=shuffle_seed) + + +def test(batch_size, total_batch_size=None, padding_total=False): + return RRPNData('val', batch_size, total_batch_size, shuffle=False) + + +def infer(file_path): + def reader(): + imgs = os.listdir(file_path) + imgs.sort() + for image in imgs: + if not os.path.exists(file_path): + raise ValueError("Image path [%s] does not exist." % + (file_path)) + with open(os.path.join(file_path, 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) + img, im_scale = _resize(img, target_size=1000, max_size=1778) + 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)) + h = img.shape[1] + w = img.shape[2] + im_info = np.array([h, w, im_scale], dtype=np.float32) + yield [(img, im_info)] + + return reader + + +if __name__ == '__main__': + from utility import parse_args + args = parse_args() + train_reader = train(1, shuffle=True) + import time + time0 = time.time() + for iter_id, data in enumerate(train_reader()): + print('iter:', iter_id) + print('cost:', time.time() - time0) + time0 = time.time() diff --git a/PaddleCV/rrpn/roidbs.py b/PaddleCV/rrpn/roidbs.py new file mode 100755 index 00000000..705244ac --- /dev/null +++ b/PaddleCV/rrpn/roidbs.py @@ -0,0 +1,364 @@ +# 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 copy +import logging +import numpy as np +import os +import scipy.sparse +import random +import time +import matplotlib +import cv2 +#import segm_utils +from config import cfg +from data_utils import DatasetPath +logger = logging.getLogger(__name__) + + +class ICDAR2015Dataset(object): + """A class representing a ICDAR2015 dataset.""" + + def __init__(self, mode): + print('Creating: {}'.format(cfg.dataset)) + self.name = cfg.data_dir + self.mode = mode + data_path = DatasetPath(mode, self.name) + data_dir = data_path.get_data_dir() + file_list = data_path.get_file_list() + self.image_dir = data_dir + self.gt_dir = file_list + + def get_roidb(self): + """Return an roidb corresponding to the txt dataset. Optionally: + - include ground truth boxes in the roidb + """ + image_list = os.listdir(self.image_dir) + image_list.sort() + im_infos = [] + count = 0 + for image in image_list: + prefix = image[:-4] + if image.split('.')[-1] != 'jpg': + continue + img_name = os.path.join(self.image_dir, image) + gt_name = os.path.join(self.gt_dir, 'gt_' + prefix + '.txt') + easy_boxes = [] + hard_boxes = [] + boxes = [] + gt_obj = open(gt_name, 'r', encoding='UTF-8-sig') + gt_txt = gt_obj.read() + gt_split = gt_txt.split('\n') + img = cv2.imread(img_name) + f = False + for gt_line in gt_split: + gt_ind = gt_line.split(',') + + # can get the text information + if len(gt_ind) > 3 and '###' not in gt_ind[8]: + pt1 = (int(gt_ind[0]), int(gt_ind[1])) + pt2 = (int(gt_ind[2]), int(gt_ind[3])) + pt3 = (int(gt_ind[4]), int(gt_ind[5])) + pt4 = (int(gt_ind[6]), int(gt_ind[7])) + edge1 = np.sqrt((pt1[0] - pt2[0]) * (pt1[0] - pt2[0]) + ( + pt1[1] - pt2[1]) * (pt1[1] - pt2[1])) + edge2 = np.sqrt((pt2[0] - pt3[0]) * (pt2[0] - pt3[0]) + ( + pt2[1] - pt3[1]) * (pt2[1] - pt3[1])) + angle = 0 + if edge1 > edge2: + width = edge1 + height = edge2 + if pt1[0] - pt2[0] != 0: + angle = -np.arctan( + float(pt1[1] - pt2[1]) / + float(pt1[0] - pt2[0])) / np.pi * 180 + else: + angle = 90.0 + elif edge2 >= edge1: + width = edge2 + height = edge1 + # print pt2[0], pt3[0] + if pt2[0] - pt3[0] != 0: + angle = -np.arctan( + float(pt2[1] - pt3[1]) / + float(pt2[0] - pt3[0])) / np.pi * 180 + else: + angle = 90.0 + if angle < -45.0: + angle = angle + 180 + x_ctr = float(pt1[0] + pt3[ + 0]) / 2 # pt1[0] + np.abs(float(pt1[0] - pt3[0])) / 2 + y_ctr = float(pt1[1] + pt3[ + 1]) / 2 # pt1[1] + np.abs(float(pt1[1] - pt3[1])) / 2 + if self.mode == 'val': + easy_boxes.append( + list(np.array([pt1, pt2, pt3, pt4]).reshape(8))) + else: + easy_boxes.append([x_ctr, y_ctr, width, height, angle]) + # can‘t get the text information + if len(gt_ind) > 3 and '###' in gt_ind[8]: + pt1 = (int(gt_ind[0]), int(gt_ind[1])) + pt2 = (int(gt_ind[2]), int(gt_ind[3])) + pt3 = (int(gt_ind[4]), int(gt_ind[5])) + pt4 = (int(gt_ind[6]), int(gt_ind[7])) + edge1 = np.sqrt((pt1[0] - pt2[0]) * (pt1[0] - pt2[0]) + ( + pt1[1] - pt2[1]) * (pt1[1] - pt2[1])) + edge2 = np.sqrt((pt2[0] - pt3[0]) * (pt2[0] - pt3[0]) + ( + pt2[1] - pt3[1]) * (pt2[1] - pt3[1])) + angle = 0 + if edge1 > edge2: + width = edge1 + height = edge2 + if pt1[0] - pt2[0] != 0: + angle = -np.arctan( + float(pt1[1] - pt2[1]) / + float(pt1[0] - pt2[0])) / np.pi * 180 + else: + angle = 90.0 + elif edge2 >= edge1: + width = edge2 + height = edge1 + if pt2[0] - pt3[0] != 0: + angle = -np.arctan( + float(pt2[1] - pt3[1]) / + float(pt2[0] - pt3[0])) / np.pi * 180 + else: + angle = 90.0 + if angle < -45.0: + angle = angle + 180 + x_ctr = float(pt1[0] + pt3[ + 0]) / 2 # pt1[0] + np.abs(float(pt1[0] - pt3[0])) / 2 + y_ctr = float(pt1[1] + pt3[ + 1]) / 2 # pt1[1] + np.abs(float(pt1[1] - pt3[1])) / 2 + if self.mode == 'val': + hard_boxes.append( + list(np.array([pt1, pt2, pt3, pt4]).reshape(8))) + else: + hard_boxes.append([x_ctr, y_ctr, width, height, angle]) + + #print(easy_boxes) + if self.mode == 'train': + boxes.extend(easy_boxes) + # hard box only get 1/3 for train + boxes.extend(hard_boxes[0:int(len(hard_boxes) / 3)]) + is_difficult = [0] * len(easy_boxes) + is_difficult.extend([1] * int(len(hard_boxes) / 3)) + else: + boxes.extend(easy_boxes) + boxes.extend(hard_boxes) + is_difficult = [0] * len(easy_boxes) + is_difficult.extend([1] * int(len(hard_boxes))) + len_of_bboxes = len(boxes) + #is_difficult = [0] * len(easy_boxes) + #is_difficult.extend([1] * int(len(hard_boxes))) + is_difficult = np.array(is_difficult).reshape( + 1, len_of_bboxes).astype(np.int32) + if self.mode == 'train': + gt_boxes = np.zeros((len_of_bboxes, 5), dtype=np.int32) + else: + gt_boxes = np.zeros((len_of_bboxes, 8), dtype=np.int32) + gt_classes = np.zeros((len_of_bboxes), dtype=np.int32) + is_crowd = np.zeros((len_of_bboxes), dtype=np.int32) + for idx in range(len(boxes)): + if self.mode == 'train': + gt_boxes[idx, :] = [ + boxes[idx][0], boxes[idx][1], boxes[idx][2], + boxes[idx][3], boxes[idx][4] + ] + else: + gt_boxes[idx, :] = [ + boxes[idx][0], boxes[idx][1], boxes[idx][2], + boxes[idx][3], boxes[idx][4], boxes[idx][5], + boxes[idx][6], boxes[idx][7] + ] + gt_classes[idx] = 1 + if gt_boxes.shape[0] <= 0: + continue + gt_boxes = gt_boxes.astype(np.float64) + im_info = { + 'im_id': count, + 'gt_classes': gt_classes, + 'image': img_name, + 'boxes': gt_boxes, + 'height': img.shape[0], + 'width': img.shape[1], + 'is_crowd': is_crowd, + 'is_difficult': is_difficult + } + im_infos.append(im_info) + count += 1 + + return im_infos + + +class ICDAR2017Dataset(object): + """A class representing a ICDAR2017 dataset.""" + + def __init__(self, mode): + print('Creating: {}'.format(cfg.dataset)) + self.name = cfg.data_dir + #print('**************', self.name) + self.mode = mode + data_path = DatasetPath(mode, self.name) + data_dir = data_path.get_data_dir() + #print("&**************", data_dir) + file_list = data_path.get_file_list() + self.image_dir = data_dir + self.gt_dir = file_list + + def get_roidb(self): + """Return an roidb corresponding to the json dataset. Optionally: + - include ground truth boxes in the roidb + """ + image_list = os.listdir(self.image_dir) + image_list.sort() + im_infos = [] + count = 0 + class_idx = 1 + class_name = {} + post_fix = ['jpg', 'bmp', 'png'] + if self.mode == 'val': + labels_map = get_labels_maps() + for image in image_list: + prefix = image[:-4] + #print(image) + + if image.split('.')[-1] not in post_fix: + continue + img_name = os.path.join(self.image_dir, image) + gt_name = os.path.join(self.gt_dir, 'gt_' + prefix + '.txt') + gt_classes = [] + #boxes = [] + #hard_boxes = [] + boxes = [] + gt_obj = open(gt_name, 'r', encoding='UTF-8-sig') + gt_txt = gt_obj.read() + gt_split = gt_txt.split('\n') + img = cv2.imread(img_name) + f = False + for gt_line in gt_split: + gt_ind = gt_line.split(',') + # can get the text information + if len(gt_ind) > 3: + if self.mode == 'val': + gt_classes.append(labels_map[gt_ind[-1]]) + else: + if gt_ind[-1] not in class_name: + class_name[gt_ind[-1]] = class_idx + #gt_classes.append(class_idx) + class_idx += 1 + gt_classes.append(class_name[gt_ind[-1]]) + pt1 = (int(gt_ind[0]), int(gt_ind[1])) + pt2 = (int(gt_ind[2]), int(gt_ind[3])) + pt3 = (int(gt_ind[4]), int(gt_ind[5])) + pt4 = (int(gt_ind[6]), int(gt_ind[7])) + edge1 = np.sqrt((pt1[0] - pt2[0]) * (pt1[0] - pt2[0]) + ( + pt1[1] - pt2[1]) * (pt1[1] - pt2[1])) + edge2 = np.sqrt((pt2[0] - pt3[0]) * (pt2[0] - pt3[0]) + ( + pt2[1] - pt3[1]) * (pt2[1] - pt3[1])) + angle = 0 + if edge1 > edge2: + width = edge1 + height = edge2 + if pt1[0] - pt2[0] != 0: + angle = -np.arctan( + float(pt1[1] - pt2[1]) / + float(pt1[0] - pt2[0])) / np.pi * 180 + else: + angle = 90.0 + elif edge2 >= edge1: + width = edge2 + height = edge1 + # print pt2[0], pt3[0] + if pt2[0] - pt3[0] != 0: + angle = -np.arctan( + float(pt2[1] - pt3[1]) / + float(pt2[0] - pt3[0])) / np.pi * 180 + else: + angle = 90.0 + if angle < -45.0: + angle = angle + 180 + x_ctr = float(pt1[0] + pt3[ + 0]) / 2 # pt1[0] + np.abs(float(pt1[0] - pt3[0])) / 2 + y_ctr = float(pt1[1] + pt3[ + 1]) / 2 # pt1[1] + np.abs(float(pt1[1] - pt3[1])) / 2 + if self.mode == 'val': + boxes.append( + list(np.array([pt1, pt2, pt3, pt4]).reshape(8))) + else: + boxes.append([x_ctr, y_ctr, width, height, angle]) + len_of_bboxes = len(boxes) + #print(len_of_bboxes) + is_difficult = np.zeros((len_of_bboxes, 1), dtype=np.int32) + if self.mode == 'train': + gt_boxes = np.zeros((len_of_bboxes, 5), dtype=np.int32) + else: + gt_boxes = np.zeros((len_of_bboxes, 8), dtype=np.int32) + gt_classes = np.array(gt_classes).reshape(len_of_bboxes, 1) + is_crowd = np.zeros((len_of_bboxes), dtype=np.int32) + for idx in range(len(boxes)): + if self.mode == 'train': + gt_boxes[idx, :] = [ + boxes[idx][0], boxes[idx][1], boxes[idx][2], + boxes[idx][3], boxes[idx][4] + ] + else: + gt_boxes[idx, :] = [ + boxes[idx][0], boxes[idx][1], boxes[idx][2], + boxes[idx][3], boxes[idx][4], boxes[idx][5], + boxes[idx][6], boxes[idx][7] + ] + #gt_classes[idx] = 1 + if gt_boxes.shape[0] <= 0: + continue + gt_boxes = gt_boxes.astype(np.float64) + im_info = { + 'im_id': count, + 'gt_classes': gt_classes, + 'image': img_name, + 'boxes': gt_boxes, + 'height': img.shape[0], + 'width': img.shape[1], + 'is_crowd': is_crowd, + 'is_difficult': is_difficult + } + im_infos.append(im_info) + count += 1 + if self.mode == 'train': + with open(os.path.join(cfg.data_dir, 'label_list'), 'w') as g: + for k in class_name: + g.write(k + "\n") + return im_infos + + +def get_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[line.strip()] = idx + 1 + return labels_map diff --git a/PaddleCV/rrpn/train.py b/PaddleCV/rrpn/train.py new file mode 100755 index 00000000..11dafa99 --- /dev/null +++ b/PaddleCV/rrpn/train.py @@ -0,0 +1,224 @@ +# 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 +import os + + +def set_paddle_flags(flags): + for key, value in flags.items(): + if os.environ.get(key, None) is None: + os.environ[key] = str(value) + + +set_paddle_flags({ + 'FLAGS_conv_workspace_size_limit': 500, + 'FLAGS_eager_delete_tensor_gb': 0, # enable gc + 'FLAGS_memory_fraction_of_eager_deletion': 1, + 'FLAGS_fraction_of_gpu_memory_to_use': 0.98 +}) + +import sys +import numpy as np +import time +import shutil +import collections +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 parse_args, print_arguments, SmoothedValue, TrainingStats, now_time, check_gpu +num_trainers = int(os.environ.get('PADDLE_TRAINERS_NUM', 1)) + + +def get_device_num(): + # NOTE(zcd): for multi-processe training, each process use one GPU card. + if num_trainers > 1: + return 1 + return fluid.core.get_cuda_device_count() + + +def train(): + learning_rate = cfg.learning_rate + image_shape = [3, cfg.TRAIN.max_size, cfg.TRAIN.max_size] + + devices_num = get_device_num() + total_batch_size = devices_num * cfg.TRAIN.im_per_batch + + use_random = True + startup_prog = fluid.Program() + train_prog = fluid.Program() + with fluid.program_guard(train_prog, startup_prog): + with fluid.unique_name.guard(): + model = model_builder.RRPN( + add_conv_body_func=resnet.ResNet(), + add_roi_box_head_func=resnet.ResNetC5(), + use_pyreader=cfg.use_pyreader, + use_random=use_random) + model.build_model(image_shape) + losses, keys, rpn_rois = model.loss() + loss = losses[0] + fetch_list = losses + + boundaries = cfg.lr_steps + gamma = cfg.lr_gamma + step_num = len(cfg.lr_steps) + values = [learning_rate * (gamma**i) for i in range(step_num + 1)] + start_lr = learning_rate * cfg.start_factor + lr = fluid.layers.piecewise_decay(boundaries, values) + lr = fluid.layers.linear_lr_warmup(lr, cfg.warm_up_iter, start_lr, + learning_rate) + optimizer = fluid.optimizer.Momentum( + learning_rate=lr, + regularization=fluid.regularizer.L2Decay(cfg.weight_decay), + momentum=cfg.momentum) + optimizer.minimize(loss) + fetch_list = fetch_list + [lr] + + for var in fetch_list: + var.persistable = True + gpu_id = int(os.environ.get('FLAGS_selected_gpus', 0)) + place = fluid.CUDAPlace(gpu_id) if cfg.use_gpu else fluid.CPUPlace() + exe = fluid.Executor(place) + + build_strategy = fluid.BuildStrategy() + build_strategy.fuse_all_optimizer_ops = False + build_strategy.fuse_elewise_add_act_ops = True + exec_strategy = fluid.ExecutionStrategy() + exec_strategy.num_iteration_per_drop_scope = 1 + exe.run(startup_prog) + + if cfg.pretrained_model: + checkpoint.load_and_fusebn(exe, train_prog, cfg.pretrained_model) + compiled_train_prog = fluid.CompiledProgram(train_prog).with_data_parallel( + loss_name=loss.name, + build_strategy=build_strategy, + exec_strategy=exec_strategy) + + shuffle = True + shuffle_seed = None + if num_trainers > 1: + shuffle_seed = 1 + if cfg.use_pyreader: + train_reader = reader.train( + batch_size=cfg.TRAIN.im_per_batch, + total_batch_size=total_batch_size, + padding_total=cfg.TRAIN.padding_minibatch, + shuffle=shuffle, + shuffle_seed=shuffle_seed) + if num_trainers > 1: + assert shuffle_seed is not None, \ + "If num_trainers > 1, the shuffle_seed must be set, because " \ + "the order of batch data generated by reader " \ + "must be the same in the respective processes." + # NOTE: the order of batch data generated by batch_reader + # must be the same in the respective processes. + if num_trainers > 1: + train_reader = fluid.contrib.reader.distributed_batch_reader( + train_reader) + py_reader = model.py_reader + py_reader.decorate_paddle_reader(train_reader) + else: + if num_trainers > 1: shuffle = False + train_reader = reader.train( + batch_size=total_batch_size, shuffle=shuffle) + feeder = fluid.DataFeeder(place=place, feed_list=model.feeds()) + + def train_loop_pyreader(): + py_reader.start() + train_stats = TrainingStats(cfg.log_window, keys) + try: + start_time = time.time() + prev_start_time = start_time + for iter_id in range(cfg.max_iter): + prev_start_time = start_time + start_time = time.time() + outs = exe.run(compiled_train_prog, + fetch_list=[v.name for v in fetch_list]) + stats = {k: np.array(v).mean() for k, v in zip(keys, outs[:-1])} + train_stats.update(stats) + logs = train_stats.log() + if iter_id % 10 == 0: + strs = '{}, iter: {}, lr: {:.5f}, {}, time: {:.3f}'.format( + now_time(), iter_id, + np.mean(outs[-1]), logs, start_time - prev_start_time) + print(strs) + sys.stdout.flush() + if (iter_id) % cfg.TRAIN.snapshot_iter == 0 and iter_id != 0: + save_name = "{}".format(iter_id) + checkpoint.save(exe, train_prog, + os.path.join(cfg.model_save_dir, save_name)) + if (iter_id) == cfg.max_iter: + checkpoint.save( + exe, train_prog, + os.path.join(cfg.model_save_dir, "model_final")) + break + end_time = time.time() + total_time = end_time - start_time + last_loss = np.array(outs[0]).mean() + except (StopIteration, fluid.core.EOFException): + py_reader.reset() + + def train_loop(): + start_time = time.time() + prev_start_time = start_time + start = start_time + train_stats = TrainingStats(cfg.log_window, keys) + for iter_id, data in enumerate(train_reader()): + prev_start_time = start_time + start_time = time.time() + if data[0][1].shape[0] == 0: + continue + + outs = exe.run(compiled_train_prog, + fetch_list=[v.name for v in fetch_list], + feed=feeder.feed(data)) + stats = {k: np.array(v).mean() for k, v in zip(keys, outs[:-1])} + train_stats.update(stats) + logs = train_stats.log() + if iter_id % 10 == 0: + strs = '{}, iter: {}, lr: {:.5f}, {}, time: {:.3f}'.format( + now_time(), iter_id, + np.mean(outs[-1]), logs, start_time - prev_start_time) + print(strs) + sys.stdout.flush() + if (iter_id + 1) % cfg.TRAIN.snapshot_iter == 0 and iter_id != 0: + save_name = "{}".format(iter_id + 1) + checkpoint.save(exe, train_prog, + os.path.join(cfg.model_save_dir, save_name)) + if (iter_id + 1) == cfg.max_iter: + checkpoint.save(exe, train_prog, + os.path.join(cfg.model_save_dir, "model_final")) + break + + end_time = time.time() + total_time = end_time - start_time + last_loss = np.array(outs[0]).mean() + + if cfg.use_pyreader: + train_loop_pyreader() + else: + train_loop() + + +if __name__ == '__main__': + args = parse_args() + print_arguments(args) + check_gpu(args.use_gpu) + train() diff --git a/PaddleCV/rrpn/utility.py b/PaddleCV/rrpn/utility.py new file mode 100755 index 00000000..d737d3e7 --- /dev/null +++ b/PaddleCV/rrpn/utility.py @@ -0,0 +1,188 @@ +# 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. +""" +Contains common utility functions. +""" + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +import sys +import paddle.fluid as fluid +import distutils.util +import numpy as np +import six +import argparse +import functools +import collections +import datetime +from collections import deque +from paddle.fluid import core +from collections import deque +from config import * + + +def print_arguments(args): + """Print argparse's arguments. + + Usage: + + .. code-block:: python + + parser = argparse.ArgumentParser() + parser.add_argument("name", default="Jonh", type=str, help="User name.") + args = parser.parse_args() + print_arguments(args) + + :param args: Input argparse.Namespace for printing. + :type args: argparse.Namespace + """ + print("----------- Configuration Arguments -----------") + for arg, value in sorted(six.iteritems(vars(args))): + print("%s: %s" % (arg, value)) + print("------------------------------------------------") + + +def add_arguments(argname, type, default, help, argparser, **kwargs): + """Add argparse's argument. + + Usage: + + .. code-block:: python + + parser = argparse.ArgumentParser() + add_argument("name", str, "Jonh", "User name.", parser) + args = parser.parse_args() + """ + type = distutils.util.strtobool if type == bool else type + argparser.add_argument( + "--" + argname, + default=default, + type=type, + help=help + ' Default: %(default)s.', + **kwargs) + + +class SmoothedValue(object): + """Track a series of values and provide access to smoothed values over a + window or the global series average. + """ + + def __init__(self, window_size): + self.deque = deque(maxlen=window_size) + + def add_value(self, value): + self.deque.append(value) + + def get_median_value(self): + return np.median(self.deque) + + +def now_time(): + return datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f') + + +class TrainingStats(object): + def __init__(self, window_size, stats_keys): + self.smoothed_losses_and_metrics = { + key: SmoothedValue(window_size) + for key in stats_keys + } + + def update(self, stats): + for k, v in self.smoothed_losses_and_metrics.items(): + v.add_value(stats[k]) + + def get(self, extras=None): + stats = collections.OrderedDict() + if extras: + for k, v in extras.items(): + stats[k] = v + for k, v in self.smoothed_losses_and_metrics.items(): + stats[k] = round(v.get_median_value(), 3) + + return stats + + def log(self, extras=None): + d = self.get(extras) + strs = ', '.join(str(dict({x: y})).strip('{}') for x, y in d.items()) + return strs + + +def parse_args(): + """return all args + """ + parser = argparse.ArgumentParser(description=__doc__) + add_arg = functools.partial(add_arguments, argparser=parser) + # yapf: disable + # ENV + add_arg('use_gpu', bool, True, "Whether use GPU.") + add_arg('model_save_dir', str, 'output', "The path to save model.") + add_arg('pretrained_model', str, 'ResNet50_cos_pretrained', "The init model path.") + add_arg('dataset', str, 'icdar2015', "icdar2015, icdar2017.") + add_arg('class_num', int, 2, "Class number.") + add_arg('data_dir', str, 'dataset/icdar2015', "The data root path.") + add_arg('use_pyreader', bool, False, "Use pyreader.") + add_arg('use_profile', bool, False, "Whether use profiler.") + add_arg('padding_minibatch',bool, False, + "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.") + #SOLVER + add_arg('learning_rate', float, 0.02, "Learning rate.") + add_arg('max_iter', int, 17500, "Iter number.") + add_arg('log_window', int, 20, "Log smooth window, set 1 for debug, set 20 for train.") + # RCNN + # RPN + add_arg('anchor_sizes', int, [128, 256, 512], "The size of anchors.") + add_arg('aspect_ratios', float, [0.2, 0.5,1.0], "The ratio of anchors.") + add_arg('anchor_angle', float, [-30.0, 0.0, 30.0, 60.0, 90.0, 120.0], "The angles of anchors.") + add_arg('variance', float, [1.0, 1.0, 1.0, 1.0, 1.0], "The variance of anchors.") + add_arg('rpn_stride', float, [16.,16.], "Stride of the feature map that RPN is attached.") + add_arg('rpn_nms_thresh', float, 0.7, "NMS threshold used on RPN proposals") + # TRAIN VAL INFER + add_arg('im_per_batch', int, 1, "Minibatch size.") + add_arg('pixel_means', float, [0.485, 0.456, 0.406], "pixel mean") + add_arg('nms_thresh', float, 0.3, "NMS threshold.") + add_arg('score_thresh', float, 0.01, "score threshold for NMS.") + add_arg('snapshot_stride', int, 1000, "save model every snapshot stride.") + # SINGLE EVAL AND DRAW + add_arg('draw_threshold', float, 0.8, "Confidence threshold to draw bbox.") + add_arg('image_path', str, 'ICDAR2015/tmp/', "The image path used to inference and visualize.") + # yapf: enable + args = parser.parse_args() + file_name = sys.argv[0] + if 'train' in file_name or 'profile' in file_name: + merge_cfg_from_args(args, 'train') + else: + merge_cfg_from_args(args, 'val') + return args + + +def check_gpu(use_gpu): + """ + Log error and exit when set use_gpu=true in paddlepaddle + cpu version. + """ + err = "Config use_gpu cannot be set as true while you are " \ + "using paddlepaddle cpu version ! \nPlease try: \n" \ + "\t1. Install paddlepaddle-gpu to run model on GPU \n" \ + "\t2. Set use_gpu as false in config file to run " \ + "model on CPU" + + try: + if use_gpu and not fluid.is_compiled_with_cuda(): + logger.error(err) + sys.exit(1) + except Exception as e: + pass -- GitLab